1)前言
- 前一篇文章分析了
FrameBuffer
模块对视频帧的插入原理,以及出队(送到解码队列)的机制。 - 在出队的过程中涉及到了很多和延迟相关的信息,没有分析,诸如渲染时间的计算、帧延迟的计算、抖动的计算等都未进行相应的分析。
- 其中
FrameBuffer
延迟JitterDleay
对视频流的单向延迟有重要的影响,很大程度上决定了应用的实时性,本文结合参考文献对JitterDleay
的计算进行深入原理分析。 - 本文首先介绍gcc算法的Arrival-time model模型
- 其次介绍Arrival-time filter(卡尔曼滤波)的公式推导
- 最后分析结合代码分析卡尔曼滤波在
JitterDleay
中的应用,以及了解jitter delay究竟描述的是个啥东西
2)Arrival-time model模型介绍
- 上图为网络传输过程中相邻两帧数据的到达时间模型
- 已知T(i-1)为第(i-1)帧数据发送时间,T(i)为第(i)帧数据发送时间
- t(i)为第i帧数据的接收时间,t(i-1)为第(i-1)帧数据的接收时间
- 理论上若传输过程中无噪声(网络噪声)和其他因素(传输大帧引起的延迟)的影响,第i帧传输曲线应该如上述红色线条(这样jitter delay应该等于0),但正由于网络噪声和传输大帧导致的影响使得第I帧传输的时延迟可能大于第i-1帧,也或者小于第i-1帧,所以就有了jitter delay
- webrtc的
FrameBuffer
模块中通过获取当前的帧间延迟.来告知解码队列何时解码该帧比较合适,从而来让接收端所接收到的视频帧能够较为平滑的进入到解码器,从而缓解卡顿现象 - 通过上图可以理论建模如下:
d(i) = t(i) - t(i-1) - (T(i) - T(i-1)) (2.1.1)
= L(i)/C(i) - L(i-1)/C(i-1) + w(i)
L(i)-L(i-1)
= -------------- + w(i)
C(i)
= dL(i)/C(i) + w(i)
- d(i) 就是最终的帧间延迟
- 其中
C(i)
为信道传输速率、L(i)
为每帧数据的数据量,dL(i)
为两帧数据的数据量的差值,以上假定传输速率恒定 - 而
w(i)
为是随机过程w的一个样本,它是C(i)、当前发送数据量和当前发送速率的函数,并假定w为高斯白噪声 - 当传输信道过载发送数据时,w(i)的方差将变大,传输信道空载时w(i)将随之变小,其他情况w(i)为0
- 若
t(i) - t(i-1) > T(i) - T(i-1)
表示帧I相对帧i-1的延迟大了 - 若将网络排队延迟
m(i)
从w(i)
中提取出来使得过程噪声的均值为0,则会得出如下:
w(i) = m(i) + v(i)
d(i) = dL(i)/C(i) + m(i) + v(i)
-
m(i)
表示网络排队延迟(如经历路由器的时候的排队延迟),v(i)
表示测量噪声(如最大帧数据量和平均每帧数据量的计算误差,时间同步等)。 - 以上就是帧间延迟的基本模型,在该模型中我们实际要求的是利用卡尔曼滤波来求得
C(i)
和m(i)
- 并通过迭代使得
C(i)
和m(i)
的误差最小,从而得到最优的d(i)帧间延迟
3)Kalman filter 建模及理论推导
3.1 卡尔曼滤波-建立状态空间方程
theta(i) = A * theta(i-1) + u(i-1) P(u) ~ (0,Q)
= theta(i-1) + u(i-1) (3.1.1)
Q(i) = E{u_bar(i) * u_bar(i)^T}
diag(Q(i)) = [10^-13 10^-3]^T
theta(i)为状态空间变量,其中A为状态转移矩阵,该案例取值为二维单位矩阵,u(i-1)为过程噪声(无法测量),其概率分布服从正太分布,数学期望为0,协方差矩阵为Q,推荐为对角矩阵
theta(i)包含两个变量如下
theta_bar(i) = [1/C(i) m(i)]^T
-
1/C(i)
为信道传输速率的倒数 -
m(i)
为帧i的网络排队时延(路由器或者交换机处理数据包排队所消耗的时间) -
C(i)
和m(i)
也是我们实际要求得的目标值
3.2 卡尔曼滤波-建立观测方程
- 观测方程如下:
d(i) = H * theta(i) + v(i) P(V) ~ (0,R) (3.2.2)
- 上述观测方程测量的是时间,为一维方程
- H为观测系数矩阵,其定义如下:
h_bar(i) = [dL(i) 1]^T
H = h_bar(i)^T = [dL(i) 1]
dL(i)
为第i帧和第i-1帧的数据量之差(detal L(i))
将其变形为google 官方公式如下:
d(i) = h_bar(i)^T * theta_bar(i) + v(i) (3.2.3)
- v(i)为观测噪声,同样其概率分布服从正太分布,数学期望为0,协方差矩阵为R
variance var_v = sigma(v,i)^2
R(i) = E{v_bar(i) * v_bar(i)^T}
= var_v
- 将
h_bar
向量代入到(3.2.3)
式中
d(i) = h_bar(i)^T * theta_bar(i) + v(i)
= [dL(i) 1] * [1/C(i) m(i)]^T + v(i) (3.2.4)
- 有了状态方程和测量方程则可以根据卡尔曼滤波的预测和校正模型进行估计
3.3 卡尔曼滤波-预测计算先验估计
- 综合上述公式可求得先验估计值如下:
- 预测其实就是使用上一次的最优结果预测当前的值
- 首先是计算先验估计的误差协方差
theta_hat^-(i) = theta_hat(i-1) + u(i-1) (3.3.1)
-
theta_hat^-(i)
第i帧的先验估计值 - 其次根据先验估计值计算先验估计的误差协方差
3.4 卡尔曼滤波-预测计算先验估计误差协方差
e^-(i) = theta(i) - theta_hat^-(i) P(E(i)) ~ (0 , P) (3.4.1)
P^-(i) = {e^-(i) * e^-(i)^T}
= E{(theta(i) - theta_hat^-(i)) * (theta(i) - theta_hat^-(i))^T} (3.4.2)
= A * P(i-1) * A^T + Q (3.4.3)
= P(i-1) + Q
= E(i-1) + Q (3.4.4)
-
e^-(i)
表示当前帧的实际值和估计值之间的误差,也就是先验估计的误差 -
P^-(i)
表示当前帧的先验误差协方差 -
P(i-1)
为上一次的误差协方差 - 也就是说先验估计的误差协方差等于上一次的后验估计的误差协方差 + 过程噪声的的误差协方差
- 有了先验估计的误差协方差之后就是计算当前帧的最优卡尔曼增益为迭代做准备
3.5 卡尔曼滤波-校正计算卡尔曼增益
- 此处不做数学理论推导,直接给出如下公式
P^-(i) * H^T
k_bar(i) = ------------------------------------------------------
H * P^-(i) * H^T + R
P^-(i) * h_bar(i)
= ------------------------------------------------------ (3.5.1)
h_bar(i)^T * P^-(i) * h_bar(i) + R
( E(i-1) + Q(i) ) * h_bar(i)
= ------------------------------------------------------ (3.5.2)
var_v_hat(i) + h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i)
- 其中
R = var_v_hat(i)
- 在webrtc gcc算法中R表示测量误差协方差,它使用指数平均滤波器,通过如下方法计算R
The variance var_v(i) = sigma_v(i)^2 is estimated using an exponential averaging filter, modified for variable sampling rate
var_v_hat(i) = max(beta * var_v_hat(i-1) + (1-beta) * z(i)^2, 1) (3.5.3)
beta = (1-chi)^(30/(1000 * f_max)) (3.5.4)
- 测量误差协方差最小取值为1
- z(i)为网络残差(当前测量值 - 上一次估计值)
3.6 卡尔曼滤波-校正计算后验估计值
theta_hat(i) = theta_hat^-(i) + k_bar(i) * (d(i) - H * theta_hat^-(i))
= theta_hat(i-1) + k_bar(i) * (d(i) - H * theta_hat(i-1)) (3.6.1)
= theta_hat(i-1) + k_bar(i) * d(i) - k_bar(i) * H * theta_hat(i-1)
= (1 - k_bar(i) * H) * theta_hat(i-1) + k_bar(i) * d(i) (3.6.2)
k_bar(i) ~ [0 ~ 1/H]
-
theta_hat(i)
为第i帧的估计值,叫做后验估计值 -
k_bar(i)
为当前帧(第i帧)的卡尔曼增益 - 卡尔曼滤波的目标就是去寻找适当的
k_bar(i)
,使得theta_hat(i)
的值更加趋向theta(i)
(也就是实际值),如何去选择值肯定是和误差v(i)
以及u(i-1)
息息相关的 - 当
k_bar(i)
趋近0的时候,更相信算出来的结果(估计值) - 当
k_bar(i)
趋近1/H的时候,更相信测量出来的结果 - 将上述公式进行替换如下
z(i) = d(i) - h_bar(i)^T * theta_hat(i-1) (3.6.3)
theta_hat(i) = theta_hat(i-1) + z(i) * k_bar(i) (3.6.4)
-
z(i)
也叫网络残差(当前测量值 - 上一次估计值) - 上述公式则为google给出的官方公式
- 结合
(3.6.1)
和(3.5.2)
可知当var_v_hat(i)
越大时,说明测量误差较大,此时卡尔曼增益将越小,最终的估计值将更加趋近与一次的估计值
3.7 卡尔曼滤波-更新误差协方差
- 计算后验估计误差协方差
e(i) = theta(i) - theta_hat(i) P(E(i)) ~ (0 , P) (3.7.1)
P(i) = E{e(i) * e(i)^T} (3.7.2)
= E{(theta(i) - theta_hat(i)) * (theta(i) - theta_hat(i))^T} (3.7.3)
= E(i)
e(i)
表示当前帧的实际值和估计值之间的误差P(i)
表示当前帧的误差协方差,此处叫做后验误差协方差综合(3.6.1)、(3.5.1)最后得出
P(i) = (I - k_bar(i) * H) * P^-(i) (3.7.4)
= (I - k_bar(i) * h_bar(i)^T) * (E(i-1) + Q(i)) (3.7.5)
= E(i)
- 其中I为2*2的单位矩阵
3.8 卡尔曼滤波-系统模型图
-
结合公式3.6.2我们可以得出如下模型图
m(i) = (1 − K(i)) * m(i−1) + K(i) * (dm(i))
4)WebRTC中JitterDelay的计算和迭代过程
- 步骤1、在
FrameBuffer
模块中通过Decode
任务队列,通过重复任务调用FindNextFrame
和GetNextFrame
获取待解码的视频帧 - 步骤2、然后通过
VCMInterFrameDelay模块的CalculateDelay()
函数依据当前找到的帧的rtp时间戳以及接收时间计算帧间延迟得到frameDelayMS
- 步骤3、以
frameDelayMS
和当前帧的frameSizeBytes
(当前帧的数据大小字节)为参数,调用VCMJitterEstimator::UpdateEstimate()
函数对卡尔曼滤波器进行迭代,并计算出当前帧的最优jitterDelay
- 步骤4、对卡尔曼滤波进行校准供下一次迭代使用
- 最后通过
VCMJitterEstimator::GetJitterEstimate()
函数返回最优估计jitterdelay
并将其作用到VCMTiming
模块供期望RenderDelay
的计算 - 其中步骤2中的
frameDelayMS
的计算对应了第二节的Arrival-time model
示意图,并将T(i)代表第i帧的发送的rtp时间戳,T(i-1)为第i-1帧的rtp时间戳,t(i)为第i帧的本地接收时间,t(i-1)为第i-1帧的本地接收时间 - 步骤2的实现代码如下:
bool VCMInterFrameDelay::CalculateDelay(uint32_t timestamp,
int64_t* delay,
int64_t currentWallClock) {
.....
// Compute the compensated timestamp difference and convert it to ms and round
// it to closest integer.
_dTS = static_cast(
(timestamp + wrapAroundsSincePrev * (static_cast(1) << 32) -
_prevTimestamp) /
90.0 +
0.5);\
// frameDelay is the difference of dT and dTS -- i.e. the difference of the
// wall clock time difference and the timestamp difference between two
// following frames.
*delay = static_cast(currentWallClock - _prevWallClock - _dTS);
_prevTimestamp = timestamp;
_prevWallClock = currentWallClock;
return true;
}
-
_dTS
表示相邻两帧的rtp时间戳 -
_prevWallClock
为上一帧的本地接收时间 -
currentWallClock
为当前帧的本地接收时间 - 接下来首先介绍
JitterDelay
的计算流程,最后分析卡尔曼滤波预测和校正过程
4.1)计算JitterDelay
- 在
FrameBuffer
模块中使用如下函数获取
int VCMJitterEstimator::GetJitterEstimate(
double rttMultiplier,
absl::optional rttMultAddCapMs) {
//调用CalculateEstimate()计算当前的jitterDelay,OPERATING_SYSTEM_JITTER默认为10ms
//这就意味着默认最小的jittterDelay至少是10ms?
double jitterMS = CalculateEstimate() + OPERATING_SYSTEM_JITTER;
uint64_t now = clock_->TimeInMicroseconds();
//kNackCountTimeoutMs = 60000
// FrameNacked会更新_latestNackTimestamp单位为微秒
//1分钟内若所有帧都未丢包则清除
if (now - _latestNackTimestamp > kNackCountTimeoutMs * 1000)
_nackCount = 0;
if (_filterJitterEstimate > jitterMS)
jitterMS = _filterJitterEstimate;
if (_nackCount >= _nackLimit) {//_nackLimit
if (rttMultAddCapMs.has_value()) {
jitterMS +=
std::min(_rttFilter.RttMs() * rttMultiplier, rttMultAddCapMs.value());
} else {
jitterMS += _rttFilter.RttMs() * rttMultiplier;
}
}
....
return rtc::checked_cast(std::max(0.0, jitterMS) + 0.5);
}
-
FrameBuffer
只针对未重传过包的帧进行jitterDelay
的迭代和计算,也就是说假设第i帧数据有丢包,那么该帧是不会计算JitterDelay
的,该帧的期望渲染时间计算过程中所用到的jitterDelay
值是上一帧的jitterDelay
- webrtc jitter delay对丢过包的数据帧会在
FrameBuffer::GetNextFrame()
函数中通过jitter_estimator_.FrameNacked()
函数告知VCMJitterEstimator
模块,使得_nackCount
变量自增(前提通过WebRTC-AddRttToPlayoutDelay/Enable
使能),超过3会清除 - 当
_nackCount
大于等于3的时候在时候jitterDelay
会在后续未丢包的帧中加上一个RTT乘以某个系数 - 通过参阅网上的一些文摘表示,如果某帧有丢包如果不处理该RTT会很容易造成卡顿
- 以上函数的核心主要是调用
CalculateEstimate()
函数计算jitterDelay
// Calculates the current jitter estimate from the filtered estimates.
double VCMJitterEstimator::CalculateEstimate() {
double ret = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
.......
_prevEstimate = ret;
return ret;
}
-
_theta[0]
记录的是信道传输速率的倒数,也就是上述卡尔曼状态方程中的1/c(i)
-
_maxFrameSize
表示自会话开始以来所收到的最大帧大小 -
_avgFrameSize
表示当前平均帧大小 -
NoiseThreshold()
计算噪声补偿阀值
double VCMJitterEstimator::NoiseThreshold() const {
double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
if (noiseThreshold < 1.0) {
noiseThreshold = 1.0;
}
return noiseThreshold;
}
-
_noiseStdDevs
表示噪声标准差系数取值2.33 -
_varNoise
表示测量噪声方差对应公式(3.2.3)
中的v(i),默认初始值为4.0 -
_noiseStdDevOffset
噪声标准差偏移(噪声扣除常数)取值30.0ms
4.2)JitterDelay迭代更新机制
- 通过第4大节中的截图看出,卡尔曼的更新是通过
Framebuffer
模块调用其UpdateEstimate
函数来实现的
// Updates the estimates with the new measurements.
void VCMJitterEstimator::UpdateEstimate(int64_t frameDelayMS,
uint32_t frameSizeBytes,
bool incompleteFrame /* = false */) {
//1)计算当前帧和上一帧的数据量之差
int deltaFS = frameSizeBytes - _prevFrameSize;
//2)计算_avgFrameSize平均每帧数据的大小
if (_fsCount < kFsAccuStartupSamples) {
_fsSum += frameSizeBytes;
_fsCount++;
} else if (_fsCount == kFsAccuStartupSamples) {//kFsAccuStartupSamples取值为5超过5帧开始计算平均帧大小
// Give the frame size filter.
_avgFrameSize = static_cast(_fsSum) / static_cast(_fsCount);
_fsCount++;
}
/*3)若当前输入帧的大小比平均每帧数据的数据量要大,则对其进行滑动平均处理,比如说如果当前是一个I帧,数据量显然会比较大,
默认incompleteFrame为false,所以每帧都会计算平均值*/
if (!incompleteFrame || frameSizeBytes > _avgFrameSize) {
//滑动平均算法,_phi的取值为0.97,取接近前30帧数据大小的平均值,求得的avgFrameSize值为接近近30帧数据的平均大小
double avgFrameSize = _phi * _avgFrameSize + (1 - _phi) * frameSizeBytes;
//如果I帧数据量会比较大,如下的判断会不成立,偏移太大不计赋值_avgFrameSize
if (frameSizeBytes < _avgFrameSize + 2 * sqrt(_varFrameSize)) {
// Only update the average frame size if this sample wasn't a key frame.
_avgFrameSize = avgFrameSize;
}
// Update the variance anyway since we want to capture cases where we only
// get key frames.
//3.1)此处更新平均帧大下的方差,默认方差为100,取其最大值,根据_varFrameSize可以得出在传输过程中每帧数据大小的均匀性
// 若方差较大则说明帧的大小偏离平均帧大小的程度越大,则均匀性也越差
_varFrameSize = VCM_MAX(
_phi * _varFrameSize + (1 - _phi) * (frameSizeBytes - avgFrameSize) *
(frameSizeBytes - avgFrameSize),
1.0);
}
// Update max frameSize estimate.
//4)计算最大帧数据量
_maxFrameSize =
VCM_MAX(_psi * _maxFrameSize, static_cast(frameSizeBytes));
if (_prevFrameSize == 0) {
_prevFrameSize = frameSizeBytes;
return;
}
//赋值上一帧数据大小
_prevFrameSize = frameSizeBytes;
// Cap frameDelayMS based on the current time deviation noise.
/*5) 根据当前时间偏移噪声求frameDelayMS,_varNoise为噪声方差,默认4.0很显然该值在传输过程中会变化,
time_deviation_upper_bound_为时间偏移上限值,默认为3.5,所以默认初始值计算出来max_time_deviation_ms
为7,对于帧率越高,默认输入的frameDelayMS会越小,这里和max_time_deviation_ms去最小值,当噪声的方差越大
max_time_deviation_ms的值月越大,其取值就会越接近取向传入的frameDelayMS*/
int64_t max_time_deviation_ms =
static_cast(time_deviation_upper_bound_ * sqrt(_varNoise) + 0.5);
frameDelayMS = std::max(std::min(frameDelayMS, max_time_deviation_ms),
-max_time_deviation_ms);
/*6)根据得出的延迟时间计算样本与卡尔曼滤波器估计的期望延迟之间的延迟差(反映网络噪声的大小),计算公式为
frameDelayMS - (_theta[0] * deltaFSBytes + _theta[1])
当前测量值 - 上一次卡尔曼滤波后的估计值 对应公式3.6.3和3.6.4*/
double deviation = DeviationFromExpectedDelay(frameDelayMS, deltaFS);
// Only update the Kalman filter if the sample is not considered an extreme
// outlier. Even if it is an extreme outlier from a delay point of view, if
// the frame size also is large the deviation is probably due to an incorrect
// line slope.
//根据注释的意思是只有当样本不被认为是极端异常值时才更新卡尔曼滤波器,言外之意就是网络残差值不能超过
// _numStdDevDelayOutlier * sqrt(_varNoise) = 30ms 默认值,随_varNoise的大小变化而变化
if (fabs(deviation) < _numStdDevDelayOutlier * sqrt(_varNoise) ||
frameSizeBytes >
_avgFrameSize + _numStdDevFrameSizeOutlier * sqrt(_varFrameSize)) {
// Update the variance of the deviation from the line given by the Kalman
// filter.
EstimateRandomJitter(deviation, incompleteFrame);
// Prevent updating with frames which have been congested by a large frame,
// and therefore arrives almost at the same time as that frame.
// This can occur when we receive a large frame (key frame) which has been
// delayed. The next frame is of normal size (delta frame), and thus deltaFS
// will be << 0. This removes all frame samples which arrives after a key
// frame.
if ((!incompleteFrame || deviation >= 0.0) &&
static_cast(deltaFS) > -0.25 * _maxFrameSize) {
// Update the Kalman filter with the new data
KalmanEstimateChannel(frameDelayMS, deltaFS);
}
} else { // 如果网络残差太大,说明噪声偏移太大,需要对测量噪声进行校正,本次不进行卡尔曼预测和校正
int nStdDev =
(deviation >= 0) ? _numStdDevDelayOutlier : -_numStdDevDelayOutlier;
EstimateRandomJitter(nStdDev * sqrt(_varNoise), incompleteFrame);
}
// Post process the total estimated jitter
//6) 求得当前帧的jitterDelay最优估计值
if (_startupCount >= kStartupDelaySamples) {
PostProcessEstimate();
} else {
_startupCount++;
}
}
-
_avgFrameSize
表示平均帧大小,使用滑动平均算法 -
_varFrameSize
表示传输过程中帧大小的方差,象征了传输过程中每帧数据量的均匀性,_varFrameSize
越大表示均匀性越差,所以在传输过程中,在发送端应当尽量使得每帧数据的数据量尽可能的接近(如尽量减少I帧的发送,按需所取) -
_maxFrameSize
表示自会话以来最大帧的数据量大小 -
deltaFS
表示当前帧和上一帧的数据量只差 -
deviation
表示网络残差,使用当前测量值减去估计值求得,代码如下:
double VCMJitterEstimator::DeviationFromExpectedDelay(
int64_t frameDelayMS,
int32_t deltaFSBytes) const {
return frameDelayMS - (_theta[0] * deltaFSBytes + _theta[1]);
}
- 以上计算对应公式
3.6.3
和3.6.4
- 上述第5步中根据当前网络残差是否在某范围内来决定是只更新测量噪声的方差还是即更新测量噪声误差也进行预测和校正工作
- 接下来着重分析
EstimateRandomJitter
测量噪声方差的计算和KalmanEstimateChannel
函数的预测以及校正分析 - 同时以上
UpdateEstimate
函数的过程中计算平均帧大小以及平均帧大小方差都使用了移动平均算法如下:
double avgFrameSize = _phi * _avgFrameSize + (1 - _phi) * frameSizeBytes;
_varFrameSize = VCM_MAX(
_phi * _varFrameSize + (1 - _phi) * (frameSizeBytes - avgFrameSize) *
(frameSizeBytes - avgFrameSize),1.0);
- 使用移动平均算法的原因是让计算出来的值和前面帧的值有关联,确保离散数据的平滑性,当_phi取1的时候就不平滑了
4.3)更新误差方差
void VCMJitterEstimator::EstimateRandomJitter(double d_dT,
bool incompleteFrame) {
uint64_t now = clock_->TimeInMicroseconds();
//1)对帧率进行采样统计
if (_lastUpdateT != -1) {
fps_counter_.AddSample(now - _lastUpdateT);
}
_lastUpdateT = now;
if (_alphaCount == 0) {
assert(false);
return;
}
//2) alpha = 399
double alpha =
static_cast(_alphaCount - 1) / static_cast(_alphaCount);
_alphaCount++;
if (_alphaCount > _alphaCountMax)
_alphaCount = _alphaCountMax;//_alphaCountMax = 400
// In order to avoid a low frame rate stream to react slower to changes,
// scale the alpha weight relative a 30 fps stream.
double fps = GetFrameRate();
if (fps > 0.0) {
double rate_scale = 30.0 / fps;
// At startup, there can be a lot of noise in the fps estimate.
// Interpolate rate_scale linearly, from 1.0 at sample #1, to 30.0 / fps
// at sample #kStartupDelaySamples.
if (_alphaCount < kStartupDelaySamples) {
rate_scale =
(_alphaCount * rate_scale + (kStartupDelaySamples - _alphaCount)) /
kStartupDelaySamples;//kStartupDelaySamples = 30
}
//alpha = pow(399/400,30.0 / fps)
alpha = pow(alpha, rate_scale);
}
double avgNoise = alpha * _avgNoise + (1 - alpha) * d_dT;
double varNoise =
alpha * _varNoise + (1 - alpha) * (d_dT - _avgNoise) * (d_dT - _avgNoise);
if (!incompleteFrame || varNoise > _varNoise) {
_avgNoise = avgNoise;
_varNoise = varNoise;
}
if (_varNoise < 1.0) {
// The variance should never be zero, since we might get stuck and consider
// all samples as outliers.
_varNoise = 1.0;
}
}
- 测量误差的更新和帧率相关,
alpha
为指数移动平均算法的指数加权系数收帧率大小的影响较大,数学模型为递减指数函数,当fps变低时,rate_scale增大,从而alpha
会变小,最终avgNoise
的值会增大,意味着此时噪声增大,实时帧率越接近30fps,效果越理想 -
_avgNoise
为自会话以来的平均测量噪声,_varNoise
会噪声方差,变量d_dT
表示网络残差 - 噪声方差以及平均噪声的更新都采用指数型移动平均算法求取,其特性是指数式递减加权的移动平均,各数值的加权影响力随时间呈指数式递减,时间越靠近当前时刻的数据加权影响力越大
double avgNoise = alpha * _avgNoise + (1 - alpha) * d_dT;
double varNoise =
alpha * _varNoise + (1 - alpha) * (d_dT - _avgNoise) * (d_dT - _avgNoise);
- 以上计算对应公式
3.5.3
和3.5.4
- 当
_varNoise
较大时,说明此时网络噪声较大,此时后续的卡尔曼增益计算的结果会较小,说明测量误差较大,使得当前帧的jitterDelay更趋近上一次的估计值,从而收当前网络残差的影响更小一些
4.4)Kalman Filter 预测及校正
- 结合第3章界的理论推导,预测首先利用上一次的估计结果求得先验估计误差协方差
- 在
VCMJitterEstimator
模块中定义如下核心矩阵
double _thetaCov[2][2]; // Estimate covariance 先验估计误差协方差
double _Qcov[2][2]; // Process noise covariance 过程噪声协方差对应Q向量
/**
@ frameDelayMS:为测量出来的当前帧和上一帧的帧间抖动延迟
@ deltaFSBytes:当前帧的数据量和上一帧的数据量之差
**/
void VCMJitterEstimator::KalmanEstimateChannel(int64_t frameDelayMS,
int32_t deltaFSBytes) {
double Mh[2];//P(i) = E[1/c(i) m(i)]
double hMh_sigma;
double kalmanGain[2];
double measureRes;
double t00, t01;
// Kalman filtering
/*1)计算先验估计误差协方差
结合公式3.4.1~3.4.4
e^-(i) = theta(i) - theta_hat^-(i)
P^-(i) = {e^-(i) * e^-(i)^T}
= E{(theta(i) - theta_hat^-(i)) * (theta(i) - theta_hat^-(i))^T}
= A * P(i-1) * A^T + Q
= P(i-1) + Q
= E(i-1) + Q
当前帧(i)的先验估计误差协防差 = 上一帧(i-1)的误差协方差 + 过程噪声协方差
*/
//Prediction
//M = M + Q = E(i-1) + Q
_thetaCov[0][0] += _Qcov[0][0];
_thetaCov[0][1] += _Qcov[0][1];
_thetaCov[1][0] += _Qcov[1][0];
_thetaCov[1][1] += _Qcov[1][1];
/*
2) 校正 根据公式3.5.1~3.5.2计算卡尔曼增益
P^-(i) * H^T
k_bar(i) = ------------------------------------------------------
H * P^-(i) * H^T + R
P^-(i) * h_bar(i)
= ------------------------------------------------------ (3.5.1)
h_bar(i)^T * P^-(i) * h_bar(i) + R
( E(i-1) + Q(i) ) * h_bar(i)
= ------------------------------------------------------ (3.5.2)
var_v_hat(i) + h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i)
*/
// Kalman gain
// K = M*h'/(sigma2n + h*M*h') = M*h'/(1 + h*M*h') = M*h'/(var_v_hat(i) + h*M*h')
// h = [dFS 1] 其中dFS对应的入参deltaFSBytes
// Mh = M*h' = _thetaCov[2][2] * [dFS 1]^
// hMh_sigma = h*M*h' + R = h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i) + R
Mh[0] = _thetaCov[0][0] * deltaFSBytes + _thetaCov[0][1];// 对应1/C(i) 信道传输速率的误差协方差
Mh[1] = _thetaCov[1][0] * deltaFSBytes + _thetaCov[1][1];// 对应网络排队延迟m(i)的误差协方差
// sigma weights measurements with a small deltaFS as noisy and
// measurements with large deltaFS as good
if (_maxFrameSize < 1.0) {
return;
}
//sigma为测量噪声标准差的指数平均滤波,对应的是测量噪声的协方差R
double sigma = (300.0 * exp(-fabs(static_cast(deltaFSBytes)) /
(1e0 * _maxFrameSize)) +
1) *
sqrt(_varNoise);
if (sigma < 1.0) {
sigma = 1.0;
}
// hMh_sigma 对应H * P^-(i) * H^T = h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i) + R
// 对应公式(3.5.1)
//[dFS 1]^ * Mh = dFS * Mh[0] + Mh[1]
hMh_sigma = deltaFSBytes * Mh[0] + Mh[1] + sigma;
if ((hMh_sigma < 1e-9 && hMh_sigma >= 0) ||
(hMh_sigma > -1e-9 && hMh_sigma <= 0)) {
assert(false);
return;
}
//计算卡尔曼增益Mh / hMh_sigma
kalmanGain[0] = Mh[0] / hMh_sigma;
kalmanGain[1] = Mh[1] / hMh_sigma;
/*
3)根据公式3.6.1~3.6.4校正计算后验估计值
theta_hat(i) = theta_hat^-(i) + k_bar(i) * (d(i) - H * theta_hat^-(i))
= theta_hat(i-1) + k_bar(i) * (d(i) - H * theta_hat(i-1)) (3.6.1)
= theta_hat(i-1) + k_bar(i) * d(i) - k_bar(i) * H * theta_hat(i-1)
= (1 - k_bar(i) * H) * theta_hat(i-1) + k_bar(i) * d(i) (3.6.2)
其中k_bar(i) ~ [0 ~ 1/H]
z(i) = d(i) - h_bar(i)^T * theta_hat(i-1) (3.6.3)
theta_hat(i) = theta_hat(i-1) + z(i) * k_bar(i) (3.6.4)
*/
// Correction
// theta = theta + K*(dT - h*theta)
// 计算网络残差,得到最优估计值
measureRes = frameDelayMS - (deltaFSBytes * _theta[0] + _theta[1]);
_theta[0] += kalmanGain[0] * measureRes; //公式(3.6.4)
_theta[1] += kalmanGain[1] * measureRes; //公式(3.6.4)
if (_theta[0] < _thetaLow) {
_theta[0] = _thetaLow;
}
/**
4)根据公式3.7.1~3.7.4更新误差协方差,为下一次预测提供最优滤波器系数
e(i) = theta(i) - theta_hat(i) P(E(i)) ~ (0 , P) (3.7.1)
P(i) = E{e(i) * e(i)^T} (3.7.2)
= E{(theta(i) - theta_hat(i)) * (theta(i) - theta_hat(i))^T} (3.7.3)
= (I - k_bar(i) * H) * P^-(i) (3.7.4)
= (I - k_bar(i) * h_bar(i)^T) * (E(i-1) + Q(i)) (3.7.5)
= E(i)
*/
// M = (I - K*h)*M
t00 = _thetaCov[0][0];
t01 = _thetaCov[0][1];
_thetaCov[0][0] = (1 - kalmanGain[0] * deltaFSBytes) * t00 -
kalmanGain[0] * _thetaCov[1][0];
_thetaCov[0][1] = (1 - kalmanGain[0] * deltaFSBytes) * t01 -
kalmanGain[0] * _thetaCov[1][1];
_thetaCov[1][0] = _thetaCov[1][0] * (1 - kalmanGain[1]) -
kalmanGain[1] * deltaFSBytes * t00;
_thetaCov[1][1] = _thetaCov[1][1] * (1 - kalmanGain[1]) -
kalmanGain[1] * deltaFSBytes * t01;
// Covariance matrix, must be positive semi-definite.
assert(_thetaCov[0][0] + _thetaCov[1][1] >= 0 &&
_thetaCov[0][0] * _thetaCov[1][1] -
_thetaCov[0][1] * _thetaCov[1][0] >=
0 &&
_thetaCov[0][0] >= 0);
}
以上分成4步完成预测以及校准过程,其中第4步涉及到举证的减法以及乘法的运算
-
下面给出更新误差协方差的矩阵运算公式
到此为止卡尔曼滤波的一次更新以及迭代完成,总结其过程,卡尔曼滤波主要是为了得到较为准确的信道传输速率的倒数
1/c(i)
以及网络排队延迟m(i)
最后通过如下计算
jitterDelay
double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
jitterDelay = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
其中
_varNoise
为测量误差,通过计算当前帧的测量帧间延迟和上一次的卡尔曼最优估计出来的传输速率的倒数1/c(i)
以及网络排队延迟m(i)
并计算出估计帧间延迟进行相减,得出网络残差依据网络残差更新测量误差平均值以及平均方差
最终将最优估计
jitterDelay
作用到vcmTiming
模块用于计算最优期望渲染时间至此代码分析完毕
4.5)webrtc jitterdelay相关数据测试
-
通过实际调试,1080P@60fps 线上测试抓得如下数据
- 以上一共近2000帧数据,从上图来看,在937帧数据左右,画面从静止编程动画,数据量瞬间增大,此时,平均帧大小的方差也随之增大,从图像右边的测量噪声的方差曲线来看,也发现测量噪声的方差随之增大
- 右下图显示的是实际测量的
frameDelayMs
、jitterMs
(估计后)、以及加上10ms操作延迟的变化曲线,由该曲线可以看出kalman filter最终确实能使实际的帧间抖动变得更加平滑,但是从曲线来看,似乎准确性有待提升。 - 综合分析,
VCMJitterEstimator
模块所得出的jitterDelay
它的核心作用是给到vcmTiming
模块用于估计期望渲染时间,为什么要这么做? - 在实际的传输过程中若完全不做处理,那么实际的帧间延迟就如上图右下方蓝色曲线所示,如果来一帧数据就送到解码器让解码器做解码并渲染,那么在数据量变化剧烈或突然出现网络波动等一系列导致帧间延迟瞬时变化较大的时候可能会出现卡顿的问题,并且视觉上也会有明显的表象
总结:
- 本文通过分析
VCMJitterEstimator
模块的实现,学习卡尔曼滤波的应用场景,并深入分析其滤波步骤等 - 通过分析
VCMJitterEstimator
模块了解webrtc视频接收流水线中jitterDelay
的用途以及更新过程,为进一步优化卡顿做铺垫
参考文献
[1].A Google Congestion Control Algorithm for Real-Time Communication draft-alvestrand-rmcat-congestion-03
[2].Analysis and Design of the Google Congestion Control for Web Real-time Communication (WebRTC)
[3].DR_CAN 卡尔曼滤波视频教程
[4].从放弃到精通!卡尔曼滤波从理论到实践