天天看點

ffmpeg開發播放器學習筆記 - 音視訊同步

作者:音視訊流媒體技術

該節是ffmpeg開發播放器學習筆記的第六節《音視訊同步》

一般來說,視訊同步指的是視訊和音頻同步,也就是說播放的聲音要和目前顯示的畫面保持一緻。想象以下,看一部電影的時候隻看到人物嘴動沒有聲音傳出;或者畫面是激烈的戰鬥場景,而聲音不是槍炮聲卻是人物說話的聲音,這是非常差的一種體驗。

ffmpeg開發播放器學習筆記 - 音視訊同步

✅ 第一節 - Hello FFmpeg

✅ 第二節 - 軟解視訊流,渲染 RGB24

✅ 第三節 - 認識YUV

✅ 第四節 - 硬解碼,OpenGL渲染YUV

✅ 第五節 - Metal 渲染YUV

✅ 第六節 - 解碼音頻,使用AudioQueue 播放

第七節 - 音視訊同步

第八節 - 完善播放控制

第九節 - 倍速播放

第十節 - 增加視訊過濾效果

第十一節 - 音頻變聲

該節 Demo 位址:github.com/czqasngit/f…

執行個體代碼提供了Objective-C與Swift兩種實作,為了友善說明,文章引用的是Objective-C代碼,因為Swift代碼指針看着不簡潔。

目标

  • 音視訊同步的背景以及産生不同步的原因
  • 音視訊同步的處理方案及選擇
  • 編碼實作音視訊同步

音視訊同步的背景以及産生不同步的原因

在視訊流和音頻流中已包含了其以怎樣的速度播放的相關資料,視訊的幀率(Frame Rate)訓示視訊一秒顯示的幀數(圖像數);音頻的采樣率(Sample Rate)表示音頻一秒播放的樣本(Sample)的個數。可以使用以上資料通過簡單的計算得到其在某一Frame(Sample)的播放時間,以這樣的速度音頻和視訊各自播放互不影響,在理想條件下,其應該是同步的,不會出現偏差。如果用上面那種簡單的計算方式,慢慢的就可能 會出現音視訊不同步的情況。要不是視訊播放快了,要麼是音頻播放快了。這就需要一種随着時間會線性增長的量,視訊和音頻的播放速度都以該量為标準,播放快了就減慢播放速度;播放快了就加快播放的速度。是以,視訊和音頻的同步實際上是一個動态的過程,同步是暫時的,不同步則是常态。以選擇的播放速度量為标準,快的等待慢的,慢的則加快速度,是一個你等我趕的過程。

音視訊同步的處理方案及選擇

處理音視訊同步的方案通常有以下三種:

1.視訊時鐘同步到音頻時鐘

以音頻時鐘為标準時鐘,音頻自然播放。視訊幀播放時判斷目前視訊幀播放結束後的時間與目前的音頻時鐘時間對比,如果視訊目前幀播放完時間比音頻時鐘時間早,則讓目前視訊播放線程暫時時間差,以保證播放完後與音頻時鐘同步。如果目前視訊幀播放完時間比音頻時間晚,則丢棄目前視訊幀讀取下一幀再判斷,以保證播放完後與音頻時鐘同步。

2.音頻時鐘同步到視訊時鐘

以視訊時鐘為标準,視訊自然播放。同步邏輯則與第1點的同步邏輯一緻:即音頻快了就暫停音頻播放線程等待時間差,慢了則丢棄目前音頻幀。以保證目前音頻幀播放完與視訊幀時鐘同步。

3.以外部時鐘為準,音頻與視訊時鐘同時同步到外部時鐘。

同步邏輯與1、2點一緻。需要注意的是外部時鐘應盡量使用毫秒時鐘以確定同步的精準。

同步方案選擇

以上3種方案都可以實作音頻與視訊的同步處理,但怎麼選擇更适合的方案呢?

  • 人的眼睛與耳朵對圖像與聲音的敏感程度不一樣,當畫面偶爾缺少一幀或者幾幀時人的眼睛可能不太容易察覺。這是因為畫面的連貫性比較強,兩幀畫面之前的差異有時候很小,眼睛比耳機敏感度更低。當聲音發生一變化,比如缺失了一點聲音或者聲音異常的,人的耳朵馬上就察覺到了。
  • 在大多數平台上聲音的播放開銷都比渲染畫面小。聲音的資料處理過程更簡單,數量量也更小。聲音線程播放聲音卡頓的機率很小。
  • 在macOS/iOS平台上,以AudioQueue播放為例,聲音的播放緩存對象是重複利用的,而這個利用則是由實際播放聲音的具體線程來回調的。不同于視訊每一幀的渲染,聲音的暫停與丢棄相比視訊實作成本更高。

綜合上以的三點,本文選擇第1點同步方案視訊時鐘同步到音頻時鐘

相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】

【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~

ffmpeg開發播放器學習筆記 - 音視訊同步

編碼實作音視訊同步

音頻視訊同步基礎

FFmpeg裡有兩種時間戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顧名思義,前者是解碼的時間,後者是顯示的時間。要仔細了解這兩個概念,需要先了解FFmpeg中的packet和frame的概念。

FFmpeg中用AVPacket結構體來描述解碼前或編碼後的壓縮包,用AVFrame結構體來描述解碼後或編碼前的信号幀。 對于視訊來說,AVFrame就是視訊的一幀圖像。這幀圖像什麼時候顯示給使用者,就取決于它的PTS。DTS是AVPacket裡的一個成員,表示這個壓縮包應該什麼時候被解碼。 如果視訊裡各幀的編碼是按輸入順序(也就是顯示順序)依次進行的,那麼解碼和顯示時間應該是一緻的。可事實上,在大多數編解碼标準(如H.264或HEVC)中,編碼順序和輸入順序并不一緻。 于是才會需要PTS和DTS這兩種不同的時間戳。

基本概念:

I幀 :幀内編碼幀 又稱intra picture,I 幀通常是每個 GOP(MPEG 所使用的一種視訊壓縮技術)的第一個幀,經過适度地壓縮,做為随機通路的參考點,可以當成圖象。I幀可以看成是一個圖像經過壓縮後的産物。

P幀: 前向預測編碼幀 又稱predictive-frame,通過充分将低于圖像序列中前面已編碼幀的時間備援資訊來壓縮傳輸資料量的編碼圖像,也叫預測幀;

B幀: 雙向預測内插編碼幀 又稱bi-directional interpolated prediction frame,既考慮與源圖像序列前面已編碼幀,也顧及源圖像序列後面已編碼幀之間的時間備援資訊來壓縮傳輸資料量的編碼圖像,也叫雙向預測幀;

PTS:Presentation Time Stamp。PTS主要用于度量解碼後的視訊幀什麼時候被顯示出來

DTS:Decode Time Stamp。DTS主要是辨別讀入記憶體中的bit流在什麼時候開始送入解碼器中進行解碼。

在沒有B幀存在的情況下DTS的順序和PTS的順序應該是一樣的。

IPB幀的不同:

I幀:自身可以通過視訊解壓算法解壓成一張單獨的完整的圖檔。

P幀:需要參考其前面的一個I frame 或者B frame來生成一張完整的圖檔。

B幀:則要參考其前一個I或者P幀及其後面的一個P幀來生成一張完整的圖檔。

兩個I幀之間形成一個GOP,在x264中同時可以通過參數來設定bf的大小,即:I 和p或者兩個P之間B的數量。 通過上述基本可以說明如果有B frame 存在的情況下一個GOP的最後一個frame一定是P。

DTS和PTS的不同:

DTS主要用于視訊的解碼,在解碼階段使用.PTS主要用于視訊的同步和輸出.在display的時候使用.在沒有B frame的情況下.DTS和PTS的輸出順序是一樣的.

2.音頻與視訊資料緩沖對象增加時鐘資料

@interface FFQueueAudioObject : NSObject
@property (nonatomic, assign, readonly)float pts;
@property (nonatomic, assign, readonly)float duration;
- (instancetype)initWithLength:(int64_t)length pts:(float)pts duration:(float)duration;
- (uint8_t *)data;
- (int64_t)length;
- (void)updateLength:(int64_t)length;
@end           
@interface FFQueueVideoObject : NSObject
@property (nonatomic, assign)double unit;
@property (nonatomic, assign)double pts;
@property (nonatomic, assign)double duration;
- (instancetype)init;
- (AVFrame *)frame;
@end           

分别在上面的音頻與視訊緩沖對象上增加變量pts與duration。

  • pts: 目前幀播放或顯示的時間。
  • duration: 目前幀播放或顯示持續的時長。音頻幀内包括多個音頻資料包,而視訊則可以通過FPS計算得到每一幀的顯示持續時長。

3.視訊時鐘同步到音頻時鐘

pthread_mutex_lock(&(self->mutex));
/// 讀取目前的音頻時鐘時間
double ac = self->audio_clock;
pthread_mutex_unlock(&(self->mutex));
FFQueueVideoObject *obj = NULL;
/// 統計路過的視訊幀數量
int readCount = 0;
/// 首先讀取一幀視訊資料
obj = [self.videoFrameCacheQueue dequeue];
readCount ++;
/// 計算目前視訊帖播放結束時的時間點
double vc = obj.pts + obj.duration;
if(ac - vc > self->tolerance_scope) {
    /// 視訊太慢,丢棄目前幀繼續讀取下一幀
    /// 這裡認為讀取下一幀或者更下一幀不會造成視訊緩沖隊列枯竭,是以未做等待處理
    /// 因為時時同步能形成的時間差比較有限
    while (ac - vc > self->tolerance_scope) {
        FFQueueVideoObject *_nextObj = [self.videoFrameCacheQueue dequeue];
        if(!_nextObj) break;
        obj = _nextObj;
        vc = obj.pts + obj.duration;
        readCount ++;
    }
} else if (vc - ac > self->tolerance_scope) {
  /// 視訊太快,暫停一下再接着渲染顯示目前視訊幀
    float sleep_time = vc - ac;
    usleep(sleep_time * 1000 * 1000);
} else {
  /// 在誤差範圍之後, 不需要處理
}           

tolerance_scope為可允許的誤內插補點,即音頻與視訊時間差小于這個資料則認為是同步的。這是因為要達到絕對的時間一緻性是不可能的,在計算時間的過程中有精度的丢失。

  • 擷取目前音頻時鐘的時間(該時間為目前音頻幀播放結束後的時間)
  • 讀取一幀視訊幀,計算出該視訊幀播放完之後的時間
  • 判斷音頻時間與視訊時間的內插補點,進行同步處理

到此,音視訊同步的完成了,現在再去看視訊就不會發現嘴巴與聲音不一緻的問題了 。

總結

  • 了解了音視訊同步的背景以及産生不同步的原因
  • 了解音視訊同步的處理方案及合理的選擇了視訊時鐘同步到音頻時鐘的方案
  • 編碼實作音視訊同步

更多内容請關注微信公衆号<<程式猿搬磚>>

原文 https://juejin.cn/post/6927164687368847367

繼續閱讀