音視頻開發---音視頻同步算法

 

目錄

ffplay簡介

爲什麼要做音視頻同步

音視頻同步算法

參考


      本文是對音視頻同步算法的總結,以閱讀ffplay.c源碼爲基礎,結合各位博主的分析, 逐漸深入理解同步算法原理, 並根據自身理解, 編寫一套簡易的視頻播放器,用於驗證音視頻同步算法。

ffplay簡介

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

    在ffmpeg4.1.3中,ffplay源碼約3700行,非常的小巧,網上關於ffplay原理分析的文章也有很多,諸如:

             雷神的博客

             ITRonnie ffplay系列博客

             ffplay源碼分析

比較系統的介紹了ffplay,是學習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://www.cnblogs.com/my_life/articles/6842155.html

ffplay播放器音視頻同步原理: https://blog.csdn.net/lrzkd/article/details/78661841

ffplay:  https://juejin.im/user/5cac7dc26fb9a06885399b1c/posts

ffplay.c 音視頻同步

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