音視頻同步原理及實現


本文主要描述音視頻同步原理,及常見的音視頻同步方案,並以代碼示例,展示如何以音頻的播放時長為基准,將視頻同步到音頻上以實現視音頻的同步播放。內容如下:

 

1.音視頻同步簡單介紹

對於一個播放器,一般來說,其基本構成均可划分為以下幾部分:
數據接收(網絡/本地)->解復用->音視頻解碼->音視頻同步->音視頻輸出。
基本框架如下圖所示:

播放器基本框圖

為什么需要音視頻同步?
媒體數據經過解復用流程后,音頻/視頻解碼便是獨立的,也是獨立播放的。而在音頻流和視頻流中,其播放速度都是有相關信息指定的:

  • 視頻:幀率,表示視頻一秒顯示的幀數。
  • 音頻:采樣率,表示音頻一秒播放的樣本的個數。

在這里插入圖片描述
從幀率及采樣率,即可知道視頻/音頻播放速度。聲卡和顯卡均是以一幀數據來作為播放單位,如果單純依賴幀率及采樣率來進行播放,在理想條件下,應該是同步的,不會出現偏差。
以一個44.1KHz的AAC音頻流和24FPS的視頻流為例:
一個AAC音頻frame每個聲道包含1024個采樣點,則一個frame的播放時長(duration)為:(1024/44100)×1000ms = 23.22ms;一個視頻frame播放時長(duration)為:1000ms/24 = 41.67ms。理想情況下,音視頻完全同步,音視頻播放過程如下圖所示:
在這里插入圖片描述
但實際情況下,如果用上面那種簡單的方式,慢慢的就會出現音視頻不同步的情況,要不是視頻播放快了,要么是音頻播放快了。可能的原因如下:

  1. 一幀的播放時間,難以精准控制。音視頻解碼及渲染的耗時不同,可能造成每一幀輸出有一點細微差距,長久累計,不同步便越來越明顯。(例如受限於性能,42ms才能輸出一幀)

  2. 音頻輸出是線性的,而視頻輸出可能是非線性,從而導致有偏差。

  3. 媒體流本身音視頻有差距。(特別是TS實時流,音視頻能播放的第一個幀起點不同)

所以,解決音視頻同步問題,引入了時間戳:
首先選擇一個參考時鍾(要求參考時鍾上的時間是線性遞增的);
編碼時依據參考時鍾上的給每個音視頻數據塊都打上時間戳;
播放時,根據音視頻時間戳及參考時鍾,來調整播放。
所以,視頻和音頻的同步實際上是一個動態的過程,同步是暫時的,不同步則是常態。以參考時鍾為標准,放快了就減慢播放速度;播放快了就加快播放的速度。

接下來,我們介紹媒體流中時間戳的概念。

2.DTS和PTS簡介

2.1I/P/B幀

在介紹DTS/PTS之前,我們先了解I/P/B幀的概念。I/P/B幀本身和音視頻同步關系不大,但理解其概念有助於我們了解DTS/PTS存在的意義。
視頻本質上是由一幀幀畫面組成,但實際應用過程中,每一幀畫面會進行壓縮(編碼)處理,已達到減少空間占用的目的。

編碼方式可以分為幀內編碼和幀間編碼。
內編碼方式:
即只利用了單幀圖像內的空間相關性,對冗余數據進行編碼,達到壓縮效果,而沒有利用時間相關性,不使用運動補償。所以單靠自己,便能完整解碼出一幀畫面。
幀間編碼:
由於視頻的特性,相鄰的幀之間其實是很相似的,通常是運動矢量的變化。利用其時間相關性,可以通過參考幀運動矢量的變化來預測圖像,並結合預測圖像與原始圖像的差分,便能解碼出原始圖像。所以,幀間編碼需要依賴其他幀才能解碼出一幀畫面。

由於編碼方式的不同,視頻中的畫面幀就分為了不同的類別,其中包括:I 幀、P 幀、B 幀。I 幀、P 幀、B 幀的區別在於:

  • I 幀(Intra coded frames)

    I 幀圖像采用幀I 幀使用幀內壓縮,不使用運動補償,由於 I 幀不依賴其它幀,可以獨立解碼。I 幀圖像的壓縮倍數相對較低,周期性出現在圖像序列中的,出現頻率可由編碼器選擇。

  • P 幀(Predicted frames)

    P 幀采用幀間編碼方式,即同時利用了空間和時間上的相關性。P 幀圖像只采用前向時間預測,可以提高壓縮效率和圖像質量。P 幀圖像中可以包含幀內編碼的部分,即 P 幀中的每一個宏塊可以是前向預測,也可以是幀內編碼。

  • B 幀(Bi-directional predicted frames)

    B 幀圖像采用幀間編碼方式,且采用雙向時間預測,可以大大提高壓縮倍數。也就是其在時間相關性上,還依賴后面的視頻幀,也正是由於 B 幀圖像采用了后面的幀作為參考,因此造成視頻幀的傳輸順序和顯示順序是不同的。

也就是說,一個 I 幀可以不依賴其他幀就解碼出一幅完整的圖像,而 P 幀、B 幀不行。P 幀需要依賴視頻流中排在它前面的幀才能解碼出圖像。B 幀則需要依賴視頻流中排在它前面或后面的I/P幀才能解碼出圖像。
對於I幀和P幀,其解碼順序和顯示順序是相同的,但B幀不是,如果視頻流中存在B幀,那么就會打算解碼和顯示順序。
正因為解碼和顯示的這種非線性關系,所以需要DTS、PTS來標識幀的解碼及顯示時間。

2.2時間戳DTS、PTS

  • DTS(Decoding Time Stamp):即解碼時間戳,這個時間戳的意義在於告訴播放器該在什么時候解碼這一幀的數據。
  • PTS(Presentation Time Stamp):即顯示時間戳,這個時間戳用來告訴播放器該在什么時候顯示這一幀的數據。

當視頻流中沒有 B 幀時,通常 DTS 和 PTS 的順序是一致的。但如果有 B 幀時,就回到了我們前面說的問題:解碼順序和播放順序不一致了,即視頻輸出是非線性的。
比如一個視頻中,幀的顯示順序是:I B B P,因為B幀解碼需要依賴P幀,因此這幾幀在視頻流中的順序可能是:I P B B,這時候就體現出每幀都有 DTS 和 PTS 的作用了。DTS 告訴我們該按什么順序解碼這幾幀圖像,PTS 告訴我們該按什么順序顯示這幾幀圖像。順序大概如下:
IPB順序
從流分析工具看,流中P幀在B幀之前,但顯示確實在B幀之后。
P幀在前,顯示時間在后 B幀在后,顯示時間在前
需要注意的是:雖然 DTS、PTS 是用於指導播放端的行為,但它們是在編碼的時候由編碼器生成的。
以我們最常見的TS為例:

TS流中,PTS/DTS信息在打流階段生成在PES層,主要是在PES頭信息里。
在這里插入圖片描述
在這里插入圖片描述
標志第一位是PTS標識,第二位是DTS標識。
標志:
00,表示無PTS無DTS;
01,錯誤,不能只有DTS沒有PTS;
10,有PTS;
11,有PTS和DTS。
PTS有33位,但是它不是直接的33位數據,而是占了5個字節,PTS分別在這5字節中取。
在這里插入圖片描述
TS的I/P幀攜帶PTS/DTS信息,B幀PTS/DTS相等,進保留PTS;由於聲音沒有用到雙向預測,它的解碼次序就是它的顯示次序,故它只有PTS。

TS的編碼器中有一個系統時鍾STC(其頻率是27MHz),此時鍾用來產生指示音視頻的正確顯示和解碼時間戳。
PTS域在PES中為33bits,是對系統時鍾的300分頻的時鍾的計數值。它被編碼成為3個獨立的字段:
PTS[32…30][29…15][14…0]。
DTS域在PES中為33bits,是對系統時鍾的300分頻的時鍾的計數值。它被編碼成為3個獨立的字段:
DTS[32…30][29…15][14…0]。
因此,對於TS流,PTS/DTS時間基均為1/90000秒(27MHz經過300分頻)。
PTS對於TS流的意義不僅在於音視頻同步,TS流本身不攜帶duration(可播放時長)信息,所以計算duration也是根據PTS得到。

附上TS流解析PTS示例:

#define MAKE_WORD(h, l) (((h) << 8) | (l))
//packet為PES
int64_t get_pts(const uint8_t *packet)
{
    const uint8_t *p = packet;
    if(packet == NULL) {
        return -1;
    }

    if(!(p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01)) {	//pes sync word
        return -1;
    }
    p += 3; //jump pes sync word
    p += 4; //jump stream id(1) pes length(2) pes flag(1)

    int pts_pts_flag = *p >> 6;
    p += 2; //jump pes flag(1) pes header length(1)
    if (pts_pts_flag & 0x02) {
        int64_t pts32_30, pts29_15, pts14_0, pts;
		pts32_30 = (*p) >> 1 & 0x07; 
        p += 1;
        pts29_15 = (MAKE_WORD(p[0],p[1])) >> 1;
        p += 2;
        pts14_0  = (MAKE_WORD(p[0],p[1])) >> 1;
        p += 2;
        pts = (pts32_30 << 30) | (pts29_15 << 15) | pts14_0;
        
        return pts;
    }
    return -1;
}
 
  •  

3.常用同步策略

前面已經說了,實現音視頻同步,在播放時,需要選定一個參考時鍾,讀取幀上的時間戳,同時根據的參考時鍾來動態調節播放。現在已經知道時間戳就是PTS,那么參考時鍾的選擇一般來說有以下三種:

  1. 將視頻同步到音頻上:就是以音頻的播放速度為基准來同步視頻。
  2. 將音頻同步到視頻上:就是以視頻的播放速度為基准來同步音頻。
  3. 將視頻和音頻同步外部的時鍾上:選擇一個外部時鍾為基准,視頻和音頻的播放速度都以該時鍾為標准。

當播放源比參考時鍾慢,則加快其播放速度,或者丟棄;快了,則延遲播放。

這三種是最基本的策略,考慮到人對聲音的敏感度要強於視頻,頻繁調節音頻會帶來較差的觀感體驗,且音頻的播放時鍾為線性增長,所以一般會以音頻時鍾為參考時鍾,視頻同步到音頻上。
在實際使用基於這三種策略做一些優化調整,例如:

  • 調整策略可以盡量采用漸進的方式,因為音視頻同步是一個動態調節的過程,一次調整讓音視頻PTS完全一致,沒有必要,且可能導致播放異常較為明顯。
  • 調整策略僅僅對早到的或晚到的數據塊進行延遲或加快處理,有時候是不夠的。如果想要更加主動並且有效地調節播放性能,需要引入一個反饋機制,也就是要將當前數據流速度太快或太慢的狀態反饋給“源”,讓源去放慢或加快數據流的速度。
    在這里插入圖片描述
  • 對於起播階段,特別是TS實時流,由於視頻解碼需要依賴第一個I幀,而音頻是可以實時輸出,可能出現的情況是視頻PTS超前音頻PTS較多,這種情況下進行同步,勢必造成較為明顯的慢同步。處理這種情況的較好方法是將較為多余的音頻數據丟棄,盡量減少起播階段的音視頻差距。

4.音視頻同步簡單示例代碼

代碼參考ffplay實現方式,同時加入自己的修改。以audio為參考時鍾,video同步到音頻的示例代碼:

  1. 獲取當前要顯示的video PTS,減去上一幀視頻PTS,則得出上一幀視頻應該顯示的時長delay;
  2. 當前video PTS與參考時鍾當前audio PTS比較,得出音視頻差距diff;
  3. 獲取同步閾值sync_threshold,為一幀視頻差距,范圍為10ms-100ms;
  4. diff小於sync_threshold,則認為不需要同步;否則delay+diff值,則是正確糾正delay;
  5. 如果超過sync_threshold,且視頻落后於音頻,那么需要減小delay(FFMAX(0, delay + diff)),讓當前幀盡快顯示。
    如果視頻落后超過1秒,且之前10次都快速輸出視頻幀,那么需要反饋給音頻源減慢,同時反饋視頻源進行丟幀處理,讓視頻盡快追上音頻。因為這很可能是視頻解碼跟不上了,再怎么調整delay也沒用。
  6. 如果超過sync_threshold,且視頻快於音頻,那么需要加大delay,讓當前幀延遲顯示。
    將delay*2慢慢調整差距,這是為了平緩調整差距,因為直接delay+diff,會讓畫面畫面遲滯。
    如果視頻前一幀本身顯示時間很長,那么直接delay+diff一步調整到位,因為這種情況再慢慢調整也沒太大意義。
  7. 考慮到渲染的耗時,還需進行調整。frame_timer為一幀顯示的系統時間,frame_timer+delay- curr_time,則得出正在需要延遲顯示當前幀的時間。
{
    video->frameq.deQueue(&video->frame);
    //獲取上一幀需要顯示的時長delay
    double current_pts = *(double *)video->frame->opaque;
    double delay = current_pts - video->frame_last_pts;
    if (delay <= 0 || delay >= 1.0)
    {
        delay = video->frame_last_delay;
    }
   
    // 根據視頻PTS和參考時鍾調整delay
    double ref_clock = audio->get_audio_clock();
    double diff = current_pts - ref_clock;// diff < 0 :video slow,diff > 0 :video fast
    //一幀視頻時間或10ms,10ms音視頻差距無法察覺
    double sync_threshold = FFMAX(MIN_SYNC_THRESHOLD, FFMIN(MAX_SYNC_THRESHOLD, delay)) ;
    
    audio->audio_wait_video(current_pts,false);
    video->video_drop_frame(ref_clock,false);
    if (!isnan(diff) && fabs(diff) < NOSYNC_THRESHOLD) // 不同步
    {
        if (diff <= -sync_threshold)//視頻比音頻慢,加快
        {
            delay = FFMAX(0,  delay + diff);
            static int last_delay_zero_counts = 0;
            if(video->frame_last_delay <= 0)
            {
                last_delay_zero_counts++;
            }
            else
            {
                last_delay_zero_counts = 0;
            }
            if(diff < -1.0 && last_delay_zero_counts >= 10)
            {
                printf("maybe video codec too slow, adjust video&audio\n");
                #ifndef DORP_PACK
                audio->audio_wait_video(current_pts,true);//差距較大,需要反饋音頻等待視頻
                #endif          
                video->video_drop_frame(ref_clock,true);//差距較大,需要視頻丟幀追上
            }
        }    
        //視頻比音頻快,減慢
        else if (diff >= sync_threshold && delay > SYNC_FRAMEDUP_THRESHOLD)
            delay = delay + diff;//音視頻差距較大,且一幀的超過幀最常時間,一步到位
        else if (diff >= sync_threshold)
            delay = 2 * delay;//音視頻差距較小,加倍延遲,逐漸縮小
    }

    video->frame_last_delay = delay;
    video->frame_last_pts = current_pts;

    double curr_time = static_cast<double>(av_gettime()) / 1000000.0;
    if(video->frame_timer == 0)
    {
        video->frame_timer = curr_time;//show first frame ,set frame timer
    }

    double actual_delay = video->frame_timer + delay - curr_time;
    if (actual_delay <= MIN_REFRSH_S)
    {
        actual_delay = MIN_REFRSH_S;
    }
    usleep(static_cast<int>(actual_delay * 1000 * 1000));
    //printf("actual_delay[%lf] delay[%lf] diff[%lf]\n",actual_delay,delay,diff);
    // Display
    SDL_UpdateTexture(video->texture, &(video->rect), video->frame->data[0], video->frame->linesize[0]);
    SDL_RenderClear(video->renderer);
    SDL_RenderCopy(video->renderer, video->texture, &video->rect, &video->rect);
    SDL_RenderPresent(video->renderer);
    video->frame_timer = static_cast<double>(av_gettime()) / 1000000.0 ;
    
    av_frame_unref(video->frame);
    
    //update next frame
    schedule_refresh(media, 1);
}
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM