1)前言
- 前一篇文章分析了
FrameBuffer
模块对视频帧的插入原理,以及出队(送到解码队列)的机制。 - 在出队的过程中涉及到了很多和延迟相关的信息,没有分析,诸如渲染时间的计算、帧延迟的计算、抖动的计算等都未进行相应的分析。
- 同时经过上文的分析,在实际的测试过程中发现,在视频接收的过程中如果不出现丢帧的现象,那么从组帧到送入到
FrameBuffer
的缓存队列的耗时是十分小的,那么实际测试过程中的延迟究竟是怎么来的,经过上文的分析,初步得出,首先是在出队前从缓存列表中获取待解码的帧的时候会根据期望渲染时间计算延迟,这个延迟直接会作用到延迟重复任务的调度时间。 - 其次就是在实际解码过程中不同的硬件平台能力不一样硬件解码器的原理也有区别,比如有些解码器本身就会缓存视频帧也是导致实际播放延迟的一个原因。
- 再者就是音频和视频之间的同步也不会是导致播放延迟的因素之一。
- 通过前面几篇文章的一系列分析,不难看出如
VideoReceiveStream2
模块、RtpVideoStreamReceiver2
模块、FrameBuffer
模块在整个工作的过程中都复用了同一个VCMTiming
模块,在Call
模块创建VideoReceiveStream2
模块的时候被实例化,之后在其他模块中被引用。 - 接下来按照实际函数调用栈的流程对视频接收模块涉及到的时序相关信息进行一一阐述。
2)PlayoutDelay更新
- 顾名思义,叫做播放延迟,该值可在发送端通过RTP头扩展进行携带,如果未携带,默认值为{-1,-1}
- 首先看其定义如下:
#common_types.h
struct PlayoutDelay {
PlayoutDelay(int min_ms, int max_ms) : min_ms(min_ms), max_ms(max_ms) {}
int min_ms;//最小播放延迟
int max_ms;//做到播放延迟
....
}
#rtp_video_header.h
struct RTPVideoHeader {
....
PlayoutDelay playout_delay = {-1, -1};
....
}
# encoded_image.h
class RTC_EXPORT EncodedImage {
public:
...
// When an application indicates non-zero values here, it is taken as an
// indication that all future frames will be constrained with those limits
// until the application indicates a change again.
PlayoutDelay playout_delay_ = {-1, -1};
...
}
RtpFrameObject::RtpFrameObject(
......
: first_seq_num_(first_seq_num),
last_seq_num_(last_seq_num),
last_packet_received_time_(last_packet_received_time),
times_nacked_(times_nacked) {
// Setting frame's playout delays to the same values
// as of the first packet's.
SetPlayoutDelay(rtp_video_header_.playout_delay);
...
}
- 在
PacketBuffer
模块组帧过程中,每组帧成功后会执行发现帧的处理,此时会创建对应的RtpFrameObject
。 - 而在
RtpFrameObject
的构造函数会通过SetPlayoutDelay
函数为当前帧设置播放延迟时间,由此可看出如果发送端未扩展该RTP头的话,那么默认值为{-1,-1}。 - 默认是未扩展的如果需要扩展需要SDP支持如下:
"http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
-
接下来介绍,在何时会将该延迟作用到其他模块。
接上文的分析,待解码视频帧的插入驱动是由
VideoReceiveStream2:: OnCompleteFrame()
函数来驱动的,在插入视频帧前首先会将播放延迟信息作用到VCMTiming
模块,上面有介绍VideoReceiveStream2
模块、RtpVideoStreamReceiver2
模块、FrameBuffer
模块在整个工作的过程中都复用了同一个VCMTiming
实例(同一路流)。
void VideoReceiveStream2::OnCompleteFrame(
std::unique_ptr frame) {
....
//拿到PlayoutDelay引用
const PlayoutDelay& playout_delay = frame->EncodedImage().playout_delay_;
if (playout_delay.min_ms >= 0) {
frame_minimum_playout_delay_ms_ = playout_delay.min_ms;
UpdatePlayoutDelays();
}
if (playout_delay.max_ms >= 0) {
frame_maximum_playout_delay_ms_ = playout_delay.max_ms;
UpdatePlayoutDelays();
}
....
}
- 如果
playout_delay.min_ms >= 0
或者playout_delay.max_ms >= 0
都会调用UpdatePlayoutDelays
函数将该播放延迟作用到VCMTiming
模块。
void VCMTiming::set_min_playout_delay(int min_playout_delay_ms) {
rtc::CritScope cs(&crit_sect_);
min_playout_delay_ms_ = min_playout_delay_ms;
}
void VCMTiming::set_max_playout_delay(int max_playout_delay_ms) {
rtc::CritScope cs(&crit_sect_);
max_playout_delay_ms_ = max_playout_delay_ms;
}
-
VCMTiming
模块记录当前帧的min_playout_delay_ms_
和max_playout_delay_ms_
供后续延迟估计使用。
3)RenderTimeMs设置流程
- 回顾上文的分析,
FindNextFrame
函数在找待解码帧的时候会通过VCMTiming
模块获取期望渲染时间。 - 本节内容着重介绍
RenderTimeMs
的设置业务流程以及大致的原理,涉及到Kalman filter
的实现原理在第4节内容将进行详细分析。本节涉及的大致流程如下:
- 从上图左侧部分可知,在当前帧插入到
frame_
的时候如果当前帧不是重传帧的话,会使用VCMTiming
模块调用TimestampExtrapolator
模块的Update()函数依据当前帧的rtp时间戳来估计当前帧的期望接收时间,并对Kalman gain
进行校准,其原理将在第4节内容进行详细分析。
int64_t FrameBuffer::FindNextFrame(int64_t now_ms) {
....
//默认该函数调用到这里的时候期望渲染时间都还未赋值的
if (frame->RenderTime() == -1) {
//首先调用VCMTiming获取期望渲染时间,然后将其设置到Frame中,供后续使用
frame->SetRenderTime(timing_->RenderTimeMs(frame->Timestamp(), now_ms));
}
...
//得出最大等待时间
wait_ms = timing_->MaxWaitingTime(frame->RenderTime(), now_ms);
....
//取最小时间,如果在一次调度时间(未超时范围内)的话,返回wait_ms
wait_ms = std::min(wait_ms, latest_return_time_ms_ - now_ms);
wait_ms = std::max(wait_ms, 0);
return wait_ms;
}
- 以上函数返回的
wait_ms
值会直接决定重复延迟队列的执行时间(也就是等多节执行),如果wait_ms
等于0,则说明重复延迟队列会立即执行。优化该值趋向0会节省一定的延迟。
3.1)VCMTiming模块获取期望渲染时间
int64_t VCMTiming::RenderTimeMs(uint32_t frame_timestamp,
int64_t now_ms) const {
rtc::CritScope cs(&crit_sect_);
return RenderTimeMsInternal(frame_timestamp, now_ms);
}
//frame_timestamp为当前帧的时间戳以1/90k为单位,now_ms为当前Clock时间
int64_t VCMTiming::RenderTimeMsInternal(uint32_t frame_timestamp,
int64_t now_ms) const {
//如果min_playout_delay_ms_=0并且max_playout_delay_ms_=0则表示立即渲染
// 不建议赋值0,若赋值0的话jitterDelay就失效了
if (min_playout_delay_ms_ == 0 && max_playout_delay_ms_ == 0) {
// Render as soon as possible.
return 0;
}
//传入当前帧的时间戳,来得到一个平滑渲染时间,TimestampExtrapolator通过卡尔曼滤波负责期望接收时间的产生
int64_t estimated_complete_time_ms =
ts_extrapolator_->ExtrapolateLocalTime(frame_timestamp);
if (estimated_complete_time_ms == -1) {
estimated_complete_time_ms = now_ms;
}
// Make sure the actual delay stays in the range of |min_playout_delay_ms_|
// and |max_playout_delay_ms_|.
// 和min_playout_delay_ms_取最大值,min_playout_delay_ms_默认值-1,
int actual_delay = std::max(current_delay_ms_, min_playout_delay_ms_);
//和max_playout_delay_ms_求最小值,max_playout_delay_ms_默认值-1
actual_delay = std::min(actual_delay, max_playout_delay_ms_);
return estimated_complete_time_ms + actual_delay;
}
- 该函数首先判断
min_playout_delay_ms_
和max_playout_delay_ms_
是否同时为0,如果同时为0则表示会理解发送到解码队列并解码后后立即渲染。 - 若上述条件不满足,也就是说什么时候渲染依据系统框架的估计来做。
- 首先调用
TimestampExtrapolator
模块的ExtrapolateLocalTime
函数来估计出一个期望接收时间,最后将该时间和actual_delay
实际延迟相加得到最终的期望渲染时间 - 其中
actual_delay
的值是通过VCMTiming::SetJitterDelay
和VCMTiming::UpdateCurrentDelay
两个函数来进行更新的,这两个函数在下面进行分析。 - 最终得出视频帧的最终期望渲染时间 = 平滑渲染时间 + 当前实际播放延迟(它的原理有是什么)
3.2)VCMTiming模块获取调度等待时间
-
FrameBuffer
模块的FindNextFrame
函数通过该函数返回一个最大等待时间,也就是说如果找到了一帧数据,但是FrameBuffer
模块并不会立即将其发送到解码队列,而是要等待一段时间,再发送到解码器,该函数的作用就是得到等待多长时间发送到解码队列进行解码
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;
}
- 上述代码显示最大等待时间 (何时发到解码队列)=
期望渲染时间 - 当前时间 - 解码所需要的的时间 - 渲染延迟的时间
。 - 其中正常情况下
期望渲染时间
是根据卡尔曼滤波理论估计出来的。 -
render_delay_ms_
默认为10ms,可通过VCMTiming::set_render_delay
进行设置,默认在初始化阶段实例化VideoReceiveStream2
模块的时候在其构造函数中有调用该函数,可通过修改webrtc::VideoReceiveStream::Config::render_delay_ms
变量进行设置。 - 经过以上的分析最大的困难之处就是平滑渲染时间
estimated_complete_time_ms
的估计过程,在后续将专门分析卡尔曼的原理。 - 按照正常的流程如果卡尔曼估计出来平滑渲染时间比较大,然后解码所需要的的时间已知的情况下,那么优化就必须放在卡尔曼滤波器的身上。
- 通过上述的分析,期望渲染时间的延迟会直接影响到
FrameBuffer
模块重复延迟队列的调度,也就决定了当前帧播放的延迟。 -
用时间轴来描述当前时间,进入解码队列时间,解码延迟时间,渲染等待时间的关系,如下:
4)TimestampExtrapolator Kalman filter期望渲染时间估计
- 本节内容从四个方面来进行介绍。
- 首先介绍卡尔曼理论5大公式以及基于rtp时间戳和当前实际接收时间的理论模型
- 其次基于理论模型建立状态转移方程以及观测方程
- 再次介绍
TimestampExtrapolator::ExtrapolateLocalTime
函数期望接收时间的计算。 - 最后结合
TimestampExtrapolator::Update
函数分析状态变量的计算原理和Kalman Ganin
的更新。
4.1)TimestampExtrapolator模块Kalman模型
-
为后续的方便分析首先介绍卡尔曼滤波的5大核心公式,本文不做理论推导
根据卡尔曼滤波的5大核心公式,首先需要建立状态转移方程和观测方程
为建立状态转移方程和观测方程,需要先了解一下rtp time stamp和实际时间的相应关系
// Local time in webrtc time base.
int64_t current_time_us = clock_->TimeInMicroseconds();
int64_t current_time_ms = current_time_us / rtc::kNumMicrosecsPerMillisec;
// Capture time may come from clock with an offset and drift from clock_.
int64_t capture_ntp_time_ms = current_time_ms + delta_ntp_internal_ms_;
// Convert NTP time, in ms, to RTP timestamp.
const int kMsToRtpTimestamp = 90;
uint32_t timestamp_rtp =
kMsToRtpTimestamp * static_cast(capture_ntp_time_ms);
- webrtc 在发送每一帧视频数据前通过上述代码来设置每帧的rtp时间戳,以90KHZ为采样率,也就是每秒钟被划分成了90000个时间块,假设是60fps每秒的帧率,那么每帧的rtp时间戳理论上是相隔90000 / 60 = 1500个时间块,也就是每帧数据之间RTP时间戳的增量为1500,如果将该时间增量换算成ms数如下:
fps = 60fps
samplerate = 90000
timestampDiff(k) = rtpTimeStamp(k) - rtpTimeStamp(0) (4.1.1)
timestampDiffToMs(k) = timestampDiff(k) * 1000 / samplerate (4.1.2)
timestampDiff(k)
表示第(k)帧视频和第一帧视频的rtpTimeStamp之差timestampDiffToMs(k)
表示第(k)帧视频和第一帧视频之间所经历的毫秒数-
基于此,建立如下到达传输模型
以第一帧(t0,T0)作为基准,来估计接收到第k帧的期望接收时间
假设上述模型没有任何误差和干扰,那么在已知
_startMs
的情况下,上述的传输曲线应该都是蓝色实现所示,并很容易就能得到如下计算
t(0) = _startMs
t(k) = timestampDiffToMs(k) + t(0) (4.1.3)
- 但事实上不上这样,传输过程有很多的不确定性诸如网络延迟抖动等,且由于采集帧率可能也具有误差,也就是
sample_rate
可能大于90KHZ,最终每帧数据的到达模型可能就变成了上述的红色虚线部分所示,这样timestampDiffToMs(k)
就会比第(k)帧达到接收端实际所经历的时间要大或者要小,从而使得t(k)第(k)帧的接收时间变得不准,这样较为准确的t(k)应该用如下公式来描述
t(k) = timestampDiffToMs(k) + t(0) + error(k) (4.1.4)
- 其中error(k)表示传输过程中采集噪声和网络噪声以及其他噪声的总集合,如果将网络延迟导致的误差和因采集噪声所导致的延迟误差提取出来,就可以将上述公司进行如下变换
t(k) = (timestampDiff(k) - jitterTimestamp(k)) / sampleratePermillage(k) + t(0) (4.1.5)
- 其中
jitterTimestamp(k)
就是因网络波动导致的第(k)帧和第1帧数据之间的rtpTimeStamp时延抖动 -
sampleratePermillage(k)
表示第k帧的千分之采样率 - 基于此我们可以建立如下状态转移方程,并使用卡尔曼滤波,通过迭代和更新使得
jitterTimestamp(k)
和sampleratePermillage(k)
的误差尽可能的小从而使得第(k)帧的期望接收时间更加的准确
w(k) = w(k-1) + u(k-1) P(u) ~ (0,Q) (4.1.6)
w_bar(k) = [sampleratePermillage(k) jitterTimestamp(k)]^
定义目标二维向量
w_bar(k)
u(k-1)
为过程噪声服从正太分布,由于样本samplerate_permillage(k)
和jitterTimestamp(k)
完全独立,所以其协方差矩阵Q似乎可以取0-
状态转移方程如果用矩阵的表示方式如下:
同时建立如下观测方程
timestampDiff(k) = t_bar(k)^ * w_bar(k) + v(k) P(v) ~ (0,R) (4.1.7)
t_bar(k) = [recvTimeMsDiff(k) 1]^
v(k)
为测量噪声服从正太分布,其协方差矩阵为R,取值为1t_bar(i)
为第(k)帧观测方程系数矩阵recvTimeMsDiff(k)
表示第(k)帧和第一帧的本地接收时间之差-
观测方程如果用矩阵的表示方式如下:
网络残差公式
residual(k) = timestampDiff(k) - t_bar(k)^ * w_hat(k-1) (4.1.8)
- 网络残差体现了噪声的大小,使用第(k)帧的观测值 - 第(k-1)的估计计算值
4.2)TimestampExtrapolator模块计算期望接收时间
int64_t TimestampExtrapolator::ExtrapolateLocalTime(uint32_t timestamp90khz) {
ReadLockScoped rl(*_rwLock);
int64_t localTimeMs = 0;
CheckForWrapArounds(timestamp90khz);
double unwrapped_ts90khz =
static_cast(timestamp90khz) +
_wrapArounds * ((static_cast(1) << 32) - 1);
if (_packetCount == 0) {
localTimeMs = -1;
} else if (_packetCount < _startUpFilterDelayInPackets) {
localTimeMs =
_prevMs +
static_cast(
static_cast(unwrapped_ts90khz - _prevUnwrappedTimestamp) /
90.0 +
0.5);
} else {
if (_w[0] < 1e-3) {
localTimeMs = _startMs;
} else {
double timestampDiff =
unwrapped_ts90khz - static_cast(_firstTimestamp);
localTimeMs = static_cast(static_cast(_startMs) +
(timestampDiff - _w[1]) / _w[0] + 0.5);
}
}
return localTimeMs;
}
- 在3.1节中有介绍到获取当前帧的期望渲染时间 = 期望接收时间 + 实际延迟时间
-
timestampDiff
= (第k帧的rtp时间戳 - 第一帧时间戳 ) -
localTimeMs = (timestampDiff - _w[1]) / _w[0] + (第一帧的实际接收时间)
,使用timestampDiff
(发送端决定的) - 由于网络延迟或波动造成的延迟所对应的rtp时间戳的大小得出第k帧和第一帧最优的时间戳只差 -
_w[0]
=sampleratePermillage(k)
表示第(k)帧的千分之采样率 -
_w[1]
=jitterTimestamp(k)
表示第(k)帧的抖动rtp timestamp 时延
4.3)TimestampExtrapolator模块Kalman预测及校正
- 对于未接收到的未丢过包的视频帧,每帧数据插入到
FrameBuffer
缓存后,通过当前帧的rtp 时间戳以及接收时间来更新TimestampExtrapolator
的卡尔曼滤波器,进行迭代和校正
//参数tMs为当前帧实际接收时间
//参数ts90khz为当前帧的rtp时间戳
void TimestampExtrapolator::Update(int64_t tMs, uint32_t ts90khz) {
_rwLock->AcquireLockExclusive();
//1)第一帧初始赋值
if (tMs - _prevMs > 10e3) {//第一帧或者10秒钟内未收到任何完整的帧则重置
// Ten seconds without a complete frame.
// Reset the extrapolator
_rwLock->ReleaseLockExclusive();
Reset(tMs);
_rwLock->AcquireLockExclusive();
} else {
_prevMs = tMs;
}
//2)根据当前帧的本地接收时间计算detalRecvTimeMs(k)
// Remove offset to prevent badly scaled matrices
// 将当前帧接收时间 - 第一帧的接收时间得当前帧和第一帧的本地接收时间差
// 此处记为detalRecvTimeMs = tMs - _startMs
int64_t recvTimeMsDiff = tMs - _startMs;
CheckForWrapArounds(ts90khz);
int64_t unwrapped_ts90khz =
static_cast(ts90khz) +
_wrapArounds * ((static_cast(1) << 32) - 1);
if (_firstAfterReset) {//重置后赋值初值
// Make an initial guess of the offset,
// should be almost correct since tMs - _startMs
// should about zero at this time.
_w[1] = -_w[0] * tMs;
_firstTimestamp = unwrapped_ts90khz;
_firstAfterReset = false;
}
/*3)使用上一次最优估计计算网络残差为计算验估计做准备,对应5大核心公式的公式(4) 以及4.1.8
用当前帧真实的rtp时间戳 - 第一帧的时间戳 - detalRecvTimeMs * _w[0] - _w[1]
detalRecvTimeMs * _w[0](上一次的最优采样率) 得出detalRtpTimeStamp
*/
double residual = (static_cast(unwrapped_ts90khz) - _firstTimestamp) -
static_cast(recvTimeMsDiff) * _w[0] - _w[1];
if (DelayChangeDetection(residual) &&
_packetCount >= _startUpFilterDelayInPackets) {
// A sudden change of average network delay has been detected.
// Force the filter to adjust its offset parameter by changing
// the offset uncertainty. Don't do this during startup.
_pP[1][1] = _pP11;
}
if (_prevUnwrappedTimestamp >= 0 &&
unwrapped_ts90khz < _prevUnwrappedTimestamp) {
// Drop reordered frames.
_rwLock->ReleaseLockExclusive();
return;
}
// T = [t(k) 1]';
// that = T'*w;
// K = P*T/(lambda + T'*P*T);
// 4)计算卡尔曼增益
double K[2];
// 对应5大公式,公式3中的分子部分
K[0] = _pP[0][0] * recvTimeMsDiff + _pP[0][1];
K[1] = _pP[1][0] * recvTimeMsDiff + _pP[1][1];
// 对应5大公式,公式3中的分母部分
double TPT = _lambda + recvTimeMsDiff * K[0] + K[1];
K[0] /= TPT;
K[1] /= TPT;
//5) 根据最优卡尔曼因子进行校正,计算后验估计值
// w = w + K*(ts(k) - that);
_w[0] = _w[0] + K[0] * residual;
_w[1] = _w[1] + K[1] * residual;
//6)更新误差协方差
// P = 1/lambda*(P - K*T'*P);
double p00 =
1 / _ * (_pP[0][0] - (K[0] * recvTimeMsDiff * _pP[0][0] + K[0] * _pP[1][0]));
double p01 =
1 / _lambda * (_pP[0][1] - (K[0] * recvTimeMsDiff * _pP[0][1] + K[0] * _pP[1][1]));
_pP[1][0] =
1 / _lambda * (_pP[1][0] - (K[1] * recvTimeMsDiff * _pP[0][0] + K[1] * _pP[1][0]));
_pP[1][1] =
1 / _lambda * (_pP[1][1] - (K[1] * recvTimeMsDiff * _pP[0][1] + K[1] * _pP[1][1]));
_pP[0][0] = p00;
_pP[0][1] = p01;
_prevUnwrappedTimestamp = unwrapped_ts90khz;
if (_packetCount < _startUpFilterDelayInPackets) {
_packetCount++;
}
_rwLock->ReleaseLockExclusive();
}
- 以上首先是根据图
WebRtc_Video_Stream_Receiver_07_05
中的公式(3)计算卡尔曼增量因子,计算的过程中先求其分子部分,然后再求分母部分 - 以上观测噪声的协方差R等于lambda,默认为1。
- 到此就能得出一个期望的接收时间,根据期望接收时间就能得出一个期望渲染时间从而得到期望渲染时间
expectRenderTime = expectRecvTime + actual_delay
- 接下来开始分析
actual_delay
是如何求取的? -
actual_delay
指的是前面已解码的帧的实际的延迟时间
5)计算期望渲染时间
-
首先再回顾
FrameBuffer
模块取帧和将帧发送到解码队列的流程如下图
解码任务队列,每次轮询通过
FrameBuffer::FindNextFrame()
找到一帧完整的帧,并为其设置期望最优渲染时间,然后再通过MaxWaitingTime
函数返回一个延迟时间,告知重复任务队列,让该任务等待wait_ms
之后开始执行,将该帧数据发送到解码任务队列进行解码,解码完毕后又重新轮询新的帧其中在计算渲染时间的时候有用到上一帧数据的平滑
actual_delay
,也就是current_delay_ms_
,基于此开始分析current_delay_ms_
的计算过程
void VCMTiming::SetJitterDelay(int jitter_delay_ms) {
rtc::CritScope cs(&crit_sect_);
if (jitter_delay_ms != jitter_delay_ms_) {
jitter_delay_ms_ = jitter_delay_ms;
// When in initial state, set current delay to minimum delay.
if (current_delay_ms_ == 0) {
current_delay_ms_ = jitter_delay_ms_;
}
}
}
-
jitter_delay_ms
是在每一帧数据送到解码队列之前使用VCMJitterEstimator
模块估计出来的,对于第一帧数据current_delay_ms_
的值就等于该帧的jitter_delay_ms_
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;
}
}
int VCMTiming::TargetDelayInternal() const {
//计算出目标延迟 = jitter_delay_ms_ + 解码耗时 + 渲染延迟
return std::max(min_playout_delay_ms_,
jitter_delay_ms_ + RequiredDecodeTimeMs() + render_delay_ms_);
}
- 以上函数的作用就是在当前帧顺利发送到解码队列之后尽量确保
current_delay_ms_
的值逼近jitter_delay_ms_ + RequiredDecodeTimeMs() + render_delay_ms_
- 然后在后续帧获取期望渲染时间的时候,尽量保证所有帧的延迟间隔趋向于平滑
current_delay_ms_ = 帧间抖动延时 + 解码耗时 + 渲染延迟
- 最后再回顾3.1)节中的VCMTiming模块获取期望渲染时间
expectRenderTime = expectRecvTime + actual_delay
= 期望接收时间 + 帧间抖动延时 + 解码耗时 + 渲染延迟
已知默认情况下渲染延迟一般都是默认值10ms
解码耗时依据硬解的性能而定,一般都会比较均匀
然而期望接收时间和帧间抖动延时都会因为网络的千变万化以及发送端的各种不确定性存在一定的波动
-
其实在调试中发现不管用不用卡尔曼滤波,对于期望接收时间,如果网络不是很差,基本上估计出来的曲线和实际曲线是基本一致的,当然也有可能是我测试的条件比较好
其中蓝色曲线直接试用
timestampDiff / 90 + _startMs
所得,当我设置接收端5%丢包的时候两条曲线完全重合了
6)总结
- 通过本文的分析,首先明确webrtc视频接收过程中的逻辑处理是十分复杂的,同时通过两大卡尔曼滤波对每帧的期望接收和每帧的帧间抖动进行滤波处理
- 其中对帧间抖动的滤波可以使得每帧数据进入解码队列的时机变得相对平滑,这样可以有效的缓解因网络丢包等情况导致视频的卡顿等问题
- 而期望渲染时间的估计使得渲染过程趋向于平滑,同时也明确本文中使用的卡尔曼滤波的作用是为每帧产生一个最优的接收时间,改时间最终决定了该帧的渲染时间,这对音视频的同步是非常有作用的
- 同时通过学习本文深入学习卡尔曼滤波的应用场景