WebRTC 音影片同步原理與實現

所有的基於網路傳輸的音影片採集播放系統都會存在音影片同步的問題,作為現代互聯網實時音影片通訊系統的代表,WebRTC 也不例外。本文將對音影片同步的原理以及 WebRTC 的實現做深入分析。

時間戳 (timestamp)

同步問題就是快慢的問題,就會牽扯到時間跟音影片流媒體的對應關係,就有了時間戳的概念。

時間戳用來定義媒體負載數據的取樣時刻,從單調線性遞增的時鐘中獲取,時鐘的精度由 RTP 負載數據的取樣頻率決定。音頻和影片的取樣頻率是不一樣的,一般音頻的取樣頻率有 16KHz、44.1KHz、48KHz 等,而影片反映在取樣幀率上,一般幀率有 25fps、29.97fps、30fps 等。

習慣上音頻的時間戳的增速就是其取樣率,比如 16KHz 取樣,每 10ms 採集一幀,則下一幀的時間戳,比上一幀的時間戳,從數值上多 16 x10=160,即音頻時間戳增速為 16/ms。而影片的取樣頻率習慣上是按照 90KHz 來計算的,就是每秒 90K 個時鐘 tick,之所以用 90K 是因為它正好是上面所說的影片幀率的倍數,所以就採用了 90K。所以影片幀的時間戳的增長速率就是 90/ms。

時間戳的生成

音頻幀時間戳的生成

WebRTC 的音頻幀的時間戳,從第一個包為 0,開始累加,每一幀增加 = 編碼幀長 (ms) x 取樣率 / 1000,如果取樣率 16KHz,編碼幀長 20ms,則每個音頻幀的時間戳遞增 20 x 16000/1000 = 320。這裡只是說的未打包之前的音頻幀的時間戳,而封裝到 RTP 包裡面的時候,會將這個音頻幀的時間戳再累加上一個隨機偏移量(構造函數里生成),然後作為此 RTP 包的時間戳,發送出去,如下面程式碼所示,注意,這個邏輯同樣適用於影片包。

影片幀時間戳的生成

WebRTC 的影片幀,生成機制跟音頻幀完全不同。影片幀的時間戳來源於系統時鐘,採集完成後至編碼之前的某個時刻(這個傳遞鏈路非常長,不同配置的影片幀,走不同的邏輯,會有不同的獲取位置),獲取當前系統的時間 timestamp_us_ ,然後算出此系統時間對應的 ntp_time_ms_ ,再根據此 ntp 時間算出原始影片幀的時間戳 timestamp_rtp_ ,參看下面的程式碼,計算邏輯也在 OnFrame 這個函數中。

為什麼影片幀採用了跟音頻幀不同的時間戳電腦制呢?我的理解,一般情況音頻的採集設備的取樣間隔和時鐘精度更加準確,10ms 一幀,每秒是 100 幀,一般不會出現大的抖動,而影片幀的幀間隔時間較大採集精度,每秒 25 幀的話,就是 40ms 一幀。如果還採用音頻的按照取樣率來遞增的話,可能會出現跟實際時鐘對不齊的情況,所以就直接每取一幀,按照取出時刻的系統時鐘算出一個時間戳,這樣可以再現真實影片幀跟實際時間的對應關係。

跟上面音頻一樣,在封裝到 RTP 包的時候,會將原始影片幀的時間戳累加上一個隨機偏移量(此偏移量跟音頻的並不是同一個值),作為此 RTP 包的時間戳發送出去。值得注意的是,這裡計算的 NTP 時間戳根本就不會隨著 RTP 數據包一起發送出去,因為 RTP 包的包頭裡面沒有 NTP 欄位,即使是擴展欄位里,我們也沒有放這個值,如下面影片的時間相關的擴展欄位。

音影片同步核心依據

從上面可以看出,RTP 包裡面只包含每個流的獨立的、單調遞增的時間戳資訊,也就是說音頻和影片兩個時間戳完全是獨立的,沒有關係的,無法只根據這個資訊來進行同步,因為無法對兩個流的時間進行關聯,我們需要一種映射關係,將兩個獨立的時間戳關聯起來。

這個時候 RTCP 包裡面的一種發送端報告分組 SR (SenderReport) 包就上場了,詳情請參考 RFC3550

SR 包的其中一個作用就是來告訴我們每個流的 RTP 包的時間戳和 NTP 時間的對應關係的。靠的就是上邊圖片中標出的 NTP 時間戳和 RTP 時間戳,通過 RFC3550 的描述,我們知道這兩個時間戳對應的是同一個時刻,這個時刻表示此 SR 包生成的時刻。這就是我們對音影片進行同步的最核心的依據,所有的其它計算都是圍繞這個核心依據來展開的。

SR 包的生成

由上面論述可知,NTP 時間和 RTP 時間戳是同一時刻的不同表示,只是精度和單位不一樣。NTP 時間是絕對時間,以毫秒為單位,而 RTP 時間戳則和媒體的取樣頻率有關,是一個單調遞增數值。生成 SR 包的過程在 RTCPSender::BuildSR(const RtcpContext& ctx) 函數裡面,老版本裡面有 bug,寫死了取樣率為 8K,新版本已經修復,下面截圖是老版本的程式碼:

計算的思路如下

首先,我們要獲取當前時刻(即 SR 包生成時刻)的 NTP 時間。這個直接從傳過來的參數 ctx 中就可以獲得:

其次,我們要計算當前時刻,應該對應的 RTP 的時間戳是多少。根據最後一個發送的 RTP 包的時間戳 last_rtp_timestamp_ 和它的採集時刻的系統時間 last_frame_capture_time_ms_,和當前媒體流的時間戳的每 ms 增長速率 rtp_rate ,以及從 last_frame_capture_time_ms_ 到當前時刻的時間流逝,就可以算出來。注意,last_rtp_timestamp_ 是媒體流的原始時間戳,不是經過隨機偏移的 RTP 包時間戳,所以最後又累加了偏移量 timestamp_offset_ 。其中最後一個發送的 RTP 包的時間資訊是通過下面的函數進行更新的:

音影片同步的計算

因為同一台機器上音頻流和影片流的本地系統時間是一樣的,也就是系統時間對應的 NTP 格式的時間也是一樣的,是在同一個坐標繫上的,所以可以把 NTP 時間作為橫軸 X,單位是 ms,而把 RTP 時間戳的值作為縱軸 Y,畫在一起。下圖展示了計算音影片同步的原理和方法,其實很簡單,就是使用最近的兩個 SR 點,兩點確定一條直線,之後給任意一個 RTP 時間戳,都可以求出對應的 NTP 時間,又因為影片和音頻的 NTP 時間是在同一基準上的,所以就可以算出兩者的差值。

上圖以音頻的兩個 SR 包為例,確定出了 RTP 和 NTP 對應關係的直線,然後給任意一個 rtp_a,就算出了其對應的 NTP_a,同理也可以求任意影片包 rtp_v 對應的 NTP_v 的時間點,兩個的差值就是時間差。

下面是 WebRTC 裡面計算直線對應的係數 rate 和偏移 offset 的程式碼:

在 WebRTC 中計算的是最新收到的音頻 RTP 包和最新收到的影片 RTP 包的對應的 NTP 時間,作為網路傳輸引入的不同步時長,然後又根據當前音頻和影片的 JitterBuffer 和播放緩衝區的大小,得到了播放引入的不同步時長,根據兩個不同步時長,得到了最終的音影片不同步時長,計算過程在 StreamSynchronization::ComputeRelativeDelay() 函數中,之後又經過了 StreamSynchronization::ComputeDelays() 函數對其進行了指數平滑等一系列的處理和判斷,得出最終控制音頻和影片的最小延時時間,分別通過 syncable_audio_->SetMinimumPlayoutDelay(target_audio_delay_ms)syncable_video_->SetMinimumPlayoutDelay(target_video_delay_ms) 應用到了音影片的播放緩衝區。

這一系列操作都是由定時器調用 RtpStreamsSynchronizer::Process() 函數來處理的。

另外需要注意一下,在知道取樣率的情況下,是可以通過一個 SR 包來計算的,如果沒有 SR 包,是無法進行準確的音影片同步的

WebRTC 中實現音影片同步的手段就是 SR 包,核心的依據就是 SR 包中的 NTP 時間和 RTP 時間戳。最後的兩張 NTP 時間-RTP 時間戳 坐標圖如果你能看明白(其實很簡單,就是求解出直線方程來計算 NTP),那麼也就真正的理解了 WebRTC 中音影片同步的原理。如果有什麼遺漏或者錯誤,歡迎大家一起交流!

「影片雲技術」你最值得關注的音影片技術公眾號,每周推送來自阿里雲一線的實踐技術文章,在這裡與音影片領域一流工程師交流切磋。