字節跳動-談下音視頻同步原理,音頻和視頻能絕對同步嗎?

面試官: 談下音視頻同步原理,音頻和視頻能絕對同步嗎

心理分析:音視頻同步本身比較難,一般使用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(還應顯示多久(含幀本身時長))即爲上一幀應結束顯示的時刻。具體原理看如下示意圖:

img

這裏給出了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,畫個圖幫助理解:

img

圖中座標軸是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的差值。
  • 並不是每時每刻都在同步,而是有一個“準同步”的差值區域。

後續

如果對音視頻感興趣學習的小夥伴可以加入我們們一起學習交流;歡迎大家在評論區討論

粉絲裙:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章