webRTC解决方案实现了P2P的音视频通信,其中有关timing的几个问题值得归纳总结。开始本文之前建议先行阅读https://xie.infoq.cn/article/738b8293dce86f7c8748e2629 了解视频传输的关键路径。webRTC是一个异步系统,通信的双方无需做时间同步。本文主要探讨webRTC是怎样解决下面两个跟时间有关的问题:1. 音视频同步 2. 基于延时的带宽评估。
发送端采集-编码-发送,接收端解码-渲染,音频流和视频流的处理和网络传输是互相独立的,而且各自的采样/播放频率也是不同的。如何在接收端还原采集端的真实场景,从来都不是一件容易的事情。好在人的听觉/视觉系统本来就有一定容忍能力,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。
一帧视频画面在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),最终得到每条流的取帧延迟。
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域组织如下类型的扩展内容
https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01
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。
发送端在发送的时候对每一个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模块负责传输层统计的反馈,周期性的把包接收的时间信息回馈到发送端。transport feedback的格式有详细的规则,定义如下 https://tools.ietf.org/id/draft-dt-rmcat-feedback-message-04.html#rfc.section.3.1
这里有一篇写的不错的注解可以参考 https://blog.jianchihu.net/webrtc-research-transport-cc-rtp-rtcp.html
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_;
}
领取专属 10元无门槛券
私享最新 技术干货