ffplay默認也是採用的這種同步策略。
ffplay中將視頻同步到音頻的主要方案是,如果視頻播放過快,則重複播放上一幀,以等待音頻;如果視頻播放過慢,則丟幀追趕音頻。
這一部分的邏輯實現在視頻輸出函數video_refresh中,分析代碼前,我們先來回顧下這個函數的流程圖:
video_refresh
在這個流程中,「計算上一幀顯示時長」這一步驟至關重要。先來看下代碼:
static void video_refresh(void *opaque, double *remaining_time) { //…… //lastvp上一幀,vp當前幀 ,nextvp下一幀
last_duration = vp_duration(is, lastvp, vp);//計算上一幀的持續時長 delay = compute_target_delay(last_duration, is);//參考audio clock計算上一幀真正的持續時長
time= av_gettime_relative()/1000000.0;//取系統時刻 if (time < is->frame_timer + delay) {//如果上一幀顯示時長未滿,重複顯示上一幀 *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time); goto display; }
is->frame_timer += delay;//frame_timer更新為上一幀結束時刻,也是當前幀開始時刻 if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) is->frame_timer = time;//如果與系統時間的偏離太大,則修正為系統時間
//更新video clock //視頻同步音頻時沒作用 SDL_LockMutex(is->pictq.mutex); if (!isnan(vp->pts)) update_video_pts(is, vp->pts, vp->pos, vp->serial); SDL_UnlockMutex(is->pictq.mutex);
//……
//丟幀邏輯 if (frame_queue_nb_remaining(&is->pictq) > 1) { Frame *nextvp = frame_queue_peek_next(&is->pictq); duration = vp_duration(is, vp, nextvp);//當前幀顯示時長 if(time > is->frame_timer + duration){//如果系統時間已經大於當前幀,則丟棄當前幀 is->frame_drops_late++; frame_queue_next(&is->pictq); goto retry;//回到函數開始位置,繼續重試(這裡不能直接while丟幀,因為很可能audio clock重新對時了,這樣delay值需要重新計算) } } }
代碼只保留了同步相關的部分,完整的代碼可以參考ffmpeg源碼,或閱讀我的這篇分析:https://zhuanlan.zhihu.com/p/44122324
這段代碼的邏輯在上述流程圖中有包含。主要思路就是一開始提到的如果視頻播放過快,則重複播放上一幀,以等待音頻;如果視頻播放過慢,則丟幀追趕音頻。實現的方式是,參考audio clock,計算上一幀(在屏幕上的那個畫面)還應顯示多久(含幀本身時長),然後與系統時刻對比,是否該顯示下一幀了。
這裡與系統時刻的對比,引入了另一個概念——frame_timer。可以理解為幀顯示時刻,如更新前,是上一幀的顯示時刻;對於更新後(is->frame_timer += delay),則為當前幀顯示時刻。
is->frame_timer += delay
上一幀顯示時刻加上delay(還應顯示多久(含幀本身時長))即為上一幀應結束顯示的時刻。具體原理看如下示意圖:
這裡給出了3種情況的示意圖:
那麼接下來就要看最關鍵的lastvp的顯示時長delay是如何計算的。
這在函數compute_target_delay中實現:
static double compute_target_delay(double delay, VideoState *is) { double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */ if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) { /* if video is slave, we try to correct big delays by duplicating or deleting a frame */ diff = get_clock(&is->vidclk) - get_master_clock(is);
/* skip or repeat frame. We take into account the delay to compute the threshold. I still dont know if it is the best guess */ sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay)); if (!isnan(diff) && fabs(diff) < is->max_frame_duration) { if (diff <= -sync_threshold) delay = FFMAX(0, delay + diff); else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) delay = delay + diff; else if (diff >= sync_threshold) delay = 2 * delay; } }
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f ", delay, -diff);
return delay; }
上面代碼中的注釋全部是源碼的注釋,代碼不長,注釋佔了快一半,可見這段代碼重要性。
這段代碼中最難理解的是sync_threshold,畫個圖幫助理解:
圖中坐標軸是diff值大小,diff為0表示video clock與audio clock完全相同,完美同步。圖紙下方色塊,表示要返回的值,色塊值的delay指傳入參數,結合上一節代碼,即lastvp的顯示時長。
從圖上可以看出來sync_threshold是建立一塊區域,在這塊區域內無需調整lastvp的顯示時長,直接返回delay即可。也就是在這塊區域內認為是準同步的。
如果小於-sync_threshold,那就是視頻播放較慢,需要適當丟幀。具體是返回一個最大為0的值。根據前面frame_timer的圖,至少應更新畫面為vp。
如果大於sync_threshold,那麼視頻播放太快,需要適當重複顯示lastvp。具體是返回2倍的delay,也就是2倍的lastvp顯示時長,也就是讓lastvp再顯示一幀。
如果不僅大於sync_threshold,而且超過了AV_SYNC_FRAMEDUP_THRESHOLD,那麼返回delay+diff,由具體diff決定還要顯示多久(這裡不是很明白代碼意圖,按我理解,統一處理為返回2*delay,或者delay+diff即可,沒有區分的必要)
至此,基本上分析完了視頻同步音頻的過程,簡單總結下:
推薦閱讀: