1. 程式人生 > >10.基於FFMPEG+SDL2播放video---音視訊同步(參考音訊時鐘)

10.基於FFMPEG+SDL2播放video---音視訊同步(參考音訊時鐘)

繼續FFMPEG學習之路。。。

參考資料:

  1. An ffmpeg and SDL Tutorial

文章目錄

1 綜述

前面在寫了使用FFMPEG+SDL2播放音訊,視訊的demo,接下來則需要將音訊視訊合入同時進行播放,在簡單的將兩份程式碼合入之後,除錯了一番,發現音訊視訊可以正常播放,但是並沒有同步,兩者之間的獨立的兩個部分,這樣就會導致畫面和人的口型對不上,看著很不舒服,這時候就需要音視訊同步了。

所謂的音視訊同步,顧名思義就是要音訊的播放速度跟上視訊的播放速度,或者說視訊的播放速度跟上音訊的播放速度,這樣就會使當前的畫面和播放的聲音一致。

2 音視訊同步

我們知道,視訊有幀率的概念,表示一秒顯示的幀數,例如25FPS表示一秒顯示25幀影象;音訊有采樣率的概念,表示每秒播放的樣本的個數,例如一段音訊音訊引數為:48khz 32bit, 則每秒播放的音訊位元組數為 48000*(32/8)= 192000 。因此如果我們保持每秒視訊播放25幀,音訊播放192000位元組資料,這樣即便音訊和視訊播放是獨立的,理論上也應該可以達到同步的目的。理想是美好的,但是隨著時間的逐漸增大,誤差就會逐漸增大,慢慢就會出現音視訊不會同步的現象。

那麼如何才能做到音視訊同步吶?既然音訊和視訊都有與時間相關的一些概念,那麼能否有一個獨立的時間引數,音訊和視訊都參考這個獨立的時間引數,播放快了就減慢播放,播放慢了就加快播放,這樣音視訊就可以參考這個時間引數實現同步播放。

獨立的時間引數可以有一下三個選擇:
參考音訊時鐘:即以音訊的播放速度為準,將視訊同步到音訊上,視訊播放快了就減慢播放速度,視訊播放慢了就加快播放速度。
參考視訊時鐘:即以視訊的播放速度為準,將音訊同步到視訊上。
參考外部時鐘:即設定一個外部的獨立時鐘,將音訊和視訊同步到此時鐘上。

本文中的同步是參考音訊時鐘的。

3 DTS 和 PTS

要實現上面講的參考音訊時鐘,那麼使用每秒播放25幀的這個引數就不容易實現了,因此我們需要視訊中更精確的時間量來和音訊時鐘同步。在視訊中,有DTS和PTS的概念,DTS(Decoding Time Stamp)解碼時間戳,用於告訴解碼器解碼的順序;PTS(Presentation Time Stamp)顯示時間戳,用於表示解碼後的資料顯示順序。

關於DTS和PTS,網上有很多詳細的資料,這裡不再描述,我們只需要知道我們可以使用PTS和音訊時鐘進行參考,從而判斷下一幀的時間,從而實現音視訊同步。

4 音訊時鐘

首先,我們先了解音訊時鐘相關的。
既然以音訊時鐘為準,那麼我們就需要實時的更新音訊時鐘,方便視訊可以獲取到最新的時鐘,因此在audio_decode_frame函式中,每取出一個packet,就要更新audio clock,如下:

		/* if update, update the audio clock w/pts */
		if(pkt->pts != AV_NOPTS_VALUE) 
		{
			/* 獲取真正的時間 */
			pState->audioClock = av_q2d(pState->pAudioStream->time_base)*pkt->pts;
			//printf("pts is %f, av is %f, clock is %f\n", pkt->pts, av_q2d(pState->pAudioStream->time_base),
			//	pState->audioClock );
		}

根據前面的文章,我們知道可能一個音訊packet裡面包含多幀音訊資料,因此在此函式中,每解碼一幀的資料,就要更新audio clock,如下:

			/* Keep audio_clock up-to-date */
			pts = pState->audioClock;
			*pts_ptr = pts;
			n = 2 * pState->pAudioStream->codec->channels;
			pState->audioClock += (double)data_size /
					(double)(n * pState->pAudioStream->codec->sample_rate);

其中,data_size為當前幀的大小, n * pState->pAudioStream->codec->sample_rate 為每秒播放的音訊資料量, (double)data_size /
(double)(n * pState->pAudioStream->codec->sample_rate);
則是當前解碼的音訊幀需要播放的時間,單位s.

注意:關於上面pts, dts, av_q2d相關知識,可以參考:https://blog.csdn.net/bixinwei22/article/details/78770090
這篇博文已經介紹的很詳細。

當視訊需要參考時鐘的時候,卻不能直接返回audio clock,因為上面介紹了當前的audio clock為當前音訊幀播放完時候的時鐘,但是如果當前緩衝區裡面還有音訊資料,就需要減去這些音訊資料佔據的時間,才是當前音訊的clock,如下:

double getAudioClock(VideoState *pState) 
{
	double pts;
	int hwBufSize = 0;        //當前剩餘的要播放的資料
	int bytesPerSec = 0;
	int n = 0;

	pts = pState->audioClock; /* maintained in the audio thread */
	
	hwBufSize = pState->audioBufSize - pState->audioBufIndex;
	
	n = pState->pAudioStream->codec->channels * 2;
	if(pState->pAudioStream) 
	{
		bytesPerSec = pState->pAudioStream->codec->sample_rate * n;
	}
	
	if(bytesPerSec) 
	{
		pts -= (double)hwBufSize / bytesPerSec;
	}
	
	return pts;
}

其中pts -= (double)hwBufSize / bytesPerSec; 即為減去緩衝區裡面剩餘音訊資料要播放需要的時間。

5 視訊PTS

根據博文:https://blog.csdn.net/bixinwei22/article/details/78770090 裡面介紹,當我們獲取到視訊幀的PTS後,就可以得到其顯示時間(單位s),如下:
time(second) = st->duration * av_q2d(st->time_base)

在我們的解碼執行緒中,在將解碼後的資料放入到PacketQueue佇列前,需要獲取其顯示時間,如下:

		/* get PTS */
		if (packet->dts != AV_NOPTS_VALUE)
		{
			pts = av_frame_get_best_effort_timestamp(pFrame);
		}
		else
		{
			pts = 0;
		}

		pts *= av_q2d(pState->pVideoStream->time_base);
		
		if(got_picture)
		{
			pts = synchronizeVideo(pState, pFrame, pts);
			if ((queuePicture(pState, pFrame, pts)) < 0)
				break;
		}

上面的流程為,獲取視訊PTS—>獲取顯示時間—>矯正時間—>放入佇列

上面的矯正時間,是有可能呼叫av_frame_get_best_effort_timestamp時候沒有獲取到正確的PTS,那麼就需要在synchronizeVideo中進行矯正。

6 同步

現在我們已經獲取到每幀視訊的顯示時間,並且能夠實時的同步音訊時鐘,接下來就是視訊顯示的該如何參考音訊時鐘?

我們的videoRefreshTimer函式的作用是重新整理定時器並將視訊顯示到螢幕上,因此我們的同步工作就在這個函式裡面進行。

所謂的同步其實是根據時間戳和音訊時鐘來計算下一幀視訊顯示的時間,設定定時器,從而通過快了變慢下一幀播放,慢了加快下一幀播放來實現同步。

大致思路為:
① 用當前PTS減去上一幀的PTS,獲取一個值delay
② 將當前要顯示的視訊的pts減去當前音訊的時鐘,獲取一個值diff
③ 將delay 和diff進行比較,如果diff小於0,說明當前要顯示的視訊播放的慢了,下一幀需要提前播放了;如果diff大於delay,說明當前要顯示的視訊播放的快了,下一幀需要慢點播放

程式碼如下:

			delay = pVideoPic->pts - pState->frameLastPTS;
			if ((delay <= 0) || (delay >= 1.0))
			{
				/* if incorrect delay, use previous one */
				delay = pState->frameLastDelay;
			}

			pState->frameLastDelay = delay;
			pState->frameLastPTS = pVideoPic->pts;

			/* update delay to sync to audio */
			refClock = getAudioClock(pState);
			diff = pVideoPic->pts - refClock;

			/* Skip or repeat the frame. Take delay into account
	 			FFPlay still doesn't "know if this is the best guess." */
			syncThreshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
			if(fabs(diff) < AV_NOSYNC_THRESHOLD) 
			{
				if(diff <= -syncThreshold)       //視訊顯示的慢了,下一幀快一點
				{
					delay = 0;
				} 
				else if(diff >= syncThreshold)   //視訊顯示的快了,下一幀慢一點
				{
					delay = 2 * delay;
				}
			}

			pState->frameTimer += delay;
			
			/* computer the REAL delay */
			actualDelay = pState->frameTimer - (av_gettime() / 1000000.0);
			if (actualDelay <= 0.01)
			{
				actualDelay = 0.01;
			}
			
			schduleRefresh(pState, (int)(actualDelay * 1000 + 0.5));

其中,frameTimer 為視訊播放到現在的延遲時間總和,這個值減去當前的時間即為下一幀的播放時間;AV_NOSYNC_THRESHOLD為0.01,這個是參考ffplay裡面來做的,保證值不能小於0.01,並且設定最小的重新整理值為0.01.

如上,便是音視訊同步的整個過程了。

7 不足

在程式碼中只是實現了基本的功能,在一些細節方面沒有優化:
①去初始化功能沒有做好
②程式碼在一個cpp檔案中,太冗雜,可以按照其功能進行拆分
③沒有新增必要的列印資訊
④程式碼編解碼細節方面還可以優化

接下來,則需要對這些方面進行修改。

8 工程

最後放上完整的工程,在vs2010上測試ok.
基於FFMPEG_SDL2_音視訊播放_參考音訊時鐘