基於FFmpeg開發視頻播放器,音視頻同步(四)

爲什麼需要音視頻同步?

從前面的代碼可以看到,播放的過程有解碼線程不斷的把解碼好的AVFrame數據放入隊列,然後播放線程從隊列中取出解碼後的數據,經過格式轉換,分別送給ANativeWindow去繪製,送給OpenSlES去播放聲音,這個過程如果不去控制,播放的速度就取決與解碼線程,播放線程的處理速度,及系統的性能.這樣播放的效果,肯定是不流暢的.

爲了讓播放儘可能流暢,就要把視頻播放的幀率考慮進來,比如希望fps是30,那麼就在繪製時的間隔控制在1/30.

加入繪製間隔的控制,雖然視頻播放比流暢了,但是畫面跟音頻沒有保持一致.音頻與視頻各播各的,由於機器運行速度,解碼效率等種種造成時間差異的因素影響,即使最初音視頻是基本同步的,也會隨着時間的流逝逐漸失去同步。所以,必須要採用一定的同步策略,不斷對音視頻的時間差作校正,使圖像顯示與聲音播放總體保持一致。所以需要做音視頻的同步.

音視頻的同步,有三種方式:
1、參考一個外部時鐘,將音頻與視頻同步至此時間;
2、以視頻爲基準,音頻去同步視頻的時間;
3、以音頻爲基準,視頻去同步音頻的時間。

由於人對聲音的變化相對於視覺更加敏感。所以頻繁的去調整聲音的播放會感覺刺耳或雜音影響用戶體驗。所以一般情況下,播放器使用第三種同步方式。

在音視頻同步的處理中,有一個音視頻時鐘的概念,通過AVFrame->pts來獲取,所以先說下PTS想關幾個概念:

視頻中的I P B幀:

I 幀:幀內編碼幀 ,一個圖像經過壓縮後的產物,包含一幅完整的圖像信息;

P 幀: 前向預測編碼幀,利用之前的I幀或P幀進行預測編碼

B 幀: 雙向預測內插編碼幀 ,利用之前和之後的I幀或P幀進行雙向預測編碼。

IDR幀:一個序列的第一個圖像叫做IDR幀(立即刷新圖像),IDR 幀都是I幀幀。H.264引入IDR幀是爲了解碼的重同步,當解碼器解碼到IDR幀時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這裏獲得重新同步的機會。IDR幀之後的圖像永遠不會使用IDR之前的圖像數據來解碼。

音視頻中時間戳:

PTS:Presentation Time Stamp。顯示時間戳,表示顯示順序。

DTS:Decode Time Stamp。解碼時間戳,表示解碼順序

在沒有B幀存在的情況下DTS的順序和PTS的順序應該是一樣的。
音頻中DTS和PTS是相同的,視頻中由於可能存在B幀,含B幀的視頻PTS與DTS不同。

顯示順序,解碼順序可以藉助下圖理解:

在視頻編碼序列中,GOP即Group of picture(圖像組),指兩個I幀之間的距離.

假如得到一段視頻數據,他的幀類型是I B B P B B...

首先 是把I 幀送給解碼器,所以他的 解碼順序 , 顯示順序 , DTS, PTS 都是1,

      然後是第一個 B幀,雖然這個B幀顯示順序是2, 但是解碼順序時3,因爲他要參考後面的P幀,要等P幀解碼了,才能解碼這個B幀,

      第二個B幀也是一個道理,雖然這個B幀顯示順序是3, 但是解碼順序是4,因爲他要參考後面的P幀,要等P幀解碼了,才能解碼這個B幀,

     第一個P幀, 雖然這個P幀顯示順序是4, 但是解碼順序時2,這樣才能解碼I 和P幀之間的B幀.

其中DTS,在這裏不需要關注,把AVPacket送給解碼器,解碼器會按照DTS的順序去解碼.

繪製視頻時,需要考慮PTS,他決定了兩張圖片之間的顯示間隔,也即是說,當要顯示下一張圖片時,需要休眠多少,除了考慮幀率,還要考慮PTSD的值.

理解了這幾個概念,下面看代碼:

首先是獲取音頻的時鐘:

這個函數是從拿到AVFrame數據,轉換成OpenSLES需要的格式時調用的.只關注其中clock屬性:

//使用轉換器,把frame_queue中的數據,轉成我們需要的。把轉換後的數據放入buffer,返回值表示轉換數據的大小。
int AudioChannel::_getData() {
    AVFrame *frame = 0;
    while (isPlaying) {
        //獲取這段音頻的時刻,pts表示這一幀的時間戳,以time_base爲單位的時間戳,time_base是AVRational結構體類型,
        // 也就是pts的單位是 (AVRational.Numerator / AVRational.Denominator),這樣下面得出的時間單位是秒。
        clock = frame->pts * av_q2d(time_base);
    }
}

然後,視頻播放這邊的處理:

1, 根據幀率,再參考額外延遲時間repeat_pict,讓視頻播放更流暢。delay是要讓視頻以正常的速度播放,

2, 根據一個閾值範圍,

#define AV_SYNC_THRESHOLD_MIN 0.04

#define AV_SYNC_THRESHOLD_MAX 0.1

調整音視頻的時間差.

代碼很容易看的懂,以音頻的時鐘爲基準,視頻快了,多休眠一會,視頻慢了,少休眠一會,讓他們的時間差保持一個合理的範圍(0.04 ~ 0.1).

void VideoChannel::_play() {
    AVFrame *frame = 0;
    double frame_delay = 1.0 / fps;
    while (isPlaying) {

      //根據幀率,再參考額外延遲時間repeat_pict,讓視頻播放更流暢。delay是要讓視頻以正常的速度播放,
        double extra_delay = frame->repeat_pict / (2*fps);
        double delay = extra_delay + frame_delay;
        if (audioChannel) {
            //處理音視頻同步,best_effort_timestamp跟pts通常是一致的,
            // 區別是best_effort_timestamp經過了一些參考,得到一個最優的時間
            clock = frame->best_effort_timestamp * av_q2d(time_base); //視頻的時鐘,
            double diff = clock - audioChannel->clock;
            //音頻,視頻的時間戳 的差,這個差有一個允許的範圍(0.04 ~ 0.1)
            double sync = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
            if (diff <= -sync) {
                delay = FFMAX(0, delay + diff); //視頻慢了
            } else if (diff > sync) {
                delay = delay + diff;
            }
            LOGE("clock ,video:%1f ,audio:%1f, delay:%1f, V-A = %1f ",clock, audioChannel->clock, delay, diff);
        }

        av_usleep(delay * 1000000);
    }
}

  

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