直播帶貨平臺開發,實現音影片同步演算法

雲豹科技曉彤發表於2021-07-05

      本文是對音影片同步演算法的總結,以閱讀 ffplay.c 原始碼為基礎,結合各位博主的分析, 逐漸深入理解同步演算法原理, 並根據自身理解, 編寫一套簡易的影片播放器,用於驗證直播帶貨平臺開發的音影片同步演算法。

ffplay簡介

    ffplay FFmpeg 提供的開源播放器,基於 FFmpeg SDL 進行影片播放, 是研究影片播放器,音影片同步演算法的很好的示例。 ffplay 原始碼涉及到很多音影片的基本概念, 在基礎理論缺乏的情況下分析起來並不容易,在分析 ffplay 原始碼之前,要對音影片的相關概念有所瞭解,關於音影片的基本知識,在網路上有很多,也可以參考我的其他文章,這些也是我在學習中的經驗總結。

     ffmpeg4.1.3 中, ffplay 原始碼約 3700 行,非常的小巧,網上關於 ffplay 原理分析的文章也有很多,這裡不再詳細的分析 ffplay 的原始碼, 僅按照自己的理解對音影片同步演算法進行總結, 並基於 ffplay ,自己動手編寫一個簡易影片播放器, 對音影片同步演算法進行驗證。

為什麼要做音影片同步

     直播帶貨平臺開發中,如果僅僅是影片按幀率播放,音訊按取樣率播放,二者沒有同步機制,即使一開始音影片是同步的,隨著時間的流逝,音影片會漸漸失去同步,並且不同步的現象會隨著時間會越來越嚴重。這是因為:

      一、播放時間難以精確控制

      二、異常、誤差會隨時間累積。

所以,必須直播帶貨平臺開發要採用一定的同步策略,不斷對音影片的時間差作校正,使影像顯示與聲音播放總體保持一致。

音影片同步演算法

音影片同步演算法的核心在於準確計算出音訊與影片播放時間的偏差, 再根據這個偏差對雙方進行調整,確保雙方在你追我趕的過程中保持同步。

1. 音影片同步介紹

         影片同步到音訊:即以音訊為主時間軸作為同步源

         音訊同步到影片:即以影片為主時間軸作為同步源

         音訊和影片同步到系統時鐘:即以系統時鐘為主時間軸作為同步源

    ffplay 預設採用第一種同步方式,本節主要闡述影片同步到音訊方式。為什麼大多播放器要採用影片同步到音訊呢,因為音訊的取樣率是固定的,若音訊稍有卡頓,都會很明顯的聽出來,反則影片則不如此,雖然表面上說的是 25P (每秒 25 幀),不一定每一幀的間隔就必須精確到 40ms (所以每幀間隔大約 40ms ,事實上,也很難做到精確的 40ms ),即便偶爾影片間隔延時大了點或小了點,人眼也是察覺不出來的,所以影片的幀率可以是動態的,並不是嚴格標準的!

     影片同步到音訊,即以音訊作為主時間軸, 儘量不去干擾音訊的播放,音訊採用獨立的執行緒獨自解碼播放 ( 音訊播放的速度在引數設定完畢後是固定的,因此我們也很容易計算音訊播放的時間 ), 在整個過程中,根據影片與音訊時間差,來決策如何改變影片的播放速度,來確保影片與音訊時間差控制在一定範圍內, 當偏移在 -90ms (音訊滯後於影片)到 +20ms (音訊超前影片)之間時,人感覺不到視聽質量的變化,這個區域可以認為是同步區域;當偏移在 -185 +90 之外時,音訊和影片會出現嚴重的不同步現象,此區域認為是不同步區域。這裡我們認為偏移 diff ‘± 一個影片幀間隔 範圍內即認為是同步的,如下圖所示:

2. 音影片時間偏差計算

直播帶貨平臺開發同步系統的關鍵,就在於計算影片與音訊時間偏差diff , 在 ffplay.c 原始碼中,是透過函式 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;

}

 

根據自身的理解,結合實際測試,得出diff 的計算方法:

當前影片幀pts frame->pts * av_q2d(video_st->time_base)

當前直播帶貨平臺影片幀至今流逝的時間: 代表當前影片幀從開始顯示到現在的時間, ffplay 中函式 get_clock(&is->vidclk) 給出了具體實現,在本次實驗中, 透過 nowtime - last_showtime 來表示流逝的時間, 由於我們是在影片顯示後立即計算 diff , 這個流逝的時間幾乎可以忽略不計,可以使用 0 表示。

音訊幀播放時間 = 音訊長度 / 取樣率

當前音訊幀播放完畢時間= 當前音訊幀的 pts + 當前音訊幀長度 / 取樣率

                                           = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;

       (在計算音訊幀長度時需要考慮取樣率, 通道數, 樣本格式)

  音訊緩衝區中未播放資料的時間: 在 ffplay.c 中,採用如下公式來獲取:   

set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);

 

          緩衝區資料總長度 =SDL A B 緩衝區總長度  +   當前音訊幀尚未複製到 SDL 緩衝區的剩餘長度 aduio_write_buf_size

到這裡,我們就可以計算得到音影片的播放時間偏差diff , 結合上面的偏差圖,我們很容易判斷出是影片落後於音訊,還是音訊落後於影片。

3. 量化影片播放的時間延時

透過第2 步我們已經計算出音影片的時間偏差, 接下來我們就要根據這個偏差來量化影片延時的時間, 來控制下一個影片幀顯示的時間。

我們參考ffplay.c 中的程式碼片段:

            last_duration = vp_duration(is, lastvp, vp);

            delay = compute_target_delay(last_duration, is);

 

            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;

            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)

                is->frame_timer = time;

 

remaining_time 為下一幀播放的延時時間, ffplay.c 藉助 frame_timer += delay 來記錄當前影片累計播放的時間。

     frame_timer + delay - av_gettime_relative()/1000000.0 :代表下一影片幀需要延時的時間,這裡需要減去當前時間,是為了得到定時器或 delay 的時間。

      另外, 我們約定任意兩個影片幀的間隔至少為 10ms, 所以才有了:

            *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);

4. 編寫簡易的影片播放器

    ffplay.c 中的同步演算法對於初學者而言理解起來還是有些難度的, 結合自身對 ffplay.c 原始碼的閱讀,以及音影片同步演算法的理解, 對上述同步程式碼進行精簡, 亦能達到音影片同步的效果程式碼片段如下。

 

        if( pm->av_sync_type == AV_SYNC_AUDIO_MASTER){//

 

            master_clock = get_master_clock(pm);

 

            diff = vp->pts - master_clock;

 

            printf("vps:%lf, audioclock:%lf, diff:%lf\n", vp->pts, master_clock, diff);

 

            sync_threshold = (delay > AV_SYNC_THRESHOLD)?delay:AV_SYNC_THRESHOLD;

 

  

 

            if( diff <= -sync_threshold){

 

                delay = 0;

 

            }else if( diff >= sync_threshold){

 

                delay *= 2;

 

            }

 

            

 

        }

 

我們直接根據直播帶貨平臺開發的diff 的值來決策下一幀要延時的時間。

————————————————

宣告:本文由雲豹科技轉發自 揮劍踏蒼穹 部落格,如有侵權請聯絡作者刪除

原文連結:https://blog.csdn.net/u011734326/article/details/97137998

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70002045/viewspace-2779779/,如需轉載,請註明出處,否則將追究法律責任。

相關文章