所有的基於網路傳輸的音視訊採集播放系統都會存在音視訊同步的問題,作為現代網際網路實時音視訊通訊系統的代表,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 中音視訊同步的原理。如果有什麼遺漏或者錯誤,歡迎大家一起交流!
「視訊雲技術」你最值得關注的音視訊技術公眾號,每週推送來自阿里雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。