面試官: 談下音視頻同步原理,音頻和視頻能絕對同步嗎
心理分析:音視頻同步本身比較難,一般使用ijkplayer 第三方做音視頻同步。不排除有視頻直播 視頻通話需要用音視頻同步,可以從三種 音頻爲準 視頻爲準 自定義時鐘爲準三種方式實現音視頻同步
**求職者: **如果被問到 放正心態,能回答多少是多少。如果你看了這篇文章肯定是可以回答上的
音視頻的直播系統是一個複雜的工程系統,要做到非常低延遲的直播,需要複雜的系統工程優化和對各組件非常熟悉的掌握。下面整理幾個簡單常用的調優技巧:
以fflay來看音視頻同步流程
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值需要重新計算)
}
}
}
這段代碼的邏輯在上述流程圖中有包含。主要思路就是一開始提到的如果視頻播放過快,則重複播放上一幀,以等待音頻;如果視頻播放過慢,則丟幀追趕音頻。實現的方式是,參考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 don't 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\n",
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的差值。
- 並不是每時每刻都在同步,而是有一個“準同步”的差值區域。
後續
如果對音視頻感興趣學習的小夥伴可以加入我們們一起學習交流;歡迎大家在評論區討論
粉絲裙: