文中提到的代码引用自 libwebrtc M96 版本 https://github.com/aggresss/libwebrtc/tree/M96
0x00 前言
WebRTC 音频和视频分别通过不同 RTP stream 传输,而 RFC 3550 Section 5.1 中明确说明 “The initial value of the timestamp SHOULD be random”,即两个不同的 RTP stream 之间不能直接通过 RTP 的 timestamp 进行同步。所以 RFC 3550 Section 6.4.1 中的 RTCP SR 维护了 RTP timestamp 与 NTP Time 的映射关系,在接收端通过与该 RTP Stream 关联的 RTCP SR 估算出墙上时钟 (wall clock) 后再进行音频和视频的同步。本文通过对 libwebrtc M96 中音频和视频同步的实现进行分析,进而讨论经过 SFU 转发后的音视频同步需要考量的因素。
0x01 libwebrtc 音视频同步原理
1.1 墙上时钟估算
RTCP SR 定义中与 NTP Timestamp 和 RTP Timestamp 相关的结构如下所示:
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 |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
...
libwebrtc 在 modules/rtp_rtcp/source/rtcp_sender.cc
中 RTCPSender::BuildSR
实现了 NTP timestamp 与 RTP timestamp 的计算过程,可通过伪代码表示为:
ntp_timestamp = now_ms;
rtp_timestamp = last_send_rtp_timestamp + (now_ms - last_send_ntp_timestamp) * clock_rate / 1000;
通过分析RTCP SR 中 rtp 与 ntp 时间戳的生成规则可以看出,两者之间在理想情况下是线性关系,如下图所示:
可以看出,只要两个不同时间的 SR 就能估算出每个 rtp timestamp 对应的 ntp timstamp, 但是收到网络抖动等影响,通过更多的 SR 进行线性拟合后会让 ntp timestamp 的估算更加精确。libwebrtc 中 system_wrappers/source/rtp_to_ntp_estimator.cc
实现了 ntp timestamp 的估算。
1.2 视频流与音频流同步关联
参考标准 WebRTC 1.0: Real-Time Communication Between Browsers 和 Media Capture and Streams ,在 MediaStream API 中找到了关于音视频同步的说明:
Each MediaStream can contain zero or more MediaStreamTrack objects. All tracks in a MediaStream are intended to be synchronized when rendered. This is not a hard requirement, since it might not be possible to synchronize tracks from sources that have different clocks. Different MediaStream objects do not need to be synchronized.
就是说在浏览器接口中一个 MediaStream 对象包含 0 或多个 MediaStreamTrack 对象,MediaStream 中的所有可同步的 MediaStreamTrack 对象在渲染时需要进行同步,不同的 MediaStream 之间不需要同步。
所有以上接口在 libwebrtc 中以 SDP 的形式被解析,通过分析 RFC 8830 可以看出,每个 m-line
中通过 a=msid
来标识 MediaStream 信息,格式为:
// a=msid:<stream id> <track id>
// msid-value = msid-id [ SP msid-appdata ]
// msid-id = 1*64token-char ; see RFC 4566
// msid-appdata = 1*64token-char ; see RFC 4566
例如:
# First MediaStream - id is 47017fee-b6c1-4162-929c-a25110252400
m=audio 56500 UDP/TLS/RTP/SAVPF 96 0 8 97 98
a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
m=video 56502 UDP/TLS/RTP/SAVPF 100 101
a=msid:47017fee-b6c1-4162-929c-a25110252400 b47bdb4a-5db8-49b5-bcdc-e0c9a23172e0
# Second MediaStream - id is 61317484-2ed4-49d7-9eb7-1414322a7aae
m=audio 56503 UDP/TLS/RTP/SAVPF 96 0 8 97 98
a=msid:61317484-2ed4-49d7-9eb7-1414322a7aae b94006c5-cade-4e0a-9ed9-d3e6747be7d9
m=video 56504 UDP/TLS/RTP/SAVPF 100 101
a=msid:61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-1497-49b5-3198-e0c9a23172e0
SDP 的解析过程详见 pc/webrtc_sdp.cc
。
在 libwebrtc 中需要同步的 stream 之间通过 sync_group
来关联,sync_group 的来源为 stream_id,config.sync_group = stream_ids[0];
。
call/call.cc
中的 Call::ConfigureSync
中实现了 audio_stream 和 video_stream 的关联逻辑。
1.3 音视频同步执行过程
音视频同步操作通常是视频同步到音频,即视频计算出渲染偏移时间同步到音频。
在 libwebrtc 中,音视频同步的基本操作对象是 AudioReceiveStream
和 VideoReceiveStream
,两者都继承自 Syncable
。
通过 call/syncable.h
可以看到,Syncable
是一个纯虚类,以下是三个与音视频同步相关的纯虚函数:
virtual bool GetPlayoutRtpTimestamp(uint32_t* rtp_timestamp, int64_t* time_ms) const = 0;
virtual bool SetMinimumPlayoutDelay(int delay_ms) = 0;
virtual void SetEstimatedPlayoutNtpTimestampMs(int64_t ntp_timestamp_ms, int64_t time_ms) = 0;
负责音视频同步的线程是 call
模块的 module_process_thread_
,主要处理文件是 video/rtp_streams_synchronizer.cc
,RtpStreamsSynchronizer
类包含以下成员:
-
StreamSynchronization
; - audio 和 video 的
Measurements
; -
AudioReceiveStream
和VideoReceiveStream
的指针:syncable_audio_
和syncable_video_
;
处理过程详见 RtpStreamsSynchronizer::Process()
。
0x02 SFU 转发后的音视频同步
2.1 SFU 中 RTCP SR 的时间戳管理
在 P2P 场景中,只有 Sender 和 Receiver,RTP 和 RTCP 都是从 Sender 直接传输到 Receiver ,而在 SFU 场景中,SFU为了适配一对多的转发需求,通常会增加一个中间层,即 Receive Stream 与 Send Stream 隔离,所以每个 Send Stream 的 Sequence 和 Timestamp 都会重新随机,RTCP 也会被隔离,Send Stream 根据发送情况重新生成 RTCP SR。
通常,SFU 会以本地时间为基准进行 NTP timestamp 与 RTP timestamp 计算,可通过伪代码表示为:
ntp_timestamp = now_ms;
rtp_timestamp = last_send_rtp_timestamp + (now_ms - last_send_ntp_timestamp) * clock_rate / 1000;
这种计算方式在简单传输的场景下,通常不会出现异常,如下图所示:
因为 audio 与 video 在同一个 PC 中,PC 的网络抖动同时作用在 audio 和 video 上,产生的延迟也相对同步,所以 Receiver A 在接收后通过 SFU 的 RTCP SR 计算的视频延迟偏移与 P2P 情况下的误差并不大。
下图演示了一个增加了一次转发,并且音频和视频在不同的传输通道中二次转发:
当音视频在不同的传输通道时,网络抖动与延迟的不同步会导致 Receiver 只以 SFU 到 Receiver 之间的 RTCP SR 计算视频延迟偏移产生较大误差,所以需要对上面 SFU 中生成 RTCP SR 的算法做一些改进,改进的思路为以 Sender 的 NTP timestamp 作为时间基准,每一级 SFU 以上一级的 SFU 作为时间基准,这样 Receiver 接收到的 RTCP SR 仍然是以 Sender 为时间基准,从而剪掉了 SFU 转发过程中每一级 SFU 独立生成 RTCP SR 而增加的误差。计算步骤如下:
- ① SFU 接收到 Receive Stream 的 RTCP SR 后通知与其关联的 Send Stream;
- ② Send Stream 接收到上游 Receive Stream 的 STCP SR 通知后,记录
- last_sync_rtp_ts
- last_sync_remote_ntp_ms
- last_sync_local_ntp_ms
- ③ rtp_timestamp 和 ntp_timestamp 计算伪代码:
rtp_timestamp = last_sync_rtp_ts + (now_ms - last_sync_local_ntp_ms) * clock_rate() / 1000; ntp_timestamp = now_ms - last_sync_local_ntp_ms + last_sync_remote_ntp_ms;
注意:因为修改了 RTCP SR 的时间基准,所以 Send Stream 接收到 RTCP RR 时通过 LSR 计算 RTT 时也需要做相应的修改。
2.2 SFU 中音视频同步关联管理
下图为 SFU 的经典场景,一个 Receiver 通过一个 PC 可以订阅多个 Sender 的音视频:
通常 Receiver 的 Remote Description 会由 Receiver 或者 SFU 将多个 Sender 的 SDP 进行合成,在 Receiver 侧的表现为一个 PC 中包含多个 MediaStream,需要根据 msid 对属于不同 MediaStream 的 MediaStreamTrack 进行描述,以保证每个视频都能关联到正确的音频。
参考文档
- WebRTC音视频同步 · 言剑
- WebRTC 音视频同步原理与实现 · 阿里云视频云
- WebRTC音视频同步机制实现分析 · weizhenwei
- WebRTC笔记(三)音视频同步 · jiayayao
- WebRTC音视频同步分析 · lincai2018
- WebRTC研究:MediaStream概念以及定义 · 剑痴乎