webrtc视频jitterbuffer全网最详细分析
文章目录
- 概要
- 整体架构流程
- 一、抖动延时jitterdelay
- 二、每帧等待延时的计算
- 小结
概要
网上很多介绍jitterbuffer的帖子,对jitterbuffer的核心介绍并不清楚,有些发帖作者可能并没有完全理解jitterbuffer就发帖分享,导致网上误导性文章较多。本人对jitterbuffer相关源码进行仔细预测之后总结内容如下,由于webrtc代码一直不断更新,不同版本的代码有所差别,但以下jitterbuffer的核心思想基本不变。
整体架构流程
判断帧的完整性以及帧的参考关系是否满足解码相关内容不做过多讲解,网上很多帖子都对该部分有较为详细介绍,下面只讲解jitterbuffer中针对去抖的部分,jitterbuffer主要功能是为了去除视频的帧间抖动。其中包括两个重要的点,一是抖动延时jitterdelay的计算,二是每帧等待延时的计算。
一、抖动延时jitterdelay
该部分可参考某大佬的帖子WebRTC QoS方法十三.2(Jitter延时的计算)。
最核心的jitterdelay计算代码,我的版本的代码是在VCMJitterEstimator::GetJitterEstimate方法中的VCMJitterEstimator::CalculateEstimate()中
double ret = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
_theta[0]:信道传输速率的倒数
_maxFrameSize :表示自会话开始以来所收到的最大帧size
_avgFrameSize:表示平均帧大小,排除keyframe等超大帧
_noiseStdDevs : 表示噪声系数2.33
_varNoise: 表示噪声方差
_noiseStdDevOffset: 表示噪声扣除常数30
上方的信道速率其实通过接受端通过不同大小帧接收的快慢估算出来的速率,这个速率理论上会接近发送端通过pacing进行发送数据的码流平滑的数据发送速率。同时结合最大帧和平均帧大小差值,去抖动模块很重要的一点就是去除发送端码流平滑模块对于大帧平滑发送引入的抖动,其次是去除网络中的随机噪声抖动。实际项目工程中发送平滑大帧引入的帧间抖动占据大头,随机的网络噪声不会太大。
还有一点是_maxFrameSize 的大小在没有更大帧过来时,每次统计一个新帧会缩小到其大小的0.9999,这个缩小的速度很慢,目的是在长时间没有大帧时可以逐渐减小jitterdelay,减小去抖动模块引入的延时。
从jitterdelay的计算公式中可以看出,jitterdelay结合了传输大帧延时以及网络噪声延时。
实际每帧视频数据都有时间戳信息,每帧数据完整接收进行处理时都能在本地获取now_ms时间,理论上完全可以根据前后两帧时间戳之间的差值和前后两帧完整接收now_ms之间的差值之差计算出帧间抖动。
试想一下为什么webrtc不直接通过该抖动值经过滤波以及和历史值加权来直接计算jitterdelay呢。这是由于如果简单的使用类似jitterdelay = k*lastjitter + (1-k)*jitterdelay;这样计算的话,当传输一段时间静态画面时每帧很小传输很快会导致 jitterdelay 计算逐渐变小。缓存的帧就会很少,当画面突然运动时图像复杂度增高,一帧数据较大到达较慢时则会卡顿延时没有去抖的效果。那么有聪明的同学可能就要问了,能不能用历史中抖动的最大值作为抖动延时呢。这样虽然可以保证jitterdelay较大但也会有一些问题,一是如果这个最大抖动是网络链路切换或是某个时间点网络的突发状况则后续去抖会一直引入较高的延时,其二是即使没有网络突发抖动仅仅是刚开始网络带宽较低传输大帧较慢则计算出的jitterdelay较大,后续即使网络带宽提升了传输大帧变快了那么这个jitterdelay也不会跟随网络变好而减小。
这就是为什么webrtc的jitterbuffer为什么要结合最大帧和信道速率来计算抖动了,这样在信道速率变大时,jitterdelay会随之减小。去抖动引入的延时就会越小。
具体信道速率的倒数_theta[0]是怎么计算出来的可以参考
VCMJitterEstimator::KalmanEstimateChannel(int64_t frameDelayMS,int32_t deltaFSBytes)接口中的代码。
二、每帧等待延时的计算
网上很多帖子对该时间的计算描述不清,把该点理解清楚是理解整个去抖模块的核心,以下我画了一个示意图用来分析等待延时是如何计算出来的
a.上图横轴为每一帧的时间戳。
b.纵轴为每帧完整时进行处理通过系统接口获取的当前时间now_ms(也可以理解为一帧在接收端接收的时间)。
c.其中绿色的点为每帧实际的时间戳对应的实际接收时间now_ms。
d.最下方黑色虚线为结合所有帧的时间戳和now_ms经过卡尔曼滤波之后拟合出来的一个直线。
e.红色点为最后这一帧根据其自身时间戳在拟合的线性关系上预测出的点,其纵轴对应的为预测出的本地时间Localtime。
f.红色点上方的绿色点为实际该帧在接端接收的时间。
g.灰色的虚线为每帧的应该解码的时间点。
h.最上方黑色虚线为每帧渲染的时间点。
i.下方黑色虚线和灰色虚线之间的距离为抖动延时jitterdelay。
j.上方黑色虚线和灰色虚线之间的距离为解码耗时+渲染耗时,解码耗时和渲染耗时基本不变。
要计算每帧的等待延时主要分为以下几个步骤
1.最下方黑色虚线的拟合
void TimestampExtrapolator::Update(int64_t tMs, uint32_t ts90khz) { //每来一帧都会调用该接口,将这一帧的接收时间和时间戳传入用来拟合上图中最下方的黑色虚线 //实际每个帧到达时拟合的虚线斜率和y轴交点都可能不同会有轻微变化,短期相邻帧帧拟合出的差别应该不大 }
2.Localtime时间点的获取(预测出来的红色点对应的纵轴)
int64_t TimestampExtrapolator::ExtrapolateLocalTime(uint32_t timestamp90khz) { //通过最后拟合出的线性关系计算出预期的本地时间Localtime return localTimeMs; }
3.渲染时刻的计算
int64_t VCMTiming::RenderTimeMsInternal(uint32_t frame_timestamp, int64_t now_ms) const { ...... int64_t estimated_complete_time_ms = ts_extrapolator_->ExtrapolateLocalTime(frame_timestamp); ...... return estimated_complete_time_ms + actual_delay; }
estimated_complete_time_ms 其实就是通过拟合直线结合该帧的时间戳预测出来的Localtime,actual_delay通过走读代码会发现最终 actual_delay = jitter_delay_ms_ + RequiredDecodeTimeMs() + render_delay_ms_。
渲染时刻estimated_complete_time_ms + actual_delay含义就是上图的红色点为基准计算出来了最上方黑色虚线对应的渲染时刻。
4.计算等待时间
int64_t VCMTiming::MaxWaitingTime(int64_t render_time_ms, int64_t now_ms) const { MutexLock lock(&mutex_); const int64_t max_wait_time_ms = render_time_ms - now_ms - RequiredDecodeTimeMs() - render_delay_ms_; return max_wait_time_ms; }
可以看出等待时间 = 渲染时刻 - 接收该帧时刻 - 解码和渲染耗时;该表达式对应上图中最后一帧绿色点到灰色虚线的距离(wait_ms)。
等待延时计算出来之后将该帧抛入到待解码任务队列中,等待wait_ms之后刚好在灰色虚线对应的时刻进行解码,经历了解码时间以及渲染消耗时间之后,渲染时刻刚好落在最上方黑色虚线上,上方所有绿色的点经过合适的wait_ms之后都会在最上方黑色虚线的时间点上显示。虽然每个绿色点到达时间抖动很大最后输出的画面仍是平滑效果,达到去抖动的目的。
还要注意的一点是,虽然我画的横轴每个时间戳是均匀的,但实际每帧数据的时间戳并不一定非要按照固定时间等间隔增长,每帧视频数据的时间戳应该由采集时刻决定按照采集的时刻进行设置,这样最终的渲染时间也会落到最上方的黑色虚线上,并且渲染每帧的间隔和采集每帧数据的间隔也是一样的。如果发送端采集的时刻有抖动,时间戳强行设置均匀增长,那么经过去抖动模块后渲染每帧数据的时间间隔虽然是均匀的,但并不是发送端采集每帧数据的时刻的间隔,这样做不仅不会优化抖动,反而会引入抖动。所以说如果发送端采集数据就有抖动,在接收端能显示的最好的效果就是按照采集的时刻进行渲染,在发送端将时间戳强行设置均匀是不可取的。
小结
以上就是本人对jitterbuffer整个核心去抖逻辑的分析,如果存在问题欢迎指正。