ffplay默認也是採用的這種同步策略。

主流程

ffplay中將視頻同步到音頻的主要方案是,如果視頻播放過快,則重複播放上一幀,以等待音頻;如果視頻播放過慢,則丟幀追趕音頻。

這一部分的邏輯實現在視頻輸出函數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源碼,或閱讀我的這篇分析:zhuanlan.zhihu.com/p/44

這段代碼的邏輯在上述流程圖中有包含。主要思路就是一開始提到的如果視頻播放過快,則重複播放上一幀,以等待音頻;如果視頻播放過慢,則丟幀追趕音頻。實現的方式是,參考audio clock,計算上一幀(在屏幕上的那個畫面)還應顯示多久(含幀本身時長),然後與系統時刻對比,是否該顯示下一幀了。

這裡與系統時刻的對比,引入了另一個概念——frame_timer。可以理解為幀顯示時刻,如更新前,是上一幀的顯示時刻;對於更新後(is->frame_timer += delay),則為當前幀顯示時刻。

上一幀顯示時刻加上delay(還應顯示多久(含幀本身時長))即為上一幀應結束顯示的時刻。具體原理看如下示意圖:

這裡給出了3種情況的示意圖:

  • time1:系統時刻小於lastvp結束顯示的時刻(frame_timer+dealy),即虛線圓圈位置。此時應該繼續顯示lastvp
  • time2:系統時刻大於lastvp的結束顯示時刻,但小於vp的結束顯示時刻(vp的顯示時間開始於虛線圓圈,結束於黑色圓圈)。此時既不重複顯示lastvp,也不丟棄vp,即應顯示vp
  • time3:系統時刻大於vp結束顯示時刻(黑色圓圈位置,也是nextvp預計的開始顯示時刻)。此時應該丟棄vp。

delay的計算

那麼接下來就要看最關鍵的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即可,沒有區分的必要)

至此,基本上分析完了視頻同步音頻的過程,簡單總結下:

  • 基本策略是:如果視頻播放過快,則重複播放上一幀,以等待音頻;如果視頻播放過慢,則丟幀追趕音頻。
  • 這一策略的實現方式是:引入frame_timer概念,標記幀的顯示時刻和應結束顯示的時刻,再與系統時刻對比,決定重複還是丟幀。
  • lastvp的應結束顯示的時刻,除了考慮這一幀本身的顯示時長,還應考慮了video clock與audio clock的差值。
  • 並不是每時每刻都在同步,而是有一個「準同步」的差值區域。

推薦閱讀:

相關文章