文章目录
概要
网上很多介绍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
从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)接口中的代码。
二、每帧等待延时的计算
网上很多帖子对该时间的计算描述不清,把该点理解清楚是理解整个去抖模块的核心,以下我画了一个示意图用来分析等待延时是如何计算出来的
上图横轴为每一帧的时间戳。
纵轴为每帧完整时进行处理通过系统接口获取的当前时间now_ms(也可以理解为一帧在接收端接收的时间)。
其中绿色的点为每帧实际的时间戳对应的实际接收时间now_ms。
最下方黑色虚线为结合所有帧的时间戳和now_ms经过卡尔曼滤波之后拟合出来的一个直线。
红色点为最后这一帧根据其自身时间戳在拟合的线性关系上预测出的点,其纵轴对应的为预测出的本地时间Localtime。
红色点上方的绿色点为实际该帧在接端接收的时间。
灰色的虚线为每帧的应该解码的时间点。
最上方黑色虚线为每帧渲染的时间点。
下方黑色虚线和灰色虚线之间的距离为抖动延时jitterdelay。
上方黑色虚线和灰色虚线之间的距离为解码耗时+渲染耗时,解码耗时和渲染耗时基本不变。
要计算每帧的等待延时主要分为以下几个步骤
1.最下方黑色虚线的拟合
voidTimestampExtrapolator::Update(int64_t tMs,uint32_t ts90khz){//每来一帧都会调用该接口,将这一帧的接收时间和时间戳传入用来拟合上图中最下方的黑色虚线//实际每个帧到达时拟合的虚线斜率和y轴交点都可能不同会有轻微变化,短期相邻帧帧拟合出的差别应该不大}
2.Localtime时间点的获取(预测出来的红色点对应的纵轴)
int64_tTimestampExtrapolator::ExtrapolateLocalTime(uint32_t timestamp90khz){//通过最后拟合出的线性关系计算出预期的本地时间Localtimereturn localTimeMs;}
3.渲染时刻的计算
int64_tVCMTiming::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_tVCMTiming::MaxWaitingTime(int64_t render_time_ms,int64_t now_ms)const{
MutexLock lock(&mutex_);constint64_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整个核心去抖逻辑的分析,如果存在问题欢迎指正。
版权归原作者 脸子姐 所有, 如有侵权,请联系我们删除。