在網(wǎng)絡(luò)視頻會(huì)議中, 我們常會(huì)遇到音視頻不同步的問(wèn)題, 我們有一個(gè)專(zhuān)有名詞 lip-sync 唇同步來(lái)描述這類(lèi)問(wèn)題,當(dāng)我們看到人的嘴唇動(dòng)作與聽(tīng)到的聲音對(duì)不上的時(shí)候,不同步的問(wèn)題就出現(xiàn)了
而在線會(huì)議中, 聽(tīng)見(jiàn)清晰的聲音是優(yōu)先級(jí)最高的, 人耳對(duì)于聲音的延遲是很敏感的
根據(jù) T-REC-G.114-200305 中的描述
- 大于~280ms 有些用戶(hù)就會(huì)不滿意
- 大于~380ms 多數(shù)用戶(hù)就會(huì)不滿意
- 大于~500ms 幾乎所有用戶(hù)就會(huì)不滿意
我們就盡量使得聲音的延遲在 280 ms 之內(nèi),這是解決 lip-sync 問(wèn)題的前提, 聲音不好的嚴(yán)重程序超過(guò)音視頻不同步。
我們可以定義一個(gè) sync_diff 值 來(lái)表示音頻幀和視頻幀之間的時(shí)間差
- 正值表示音頻領(lǐng)先于視頻
- 負(fù)值表示音頻落后于視頻
ITU 對(duì)此給出以下的閾值:
- 不可感知 Undetectability (-100ms, +25ms)
- 可感知 Detectability: (-125ms, +45ms)
- 可接受 Acceptability: (–185ms, +90 ms)
- 影響用戶(hù) Impact user experience (-∞, -185ms) ∪ (+90ms,∞)
(ITU-R BT.1359-1, Relative Timing of Sound and Vision for Broadcasting" 1998. Retrieved 30 May 2015)
當(dāng)我們?cè)诓シ乓粋€(gè)視頻幀及對(duì)應(yīng)的音頻幀的時(shí)候,要計(jì)算一下這個(gè) sync_diff
sync_diff = audio_frame_time - video_frame_time
如果這個(gè) sync_diff 大于 90ms, 也就是音頻包到得過(guò)早,就會(huì)有音視頻不同步的問(wèn)題 - 聲音聽(tīng)到了,嘴巴沒(méi)跟上.
如果這個(gè) sync_diff 小于 -185ms, 也就是視頻包到得過(guò)早,就會(huì)有音視頻不同步的問(wèn)題 - 嘴巴在動(dòng),聲音沒(méi)跟上.
不同步的原因

這個(gè)問(wèn)題的原因主要在于音頻的采集, 編碼,傳輸, 解碼, 播放與視頻的采集,編碼,傳輸,解碼以及渲染一般是分開(kāi)進(jìn)行的,因?yàn)橐纛l和視頻采集自不同的設(shè)備,即它們的來(lái)源不同,在網(wǎng)絡(luò)上傳輸也會(huì)有延遲,也由不同的設(shè)備進(jìn)行播放,這樣如果在接收方不采取措施進(jìn)行時(shí)間同步,就會(huì)極有可能看到口型和聽(tīng)到的聲音對(duì)不上的情況。
由此派生出 3 個(gè)小問(wèn)題:
- 如何將來(lái)自同一個(gè)人或設(shè)備的多路 audio 及 video stream關(guān)聯(lián)起來(lái)?
- 如何將 RTP 中的時(shí)間戳 timestamp 映射到發(fā)送方的音視頻采集時(shí)間
- 如何調(diào)整音頻或者視頻幀的播放時(shí)間,讓它們?cè)趺粗g相對(duì)同步?
解決方案
1. 如何將來(lái)自同一個(gè)人或設(shè)備的音視頻流關(guān)聯(lián)起來(lái)?
對(duì)于多媒體會(huì)話,每種類(lèi)型的媒體(例如音頻或視頻)一般會(huì)在單獨(dú)的 RTP 會(huì)話中發(fā)送,發(fā)送方會(huì)在 RTCP SDES 消息中指明
接收方通過(guò) CNAME 項(xiàng)關(guān)聯(lián)要同步的RTP流, 而這個(gè) CNAME 包含在發(fā)送方所發(fā)送的 RTCP SDES 中
SDES 數(shù)據(jù)包包含常規(guī)包頭,有效負(fù)載類(lèi)型為 202,項(xiàng)目計(jì)數(shù)等于數(shù)據(jù)包中 SSRC/CSRC 塊的數(shù)量,后跟零個(gè)或多個(gè) SSRC/CSRC 塊,其中包含有關(guān)特定 SSRC 或 CSRC,每個(gè)都與 32 位邊界對(duì)齊。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P| SC | PT=SDES=202 | length L |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| SSRC/CSRC_1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SDES items |
| ... |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| SSRC/CSRC_2 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SDES items |
| ... |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
CNAME 項(xiàng)在每個(gè) SDES 數(shù)據(jù)包中都是必需的,而 SDES 數(shù)據(jù)包又是每個(gè)復(fù)合 RTCP 數(shù)據(jù)包中的必需部分。
與 SSRC 標(biāo)識(shí)符一樣,CNAME 必須與其他會(huì)話參與者的 CNAME 不同。 但 CNAME 不應(yīng)隨機(jī)選擇 CNAME 標(biāo)識(shí)符,而應(yīng)允許個(gè)人或程序通過(guò) CNAME 內(nèi)容來(lái)定位其來(lái)源。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CNAME=1 | length | user and domain name ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
例如 Alice 向外發(fā)送一路音頻流,一路視頻流, 這兩路流會(huì)使用不同的 SSRC, 但是在其所發(fā)送的 RTCP SDES 消息會(huì)使用相同的 CNAME.
- RTP SSRC 1 ~ CNAME 1
- RTP SSRC 2 ~ CNAME 1
2. 同步的時(shí)間如何計(jì)算
來(lái)自同一個(gè)終端用戶(hù)的音頻和視頻, 在編碼發(fā)送的 RTP 包中有一個(gè) timestamp, 這個(gè)時(shí)間戳表示媒體流的捕捉時(shí)間。
同時(shí), 作為發(fā)送者也會(huì)發(fā)送 RTCP Sender Report, 其中包含發(fā)送的 RTP timestamp 和 NTP timestamp 的映射關(guān)系,這樣我們?cè)诮邮辗骄涂梢园?RTP 包里的

對(duì)于每個(gè) RTP 流,發(fā)送方定期發(fā)出 RTCP SR, 其中包含一對(duì)時(shí)間戳:
NTP 時(shí)間戳以及與該 RTP 流關(guān)聯(lián)的相應(yīng) RTP 時(shí)間戳。
這對(duì)時(shí)間戳傳達(dá)每個(gè)媒體流的 NTP 時(shí)間和 RTP 時(shí)間之間的關(guān)系。
先回顧一下 RTP packet 和 RTCP sender report
- RTP 包結(jié)構(gòu)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- RTCP Sender Report 結(jié)構(gòu)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
header |V=2|P| RC | PT=SR=200 | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of sender |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
sender | NTP timestamp, most significant word |
info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NTP timestamp, least significant word |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| sender's packet count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| sender's octet count |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
report | SSRC_1 (SSRC of first source) |
block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1 | fraction lost | cumulative number of packets lost |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| extended highest sequence number received |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| interarrival jitter |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| last SR (LSR) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| delay since last SR (DLSR) |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
report | SSRC_2 (SSRC of second source) |
block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2 : ... :
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| profile-specific extensions |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
通過(guò) NTP timestamp 和 RTP timestamp 之間的映射, 我們可以知道 audio 包的時(shí)間和 video 包的時(shí)間。
具體的計(jì)算可以參見(jiàn) WebRTC 的 RtpToNtpEstimator 類(lèi), 它將收到的若干 SR 中的 NTP time 和 RTP timestamp 保存下來(lái),然后 應(yīng)用最小二乘法來(lái)估算后續(xù) RTP timestamp 所對(duì)應(yīng)的 NTP timestamp, 大致為用最近 N=20 個(gè) RTCP SR 包的 ntp timestamp 和 rtp timestamp 的構(gòu)造出線性關(guān)系 y = ax + b, 通過(guò)最小二乘法來(lái)計(jì)算收到的 RTP 包對(duì)應(yīng)的 ntp timestamp.
// Converts an RTP timestamp to the NTP domain.
// The class needs to be trained with (at least 2) RTP/NTP timestamp pairs from
// RTCP sender reports before the convertion can be done.
class RtpToNtpEstimator {
public:
//...
enum UpdateResult { kInvalidMeasurement, kSameMeasurement, kNewMeasurement };
// Updates measurements with RTP/NTP timestamp pair from a RTCP sender report.
UpdateResult UpdateMeasurements(NtpTime ntp, uint32_t rtp_timestamp);
// Converts an RTP timestamp to the NTP domain.
// Returns invalid NtpTime (i.e. NtpTime(0)) on failure.
NtpTime Estimate(uint32_t rtp_timestamp) const;
// Returns estimated rtp_timestamp frequency, or 0 on failure.
double EstimatedFrequencyKhz() const;
private:
// Estimated parameters from RTP and NTP timestamp pairs in `measurements_`.
// Defines linear estimation: NtpTime (in units of 1s/2^32) =
// `Parameters::slope` * rtp_timestamp + `Parameters::offset`.
struct Parameters {
double slope;
double offset;
};
// RTP and NTP timestamp pair from a RTCP SR report.
struct RtcpMeasurement {
NtpTime ntp_time;
int64_t unwrapped_rtp_timestamp;
};
void UpdateParameters();
int consecutive_invalid_samples_ = 0;
std::list<RtcpMeasurement> measurements_;
absl::optional<Parameters> params_;
mutable RtpTimestampUnwrapper unwrapper_;
};
3. 調(diào)整播放和渲染時(shí)間
一般我們會(huì)以 audio 為主, video 向 audio 靠攏, 兩者時(shí)間一致也就會(huì)達(dá)到 lip sync 音視頻同步
- audio 包先來(lái), video 包后來(lái): audio 包放在 jitter buffer 時(shí)等一會(huì)兒, 但是這個(gè)時(shí)間是有限的, 音頻的流暢是首先要保證的, 視頻跟不上可以降低視頻的碼率
- video 包先來(lái), audio 包后來(lái): video 包要等 audio 包來(lái), 這是為了讓音視頻同步要付出的代價(jià)
一般以音頻為主流 master stream,視頻為從流 slave stream。 一般方法是接收方維護(hù)音頻流的緩沖區(qū)的管理,并通過(guò)將視頻 RTP 時(shí)間戳轉(zhuǎn)換為正確從屬于音頻流的時(shí)間戳來(lái)調(diào)整視頻流的播放。
當(dāng)帶有RTP時(shí)間戳 RTPv的視頻幀到達(dá)接收器時(shí),接收器通過(guò)四個(gè)步驟將RTP時(shí)間戳 RTPv 映射到視頻設(shè)備時(shí)間戳VTB( Video Time Base),如圖所示。
使用 Video RTCP SR 中的 RTP/NTP 時(shí)間戳對(duì)建立的映射,將視頻 RTP 時(shí)間戳 RTPv 映射到發(fā)送方 NTP 時(shí)間。
根據(jù)該 NTP 時(shí)間戳,使用 Audio RTCP SR 中的 RTP/NTP 時(shí)間戳對(duì)建立的映射,計(jì)算來(lái)自發(fā)送方的相應(yīng)音頻 RTPa 時(shí)間戳。
此時(shí),視頻RTP時(shí)間戳被映射到音頻RTP 包的相同時(shí)間基準(zhǔn)。根據(jù)該音頻 RTP 時(shí)間戳,使用卡爾曼濾波的方法計(jì)算音頻設(shè)備時(shí)間基準(zhǔn)中的相應(yīng)時(shí)間戳。 結(jié)果是音頻設(shè)備時(shí)間基準(zhǔn) ATB(Audio Time Base) 中的時(shí)間戳。
根據(jù) ATB,使用偏移量 AtoV 計(jì)算視頻設(shè)備時(shí)基 VTB 中的相應(yīng)時(shí)間戳。
接收方需要確保帶有 RTP 時(shí)間戳 RTPv 的視頻幀使用所計(jì)算出的發(fā)送方視頻設(shè)備時(shí)間基準(zhǔn) VTB 播放。
AtoV = V_time - A_Time/(audio sample rate)
注:
- AtoV: 音頻相較視頻的偏移量
- ATB: Audio device Time Base 音頻設(shè)備的時(shí)間基準(zhǔn)
- VTB: Video device Time Base 視頻設(shè)備的時(shí)間基準(zhǔn)
具體方法可以參見(jiàn) https://www.ccexpert.us/video-conferencing/using-rtcp-for-media-synchronization.html)

WebRTC 的做法原理上差不多,實(shí)現(xiàn)略有不同,可以參見(jiàn) WebRTC 的源代碼 StreamSynchronization 類(lèi)和 RtpStreamsSynchronizer 類(lèi)
大致上它會(huì)計(jì)算出 video 的延遲
current_delay_ms = max(min_playout_delay_ms, jitter_delay_ms + decode_time _ms + render_delay_ms)
然后再計(jì)算視頻相對(duì)于音頻的延遲 relative_delay_ms,
- 如果它大于0, 視頻比音頻慢,減小視頻延遲(主要是調(diào)整 jitter buffer delay),或者是增大音頻延遲, 取決于閾值 base_target_delay_ms
- 如果它小于0, 音頻比視頻慢,減小音頻延遲,或者是增大視頻延遲, 取決于閾值base_target_delay_ms
base_target_delay_ms 的比較邏輯參見(jiàn)StreamSynchronization::ComputeDelays,
if (diff_ms > 0) {
// The minimum video delay is longer than the current audio delay.
// We need to decrease extra video delay, or add extra audio delay.
if (video_delay_.extra_ms > base_target_delay_ms_) {
// We have extra delay added to ViE. Reduce this delay before adding
// extra delay to VoE.
video_delay_.extra_ms -= diff_ms;
audio_delay_.extra_ms = base_target_delay_ms_;
} else { // video_delay_.extra_ms > 0
// We have no extra video delay to remove, increase the audio delay.
audio_delay_.extra_ms += diff_ms;
video_delay_.extra_ms = base_target_delay_ms_;
}
} else { // if (diff_ms > 0)
// The video delay is lower than the current audio delay.
// We need to decrease extra audio delay, or add extra video delay.
if (audio_delay_.extra_ms > base_target_delay_ms_) {
// We have extra delay in VoiceEngine.
// Start with decreasing the voice delay.
// Note: diff_ms is negative; add the negative difference.
audio_delay_.extra_ms += diff_ms;
video_delay_.extra_ms = base_target_delay_ms_;
} else { // audio_delay_.extra_ms > base_target_delay_ms_
// We have no extra delay in VoiceEngine, increase the video delay.
// Note: diff_ms is negative; subtract the negative difference.
video_delay_.extra_ms -= diff_ms; // X - (-Y) = X + Y.
audio_delay_.extra_ms = base_target_delay_ms_;
}
}
更多細(xì)節(jié)在 WebRTC 的代碼中
- class StreamSynchronization
- class RtpStreamsSynchronizer
通過(guò)StreamSynchronization::ComputeDelays計(jì)算出音頻和視頻的相對(duì)延遲,如果相對(duì)延遲很小( < 30ms), 則無(wú)需調(diào)整音視頻的播放時(shí)間,如果相對(duì)延遲很大, 則以 80ms 的幅度進(jìn)行逐步調(diào)整。 與傳統(tǒng)的只調(diào)視頻延遲,不調(diào)音頻延遲, WebRTC 會(huì)兩邊都調(diào)點(diǎn),使得音視頻的時(shí)間彼此靠近,前提是音頻的延遲是在上面提到的可接受范圍之內(nèi)。
參考資料
- https://www.ciscopress.com/articles/article.asp?p=705533&seqNum=6
- https://www.ccexpert.us/video-conferencing/using-rtcp-for-media-synchronization.html
- https://testrtc.com/docs/how-do-you-find-lip-sync-issues-in-webrtc/
- https://en.wikipedia.org/wiki/Audio-to-video_synchronization
-
https://www.simplehelp.net/2018/05/29/how-to-fix-out-of-sync-audio-video-in-an-mkv-mp4-or-avi/
*RFC6051: Rapid Synchronisation of RTP Flows