jitterbuffer 也叫抖动缓冲区,分为jitter和buffer两部分即延时和缓冲区管理。工作在接收端,通常在播放器,主要目的是保证平滑播放。常见的抖动缓冲区分为静态抖动缓冲区和自适应抖动缓冲区。静态抖动缓冲区缓冲区时长固定,自适应抖动缓冲区可以自适应网络抖动、解码延时的变化。WebRTC采用自适应抖动缓冲区,本文重点分析WebRTC中对延时的计算。
1 基本思想
要保证平滑播放,对于接收到的数据帧尽量按照原始数据帧的采集间隔进行播放。但由于网络延时、解码延时、渲染延时的存在,如果收集到一帧数据立即播放就会造成快放或卡顿。WebRTC收集到完整一帧数据后不是立即发送到解码器而是添加一个延时后才开始解码,避免有些帧到的太慢从而造成卡顿,原则上只要数据帧在该延迟时间内到达就不会引起卡顿,WebRTC中的延迟是动态变化的。
假设数据包A、B、C、D以30ms间隔发送数据,网络时延分别为(10ms、30ms、10ms、10ms), 此时到达时间为(40ms、90ms、100ms、130ms), 产生的时间间隔分别为(50ms、10ms、30ms)。
为了平滑播放先在缓冲区中延时20ms再开始播放,此时播放时间分别为(60ms、90ms、120ms、150ms), 播放间隔与发送间隔同为30ms。
假设缓冲区大小为10ms, 此时播放时间分别为(50ms、80ms、110ms、140ms), 为了产生30ms的播放间隔B数据包需要丢弃。
可见抖动缓冲区的大小至关重要,小了会产生丢包,大了会增加延时。抖动缓冲区的理想状态是数据包在网络中的延迟与其在缓冲区中的延迟之和相等。一般的抖动缓冲区的消除思想是将抖动缓冲区的大小设置成目前测量的最大网络延迟。
2. 基本流程
jitterbuffer分为buffer和jitter。
buffer主要包括PacketBuffer、RtpFrameReferenceFinder、FrameBuffer。PacketBuffer保证数据帧的完整性,RtpFrameReferenceFinder给每个帧设置参考帧,FrameBuffer保证帧的连续性和可解码性。
jitter部分主要涉及到延时的计算,FrameBuffer在InsertFrame时会设置帧的期望接收时间,FindNextFrame中会设置帧的渲染时间,GetNextFrame中会更新网络延时jitter。
2.1 常用类介绍
视频jitterbuffer 的jitter计算主要涉及到以下几个类:
1.RtpVideoStreamReceiver:rtp数据接收;2.VideoReceiveStream: 负责数据驱动,接收到完整的数据帧,插入到frame_buffer并在适当时机执行解码操作;3.FrameBuffer:负责帧的连续性和可解码性,连续性是指某帧的所有参考帧都已经收到,帧的可解码性是指帧的所有参考帧都已经被解码。保存未解码的帧,已解码的历史帧;4.VCMJitterEstimator:计算抖动jitter值;5.VCMTiming:计算当前延迟currentDelayMs,用于计算渲染时间;6.VCMCodecTimer: 统计解码延时。
2.2 基本流程分析
1.RtpVideoStreamReceiver 收到rtp数据包后解包后封装成VCMPacket,并插入到PacketBuffer中;2.在PacketBuffer中组帧,若发现完整的数据帧则回调RtpVideoStreamReceiver的OnAssembledFrame函数;3.在RtpVideoStreamReceiver中调用成员RtpFrameReferenceFinder进行参考帧设定,参考帧设置完成后回调RtpVideoStreamReceiver的OnCompleteFrame接口,接着回调VideoReceiveStream的OnCompleteFrame接口;4.在VideoReceiveStream中将一帧数据插入到FrameBuffer中,更新帧的引用完整性和解码完整性;5.解码时刻到来从FrameBuffer中取出一帧解码,并在渲染延迟后开始渲染。
2.3 插入到FrameBuffer后处理流程
对FrameBuffer的主要操作分为读取和写入,以下流程图是帧插入到FrameBuffer后详细处理流程。
读取:
1.VideoReceiveStream start中会启动解码线程,解码线程最终会调用frame_buffer的FindNextFrame查找可解码的数据帧并返回一个等待时间;2.延迟该等待时间后调用GetNextFrame取出可解码的数据帧执行解码操作;3.在GetNextFrame中会根据实际的解码时间更新延迟信息。
写入:
填充好参考帧的完整帧后会回调 VideoReceiveStream 的OnCompleteFrame接口,该函数调用frame_buffer的InsertFrame把一帧数据写入JitterBuffer里之后执行以下操作:
1.检查视频帧是否有效。缓冲区是否满等;2.更新帧信息,主要是设置帧的还未连续的参考帧数量,并建立被参考帧与参考他的帧之间的参考关系;3.如果不是重传帧,更新帧的渲染时间;4.如果当前帧连续则传播帧的连续性;5.触发解码任务,寻找待解码的帧,并发送到解码任务队列。
3. 延时分类
主要有三种,网络抖动延时、解码延时、固定渲染延时。网络抖动延时是由网络抖动造成的,解码延时是解码器解码产生的是个统计值,固定渲染延时一般是10ms。
3.1 接收到一帧rtp数据时间线
名称 | 含义 | 类型 |
now_ms | 当前时间,单位ms | 时间点 |
expect_decode_time | 期望开始解码时间 | 时间点 |
actual_decode_time | 实际解码时间 | 时间点 |
decode_finish_time | 解码结束时间 | 时间点 |
render_time | 渲染时间 | 时间点 |
wait_time | 到期望开始解码的等待时间 | 时间段 |
current_delay | 当前延时 | 时间段 |
target_delay | 目标延时,最优延时 | 时间段 |
frame_delay | 数据帧产生的延迟 | 时间段 |
render_delay | 固定10ms延迟 | 时间段 |
其中:
•expect_decode_time = render_time_ms – decode_delay_ms – render_delay_ms_
•frame_delay = actual_decode_time – expect_decode_time
•current_delay = max(current_delay + frame_delay, target_delay)
•wait_time = render_time – now_ms – decode_delay – render_delay
•decode_delay = decode_finish_time – expect_decode_time
3.2 wait_times计算
在上述时间线图中用wait time 表示,指接收一帧数据到解码最小的等待时间。
int64_t VCMTiming::MaxWaitingTime(int64_t render_time_ms,
int64_t now_ms) const {
rtc::CritScope cs(&crit_sect_);
const int64_t max_wait_time_ms =
render_time_ms - now_ms - RequiredDecodeTimeMs() - render_delay_ms_;
return max_wait_time_ms;
}
3.3 目标渲染时间 RenderTime 计算
期望的开始渲染时间,对应上述t3,整个计算过程如下图所示。其中会涉及到抖动jitter、期望接收时间、真实延时、当前延时、解码延时等的计算。
RenderTime = estimated_complete_time_ms(期望接收时间) + actual_delay(当前延时);
3.4 抖动jitter计算
3.4.1 jitter定义
定义:指由于各种延时的变化导致网络中的数据分组到达速度的变化。可将抖动定义为数据流在发送端发送间隔与接收端接收间隔之差:Ji = Si – Ri i= 1, 2, …,n 其中,
1.si为发送第i和第i+1个数据包的发送间隔;
2.Ri为接收第i和第i+1个数据包的到达间隔;
3.Ji为数据包i的抖动延迟。
更直观的可以用下图描述, 其中jitter delay 就是产生的网络延时,会经卡尔曼滤波进行处理,得到最优值。
计算步骤分为以下两步:
1.计算帧间延迟 = 两帧的接收时间差 – 两帧的发送时间差, 用VCMInterFrameDelay表示;2.根据VCMInterFrameDelay计算的帧间延迟计算出最优抖动值,用VCMJitterEstimator表示,是一个卡尔曼滤波的过程。
3.4.2 jitter 计算模型
设T(i) 为发送时间,t(i) 为接收时间,d(i)为第i帧的接收时间之差减去发送时间之差。d(i)就是相邻两帧产生的延时,W(i)是误差。
d(i) = t(i) - t(i-1) - (T(i) - T(i-1)) +W(i)
= (t(i) - T(i)) - (t(i-1) - T(i-1)) + W(i)
= L(i)/C(i) - L(i-1)/C(i-1) + w(i) (此处假设C(i) = C(i-1) 即网路传输速率不变)
= dL(i) / C(i) + w(i)
d(i) 就是最终延迟
L(i) 为每帧的数据量大小
dL(i) 为两帧数据量差值
上述jitter = d(i) = dL(i) / C(i) + w(i)
由于观测过程会存在误差,导致帧间抖动以及网络传输速率不准,引入了卡尔曼滤波去修正。jitter的主要目的是根据视频帧的延时时间计算出目标渲染时间。
3.5 期望接收时间 estimated_complete_time_ms计算
期望接收时间由TimestampExtrapolator计算。在代码中用estimated_complete_time_ms表示。TimestampExtrapolator是一个卡尔曼滤波器,其输入为帧的rtp时间和接收时间,根据输入帧的时间戳的间隔计算期望接收时间,得到一个最优的帧接收时间。
3.5.1 主要原理
TimestampExtrapolator 负责期望接收时间计算。FrameBuffer对于收到的每帧数据都会包含rtp时间和实际的接收时间。
1.第一帧接收时间定义为startMs;
2.第一帧rtp时间定义为firstTimestamp;
3.当前帧的rtp时间为R(k);
4.当前帧的期望接收时间为T(k);
5.当前抖动延时为jitterTimestamp, 在代码中是W[1];
6.千分之一采样率samplerate/1000, 在代码中是W[0]。
3.5.2 基本模型
不同帧的采样率和jitter是线性关系,误差U服从高斯分布。
3.5.3 获取当前帧的期望接收时间
计算rtp时间差:timestampDiff = R(k) – firstTimestamp
期望接收时间:T(k) = startMs + (timestampDiff – w[1]) / w[0] + 0.5
这里的关键是计算jitterTimestamp,基本原理是根据每一帧的实际接收时间和rtp时间根据卡尔曼滤波器计算抖动值。具体的计算过程可以参见附录部分。
3.6 真实延时actual_delay计算
计算Render_time时关键在于计算实际延时。actual_delay主要由current_delay_ms_的值决定,该值会控制在[min_playout_delay_ms_, max_playout_delay_ms_] 范围内。
3.7 当前延时current_delay_ms_计算
current_delay_ms_ 主要在UpdateCurrentDelay中计算, 下面是主要的代码。
void VCMTiming::UpdateCurrentDelay(int64_t render_time_ms, //期望渲染时间, 时间点
int64_t actual_decode_time_ms) {//当前解码时, 时间点
rtc::CritScope cs(&crit_sect_);
uint32_t target_delay_ms = TargetDelayInternal(); // 目标延迟 时间段
int64_t delayed_ms =
actual_decode_time_ms -
(render_time_ms - RequiredDecodeTimeMs() - render_delay_ms_);
if (delayed_ms < 0) {
return;
}
if (current_delay_ms_ + delayed_ms <= target_delay_ms) {
current_delay_ms_ += delayed_ms;
} else {
current_delay_ms_ = target_delay_ms;
}
current_delay_ms 先计算实际的解码时间与期望解码时间之间的延时,该延时加上上次计算的的当前延时,逼近于目标延时。
3.8 目标延时target_delay计算
target_delay 是当前的目标延时,是由网络抖动、解码延时、渲染延时叠加产生。
target_delay = max(min_playout_delay_ms_,
jitter_delay_ms_ + RequiredDecodeTimeMs() + render_delay_ms_)
jitter_delay_ms_ 是网络抖动延时,通过CalculateDelay计算,RequiredDecodeTimeMs 是解码延时,render_delay_ms_ 是固定渲染延时。
3.9 解码延时 decode_delay计算
解码延时在代码中通过RequiredDecodeTimeMs计算,统计最近最多10000次时间窗口内的解码时间消耗,计算95百分位数作为解码时间。
int VCMTiming::RequiredDecodeTimeMs() const {
const int decode_time_ms = codec_timer_->RequiredDecodeTimeMs();
assert(decode_time_ms >= 0);
return decode_time_ms;
}
具体的计算在VCMCodecTimer类中进行。
4. 查看各指标变化
以上各延时的值可以在chrome://webrtc-internals页面中看到:
名字 | 含义 |
googDecodeMs | 最近一次解码耗时. |
googMaxDecodeMs | 最大解码耗时,95分位数,大于采样集合95%的解码延时 |
googRenderDelayMs | 渲染延时,10ms |
googJitterBufferMs | 网络抖动延时 |
googMinPlayoutDelayMs | 最小播放时延,音视频同步器输出的视频帧播放应该延迟的时长 |
googTargetDelayMs | 目标时延 |
googCurrentDelayMs | 当前时延,用于计算RenderTime |
下图是实际观测到的各值变化情况:
作者:贺雄峰
审稿:肖辉 斗鱼流媒体技术委员会
原文:https://mp.weixin.qq.com/s/smdviU9NZaOj5faJtOTKjw
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。