webRTC 是一个异步系统,通信的双方无需做时间同步。
本文主要探讨 webRTC 是怎样解决下面两个跟时间有关的问题:1. 音视频同步 2. 基于延时的带宽评估。
音视频同步(Lip-sync)
发送端采集-编码-发送,接收端解码-渲染,音频流和视频流的处理和网络传输是互相独立的,而且各自的采样/播放频率也是不同的。如何在接收端还原采集端的真实场景,从来都不是一件容易的事情。好在人的听觉/视觉系统本来就有一定容忍能力,ITU(国际电信联盟)给了一个建议:音频之于视频在这个范围内[-125ms,45ms],也就是落后 125ms 或早于 45ms,人类感觉上是可以接受的,我们认为这是音视频处于同步状态。
webRTC 解决这个问题的原理也比较简单,发送端给音频流和视频流的数据包都打上时间戳,这些时间戳都可以跟同一个时间基准对齐,接收端利用时间戳和缓存就可以调整每个流上音频/视频帧渲染时间,最终达到同步的效果。我们可以进一步了解一下实现的细节,以视频流为例。下图为视频处理的流水线,每个矩形框是一个线程实例。
实现上有三种时间信息:
1.本地系统时间:从操作系统启动计时至当前的时间差值
2.NTP(Network Time Protocol)时间:全局时间信息,从 1/1/1900-00:00h 计时到当前的时间差值
3.RTP 时间:帧时间戳,以视频采样 90k 频率为例,rtp_timestamp=ntp_timestamp*90
这三种时间坐标都是对时间的度量,只是描述时间的方式不同。比如当前绝对时间 2020-08-05T06:08:52+00:00, 它们是这样表达的。
本地时间 1919620051:表示开机计数起,过去 1919620051ms 了,大概 22.2days。
NTP 时间 3805596543795:表示距离 1/1/1900-00:00h,过去 3805596543795ms 了。
RTP 时间:RTP 时间由 NTP 时间计算而来,时间单位 1/90000s,u32 存储,计算过程会发送溢出,((u32)3805596543795)*90=1521922030。
音视频开发视频教程:在线观看→2023年初版!C++音视频开发项目实战教程,少走弯路,避免盲目自学。教程包含(FFmpeg6.0/H265/H264/RTMP/RTSP/WebRTC/SRS_哔哩哔哩_bilibili
发送端
一帧视频画面在 caputer 线程就记录下了,这一帧对应的三个时间信息,尤其重要的是 RTP 时间。这个 rtp_timestamp 在 Packet pacer 模块会加一个提前设定的偏移量,作为最终的 rtp 时间发出去。这个偏移量加在了整个 rtp 时间坐标系内,所有的对外的 RTP 时间都加了。
视频流按照自己的 RTP 时间对每一个包做了标记,音频流也类似的根据自己的 RTP 时间对每一个音频包做了标记,但这两条流里的时间都是按照自己的步调在走,是独立的。如果要求接收端使这两条流同步渲染,就要想办法让这些时间统一跟同一个时间基准对齐。如下图示,逻辑上举例描述了两条流如何同步,其中的时间数字只做参考,非真实数据。
RTCP SR(sender report)的作用之一就是做时间对齐的,将该流中的 RTP 时间于 NTP 时间对齐。所有的流都对齐发送端的 NTP 时间,这样接收端就有了统一时间基准。
RTCP SR format 如下:
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The sender report packet consists of three sections, possibly
followed by a fourth profile-specific extension section if defined.
The first section, the header, is 8 octets long. The fields have the
following meaning:
version (V): 2 bits
Identifies the version of RTP, which is the same in RTCP packets
as in RTP data packets. The version defined by this specification
is two (2).
接收端
上图中可以看到经过网络传输后,到达接收端的帧数据可能经过了 jitter(抖动),乱序,比如 stream1 的帧 2/3/4。接收端通过 RTCP SR 和 buffer 的设计,采用 pull 的模式,以渲染作为终点倒推从 frame queue 中取帧的延迟。从单条流处理过程中可以看到该延迟包含渲染+解码+抖动延迟,而多流之间的同步还需要考虑流之间的相对传输延迟(参考 RtpStreamsSynchronizer),最终得到每条流的取帧延迟。
接收端多流同步其实是包含两部分的含义:单流内的流畅播放和多流间的时间同步播放。音频和视频延迟处理原理类似,只是时延的计算方式有所不同,接下来以视频流的延迟处理来说明这个过程。
视频流流畅播放
视频解码线程执行 loop 不断从 frame_queue 中取下一帧解码渲染,通过严格控制每一帧画面的执行解码-渲染的时间起点来达到帧与帧之间最终渲染播放的时间间隔是大致相等的,视觉感受是流畅的。这个开始时间我们用 waitTime 来表达,即 thread 等待多久去取帧处理。
一些重要时间量的说明
waitTime = render_systime - current_systime - render_cost - decoder_cost;
render_systime =local_systime + max(min_playout_delay,target_delay); //计算渲染时间
local_systime = rtpToLocaltime(rtp_time)//把帧rtp时间转换为本地系统时间
target_delay = jitter_delay+render_cost+decoder_cost; //计算预估目标延迟
min_playout_delay //流间同步用的时延调节参数
jitter_delay //抖动延迟
render_cost //渲染延迟,固定10ms
decoder_cost //解码延迟
要得到待解码帧的 waitTime 首先要计算该帧的渲染时间(render_systime),先把帧附带的 rtp 时间转成本地系统时间(local_systime,转换方法应该容易理解,不展开),然后叠加一个时间延迟计算方法为 max(min_playout_delay,target_delay),min_playout_delay 为流间同步用的调节参数,这里讲流内的延迟处理,可以暂时略过。target_delay 为系统评估的三个延迟(抖动延迟+渲染延迟+解码延迟)之和,注意这里计算得到的 target_delay 延迟是统计累积得来的,实际参与到 render_systime 的计算时,还有些实现上的处理。这里也做了简化处理,便于理解原理。
有了渲染时间(render_systime)后,waitTime 很容易得出了。细心观察,每一帧的处理都多等待一个 jitter_delay,但帧间的解码间隔还是保持相同的。jitter_delay 的存在就是对抗网络传输的不确定性的。webRTC 动态计算它的取值,稳定的网络下它的值接近 0,造成的延迟比较小;弱网下延迟不稳定,这个值计算出来较大,增加等待时间换取渲染的平稳流畅,很好的做到了延迟与流畅的平衡。
音视频流同步
单条流内做到流畅播放的同时还需要做流之间的时间对齐,以下图为例。假设最近的一对音频+视频包在同一时间采样,(实际情况可以是不同时间点,这里做了简化)那我们也期望他们同一时间在接收端渲染。可以看出整个过程主要包含两个时间信息,传输时间延迟(xxx_transfer_delay)和接收方的处理延迟(xxx_current_delay)。这两个时间不同的流各自维护各自的延迟信息,如果希望音频和视频包经过相同的延迟后同时渲染,可以在两条流上各加一个延迟调整参数(xxx_min_playout_delay),通过增减调整这个参数使得两条流的延迟逼近相等。
webRTC 里面是周期性(1s)来计算调整这些延迟调整参数(xxx_min_playout_delay)的。如上图例子,伪代码大概如此。
//Time diff video vs audio
time_diff = (video_transfer_delay+video_current_delay)-(audio_transfer_delay+audio_current_delay)
if(time_diff>0){ //video is slower
down(video_min_playout_delay);
up(audio_min_playout_delay);
}
else{//video is faster
up(video_min_playout_delay);
down(audio_min_playout_delay);
}
基于延时的带宽估计
WebRTC 的成功之一在于其设计一套拥塞控制算法,基础数据来自于发送端的丢包统计和包接收时间的统计。这里只讲一下有关 timing 的 RTP 包接收时间的统计和反馈,不对拥塞算法展开讲述。
拥塞控制的逻辑现在默认都在发送端执行,有关时间延迟的计算包括发送时间 T 和接收时间 t,发送端自己可以保存每个包 T,接收端只需要反馈 t 即可。算法的逻辑是每个包的接收时间都要反馈,这涉及到交互数据和频次就会比较多,webRTC 对此也有精心设计。
webRTC 默认在发送端做传输带宽的估计,媒体流走的 RTP/UDP 协议栈,UDP 层没有带宽估计的功能,webRTC 通过扩展 RTP/RTCP 的传输格式使得可以在发送端做传输层的带宽估计。
RTP format
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(if mixed) |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| header extension (optional) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| payload header (format depended) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| payload data |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
在 header extension 域组织如下类型的扩展内容
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0xBE | 0xDE | length=1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ID | L=1 |transport-wide sequence number | zero padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Tips: 可以注意到 RTP 包里有两种 sequence number。
equence number:是 RTP 层的概念,用于 RTP stream 的重组解复用。比如多条流复用的场景,每条流有各自的自增序列。
transport-wide sequence number:是传输层概念,传输层包的标识,用于传输层的码率统计。该序列自增不受多流复用的影响,因为复用发生在 RTP 层。
发送端在发送的时候对每一个 RTP packet 都打上 transport-wide sequence number 的序号(PacketRouter::SendPacket),比如发送 seq=53,54,55。
接收端收到该包后,把该包的到达时间记录下来,记录时间为本地内部时间戳(单位 ms),即开机多久了。
packet_arrival_times_[53]=1819746010
packet_arrival_times_[54]=1819746020
packet_arrival_times_[55]=1819746026
接收端 RemoteEstimatorProxy 模块负责传输层统计的反馈,周期性的把包接收的时间信息回馈到发送端。
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| FMT=CCFB | PT = 205 | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of packet sender |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of 1st media source |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| begin_seq | end_seq |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|L|ECN| Arrival time offset | ... .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
. .
. .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of nth media source |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| begin_seq | end_seq |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|L|ECN| Arrival time offset | ... |
. .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Report Timestamp (32bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RTCP transport feedback 一般是 RTCP 通道上最频繁的传递内容,webRTC 对其传输也有特别的设计。关注以下几个参数
max_intervel = 250ms //feedback 最大周期
min_intervel =50ms //feedback 最小周期
rtcp_ratio = 5% //feedback占用带宽比例
Avg_feedback_size = 68bytes //平均一个feedback包的大小
发送 RTCP transport feedback 的时间周期控制在[50ms,250ms]内,在这个范围内根据当前带宽动态调整,尽量把 RTCP transport feedback 的传输占用带宽比例控制在 5%。可以计算得到边界,单单传输 feedback 占用的带宽范围[2176bps,10880bps],也是一笔不小的开销了。
总结
本文总结了 webRTC 中三种 timing 类型,本地时间、NTP 时间、RTP 时间,同时分析了音视频同步和基于延迟带宽评估两个专题对时间信息的使用。
附录
附上 webRTC 工程上有关 timing 的几个关键数据结构
Capturer
class webrtc::VideoFrame{
...
uint16_t id_; //picture id
uint32_t timestamp_rtp_; //rtp timestamp, (u32)ntp_time_ms_ *90
int64_t ntp_time_ms_; //ntp timestamp, capture time since 1/1/1900-00:00h
int64_t timestamp_us_; //internal timestamp, capture time since system started, round at 49.71days
}
VideoStreamEncoder::OnFrame // caluclate capture timing
VideoStreamEncoder::OnEncodedImage // fill capture timing
RtpVideoSender::OnEncodedImage // timestamp_rtp_+random value
class webrtc::EncodedImage{
...
//RTP Video Timing extension
//https://webrtc.googlesource.com/src/+/refs/heads/master/docs/native-code/rtp-hdrext/video-timing
struct Timing {
uint8_t flags = VideoSendTiming::kInvalid;
int64_t encode_start_ms = 0; //frame encoding start time, base on ntp_time_ms_
int64_t encode_finish_ms = 0; //frame encoding end time, base on ntp_time_ms_
int64_t packetization_finish_ms = 0; //encoded frame packetization time, base on ntp_time_ms_
int64_t pacer_exit_ms = 0; //packet sent time when leaving pacer, base on ntp_time_ms_
int64_t network_timestamp_ms = 0; //reseved for network node
int64_t network2_timestamp_ms = 0; //reseved for network node
int64_t receive_start_ms = 0;
int64_t receive_finish_ms = 0;
} timing_;
uint32_t timestamp_rtp_; //same as caputrer.timestamp_rtp_
int64_t ntp_time_ms_; //same as caputrer.ntp_time_ms_
int64_t capture_time_ms_; //same as caputrer.capture_time_ms_
}
RTPSenderVideo::SendVideo
class webrtc::RtpPacketToSend{
...
// RTP Header.
bool marker_; //frame end marker
uint16_t sequence_number_; //RTP sequence number, start at random(1,32767)
uint32_t timestamp_; //capturer timestamp_rtp_ + u32.random()
uint32_t ssrc_; //Synchronization Source, specify media source
int64_t capture_time_ms_; //same as capturer.capture_time_ms_
}
===
receiver side
RtpTransport::DemuxPacket
class webrtc::RtpPacketReceived{
...
NtpTime capture_time_;
int64_t arrival_time_ms_; //RTP packet arrival time, local internal timestamp
// RTP Header.
bool marker_; //frame end marker
uint16_t sequence_number_; //RTP sequence number, start at random(1,32767)
uint32_t timestamp_; //sender's rtp timestamp maintained by RTPSenderVideo
uint32_t ssrc_; //Synchronization Source, specify media source
}
RtpVideoStreamReceiver::ReceivePacket /OnReceivedPayloadData
struct webrtc::RTPHeader{
...
bool markerBit;
uint16_t sequenceNumber; //RTP sequence, set by sender per RTP packet
uint32_t timestamp; //sender's RTP timestamp
uint32_t ssrc;
RTPHeaderExtension extension; //contains PlayoutDelay&VideoSendTiming if has
}
class webrtc::RtpDepacketizer::ParsedPayload{
RTPVideoHeader video;
const uint8_t* payload;
size_t payload_length;
}
class webrtc::RTPVideoHeader{
...
bool is_first_packet_in_frame;
bool is_last_packet_in_frame;
PlayoutDelay playout_delay; //playout delay extension
VideoSendTiming video_timing; //Video Timing extension, align with sender's webrtc::EncodedImage::timing
}
class webrtc::VCMPacket{
...
uint32_t timestamp; //sender's RTP timestamp
int64_t ntp_time_ms_;
uint16_t seqNum;
RTPVideoHeader video_header;
RtpPacketInfo packet_info;
}
class webrtc::RtpPacketInfo{
...
uint32_t ssrc_;
uint32_t rtp_timestamp_; //sender's rtp timestamp
//https://webrtc.googlesource.com/src/+/refs/heads/master/docs/native-code/rtp-hdrext/abs-capture-time
absl::optional<AbsoluteCaptureTime> absolute_capture_time_; //
int64_t receive_time_ms_; //packet receive time, local internal timestamp
}
PacketBuffer::InsertPacket
class webrtc::video_coding::RtpFrameObject: public EncodedImage{
...
RTPVideoHeader rtp_video_header_;
uint16_t first_seq_num_;
uint16_t last_seq_num_;
int64_t last_packet_received_time_;
int64_t _renderTimeMs;
//inherit from webrtc::EncodedImage
uint32_t timestamp_rtp_;
int64_t ntp_time_ms_;
int64_t capture_time_ms_;
}