作者:付明旺。唐桥科技资深架构师。负责实时通信软件的技术创新与研发。曾就职于中兴通讯、诺基亚,在4G和IMS-webRTC等电信通信产品担任软件工程师、系统架构师等角色,具有丰富的无线/互联网通信、实时音视频通信产品技术经验。
唐桥科技,医疗云通信专家,是专业的智能视频PaaS及SaaS云服务提供商。致力于三网融合视频通讯平台的研发和在不同领域的应用,已经与医疗行业应用深度结合,推出了远程医疗、医学视频云会议、互动医教、医学线上峰会平台等一系列行业解决方案,为医疗行业提供专业音视频通讯服务。在此基础上,唐桥科技将多年积累的音视频技术以SDK/API的形式开放给企业及开发者,降低技术门槛,让企业跑得更快。
webRTC解决方案实现了P2P的音视频通信,其中有关timing的几个问题值得归纳总结。开始本文之前建议先行阅读https://xie.infoq.cn/article/738b8293dce86f7c8748e2629 了解视频传输的关键路径。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。
发送端
一帧视频画面在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域组织如下类型的扩展内容
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。
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模块负责传输层统计的反馈,周期性的把包接收的时间信息回馈到发送端。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
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_;
}