技术解码 | WebRTC音视频延时、同步分析以及超低延时优化

2021-10-12 11:10:38 浏览数 (1)

导语 | 在实时音视频中,我们关注的最主要的指标是低延时、高质量和高流畅,那么这篇文章就从延时和流畅方面来介绍一下WebRTC框架中的低延时、流畅以及对于它们的优化。

- 延时 -

由于音频和视频包大小的不同,在WebRTC中,音频和视频的jitterbuffer也就有各自的实现。其中音频延时为playout_delay_ms和jitter_delay(NetEq接收缓存延时)。视频延时则包含jitter_delay(jitterbuffer接收缓存延时),decode_delay和render_delay,其中decode_delay和render_delay比较稳定,对整体延时计算影响较小。不论是音频还是视频,对延时影响较大都是网络延时或者网络抖动。

1.1音频延时

playout_delay_ms主要为播放设备延时,相对来说比较稳定,jitter delay即网络延时,音频网络延时在NetEq中计算得出,结合WebRTC源码分析一下音频延时计算。

ChannelReceive::GetDelayEstimate()->获取到音频delay(即Info结构的current_ delay_ms),该delay为playout_delay_ms_ acm_receiver_.FilteredCurrentDelayMs()。

playout_delay_ms_为ChannelReceive::UpatePlayoutTimestamp获取硬件播放延时毫秒数。

后者为NetEq中得出。NetEqImpl::FilteredCurrentDelayMs()具体计算为:

首先SyncBuffer::FutureLength()获取AudioVector(即播放缓冲区)中包的个数,其次在接收rtp包后,调用DelayManager::Update更新统计信息,BufferLevelFilter::Update更新抖动延时,DecisionLogic:: GetFilteredBufferLevel()音频间隔(具体算法比较复杂,本文不做详细介绍),两者计算得出delay_samples数,结合采样率计算出delay毫秒数。

1.2视频延时

decode_delay和render_delay比较简单,不做介绍,主要介绍下WebRTC中jitter delay。jitter delay主要由网络噪声和视频长帧造成的网络冲击造成的延时。

在WebRTC中,我们认为网络传输为一个线性系统(在WebRTC中像带宽估计、网络延时、帧延时等都作为线性系统来看,而且噪声都符合高斯分布)。在线性系统中我们很容易想到采用卡尔曼滤波来预测下一时刻的状态。

WebRTC中就是采用卡尔曼滤波来估计网络传输速率和网络排队延时。其计算过程主要依赖于当前帧大小、时间戳和当前本地时刻,以及接收过程中不断更新的最大帧大小、平均帧大小、噪声均值、传输速率、网络排队延时等状态参数。下面从WebRTC代码层面分析一下jitter delay的计算。

主要状态公式及值计算如下:

1.  帧间相对延时frame_delay=t(i)-t(i-1)-(T(i)-T(i-1)),其中t(i/i-1)分别为当前帧和上一帧的接收时间,T(i/i-1)分别为当前帧和上一帧的发送时间;

2.  帧间帧大小差delta_frame_size=frame_size-prev_frame_size;

3.  平均帧大小avg_frame_size=phi*avg_frame_size (1-phi)*frame_size采用滑动平均算法计算(phi=0.997);

4. 帧方差var_frame_size=phi*var_frame_size (1-phi)*(frame_size-avg_frame_s ize)^2 (这个的主要用处就是过滤过大帧、关键帧);

5.  观测噪声residual=frame_delay-(_theta[0]*delta_fs_bytes _theta[1]) 观测噪声即表示延时测量值与卡尔曼滤波估计值的偏差;

6.  平均噪声avg_noise=alpha*avg_noise (1-alpha)*residual;

7.  噪声方差var_noise= alpha*var_noise (1-alpha)*(residual-avg_noise)^2(初始值为4);

8.  概率系数alpha=pow(399/400,30/fps) 该系数受帧率影响,帧率fps小时alpha变小,噪声均值和方差变大;

由上述状态公式,计算出相应的状态变量后,通过卡尔曼滤波估计(修正)下一次延时值jitter delay。

在WebRTC源代码中,主要流程如下:

代码语言:javascript复制
视频render线程轮询是否有帧播放,获取下一帧数据->FrameBuffer::GetNextFrame()->VCMInterFrameDelay::CalculateDelay()->  该行获取帧延时(frameDelay=(r(i)-r(i-1))-(s[i]-s[i-1])),即两帧的接收时间差与发送时间差的间差,发送时间差通过rtp时间戳和采样率计算得出。VCMJitterEstimator::UpdateEstimate()->  根据上行得到的帧延时和帧长差进行卡尔曼滤波计算出传输速率_theta[0]和队列延时theta[1]。VCMJitterEstimator::DeviationFromExpectedDelay()-> 计算观察误差residualVCMJitterEstimator::EstimateRandomJitter()-> 根据观测噪声residual计算观测噪声方差和观测噪声均值。VCMJitterEstimator::KalmanEstimateChannel()-> 卡尔曼滤波更新状态变量VCMJitterEstimator::GetJitterEstimate()->VCMJitterEstimator::CalculateEstimate()

计算出

jitterMS=_theta[0]*(_maxFrameSize-_avgFrameSize) NoiseThreshold() JITTER

其中JITTER 为固定值10ms,_theta[0]为传输速率的倒数,maxFrameSize为最大帧长度(这里可以看出,我们的帧应当尽可能的平稳,否则容易造成jitter delay很不稳定的情况),avgFrameSize为平均帧长度(这里会忽略大帧、关键帧的影响,防止关键帧过分拉大均值导致jitter delay突然变较小)。

NoiseThreshold()为传输噪声,计算如下:

_noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset

其中noiseStdDevs为噪声系数,固定值2.33,表示高斯分布99%的概率分布,varNoise为噪声方差,noiseStdDevOffset为噪音扣除值,固定为30ms。

下面1.3节重点讲一下_theta[2]的计算(即状态变量更新)。

1.3卡尔曼滤波

1.3.1卡尔曼滤波公式

卡尔曼滤波公式:参考(卡尔曼滤波五个公式https://blog.csdn.net/wccsu1994/article/details/84643221)

1.3.2 jitter滤波算法

由于视频传输延时抖动主要与帧大小、传输带宽、网络排队相关,即我们需要观测的值,因此建立数学模型如:

状态方程(即某一时刻的网络状态):theta(i) = theta(i-1) u(i-1)

其中:theta(i)为一个二维矩阵[1/C(1) m(i)]^T,C(i)为传输速率,m(i)为网络排队延时。u(i)为过程噪声(误差),是一个高斯噪声矩阵——P(u)~(0,Q)

观测方程(即某一时刻观测到的jitter delay):d(i) = dL(i)/C(i) m(i) v(i)

即d(i) = H * theta(i) v(i)

其中:d(i)为帧间测量延时。H为长度差测量矩阵[dL(i) 1],dL(i)为当前帧与上一帧的帧长差值。v(i)表示测量噪声(误差)——P(v)~(0,R)

因此也即观测方程为:

d(i) = [dL(i) 1] * [1/C(i) m(i)]^T v(i)

由上面的模型结合卡尔曼滤波五个公式,对应到WebRTC的定义公式为(WebRTC相关文档连接为https://datatracker.ietf.org/doc/html/draft-alvestrand-rmcat-congestion-03):

先验期望:theta_hat-(i)=theta_hat(i-1)

先验方差:theta_cov-(i)=theta_cov-(i-1) Q(i)

卡尔曼增益:K=theta_cov-(i)*H^T /H*theta_cov-(i)*H^T R

后验期望:theta_hat(i)=theta-(i) K*(d(i)-H*theta_hat-(i))

后验方差:theta_cov(i)=(I-K*H)*theta_cov-(i)

1.3.3 卡尔曼滤波代码分析

定义:

M: _thetaCov[2][2]为theta_cov(i)即[1/C(1) m(i)]^T协方差矩阵;

Q: _Qcov[2][2]为高斯噪声矩阵(一个对角矩阵),建议值为_Qcov[0][0] = 2.5e-10; _Qcov[1][1] = 1e-10; _Qcov[0][1] = _Qcov[1][0] = 0;(WebRTC中有不少这样的建议值)

h: [deltaFSBytes 1]即为[dL(i) 1]测量矩阵;

Mh: M*h^T;

hMh_sigma: h*M*h^T R;

K: M*h^T/hMh_sigma。

源码实现:

代码语言:javascript复制
void VCMJitterEstimator::KalmanEstimateChannel(int64_t frameDelayMS,                                               int32_t deltaFSBytes) {  double Mh[2]; //H*theta_cov-(i)  double hMh_sigma; //H*theta_cov-(i)*H^T R  double kalmanGain[2]; //卡尔曼增益K  double measureRes;  //测量误差 d(i)-H*theta_hat-(i)  double t00, t01;
  // Kalman filtering
  // Prediction  // M = M   Q  //先验协方差计算theta_cov-(i)=theta_cov-(i-1) Q(i)  _thetaCov[0][0]  = _Qcov[0][0];  _thetaCov[0][1]  = _Qcov[0][1];  _thetaCov[1][0]  = _Qcov[1][0];  _thetaCov[1][1]  = _Qcov[1][1];
  //Mh[2] = M[2][2]*h^T = _thetaCov[2][2]*[dL(i) 1]  Mh[0] = _thetaCov[0][0] * deltaFSBytes   _thetaCov[0][1];//表示1/C(1)协方差  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;  }  //观测噪声协方差R计算为使用指数滤波方式,deltaFSBytes越大噪声越小  double sigma = (300.0 * exp(-fabs(static_cast<double>(deltaFSBytes)) /                              (1e0 * _maxFrameSize))                    1) *                 sqrt(_varNoise);  if (sigma < 1.0) {    sigma = 1.0;  }
  // hMh_sigma = h*M*h'   R = [dL(i) 1]*Mh[2]   sigma  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;  }  //卡尔曼增益K[2]=Mh[2]/hMh_sigma,噪声越大,则hMh_sigma越大,则卡尔曼增益越小,则目标值更接近上一次的  kalmanGain[0] = Mh[0] / hMh_sigma;  kalmanGain[1] = Mh[1] / hMh_sigma;
  //修正目标值,即测量值与评估值校验出最终目标结果  //theta = theta   K*(dT - h*theta)  //      = theta   K*dT - K*h*theta  //      = (1 – K*h) * theta   k*dT  //由此可见,当K接近0时,当前值更接近上一次后验期望  //计算测量误差 measureRes = d(i)-H*theta_hat-(i)  measureRes = frameDelayMS - (deltaFSBytes * _theta[0]   _theta[1]);  _theta[0]  = kalmanGain[0] * measureRes;  _theta[1]  = kalmanGain[1] * measureRes;
  if (_theta[0] < _thetaLow) {    _theta[0] = _thetaLow;  }
  // M = (I - K*h)*M  //更新(修正)theta[2][2]协方差矩阵,I[2][2]-[K0 K1]^T*[deltaFSBytes 1],其中I为2x2的单位矩阵  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);}

- 音视频同步 -

RtpStreamsSynchronizer::UpdateDelay()每秒更新一次同步信息,

设置同步信息:

BaseChannel::AddRecvStream_w->

WebRtcVideoChannel::AddRecvStream->

Call::CreateVideoReceiveStream->

Call::ConfigureSync->

VideoReceiveStream2::SetSync->

RtpStreamsSynchronizer::ConfigureSync

ConfigureSync中配置同步信息,如果没有音频,则不做同步,且不更新同步引入的delay 的状态(audio和video自身的jitter delay还是不受影响的)。

在音视频同步中,使用到一些关键参数,其中记录音视频包的发送和接收时间的Info结构为:

代码语言:javascript复制
struct Info {    int64_t latest_receive_time_ms; //最新接收到rtp包的本地时间    uint32_t latest_received_capture_timestam; //最新接收到的rtp包的rtp时间戳    uint32_t capture_time_ntp_secs; //sr的ntp秒    uint32_t capture_time_ntp_frac; //sr的ntp毫秒    uint32_t capture_time_source_clock; //sr发送的最后rtp时间戳    int current_delay_ms; //  };

同步主要依赖SenderReport,依据SenderReport中的rtp timestamp和ntp time以及本地接收时间latest_receive_time_ms作为参考。

下面介绍下音视频同步的关键步骤,如:

第一步:通过GetInfo获取音频发送端和接收端信息audio_info,流程如下:

ChannelReceive::GetSyncInfo()->

ModuleRtpRtcpImpl2::RemoteNTP()->

RTCPReceiver::NTP()获取remote的sr的rtp ts、send ntp。

第二步:通过获取到的audio info更新音频测量时间信息,流程如下:

RtpToNtpEstimator::UpdateMeasurements->

RtpToNtpEstimator::UpdateParameters()该流程通过最近20个SR包中ntp和rtp时间戳计算出两者的线性关系并记录,因为不是每一个rtp时间戳都有对于SR的ntp时间戳,所以通过该线性关系,估算出每一个rtp时间戳对应的ntp时间,公式为:y=k*x b。

由此则可以得到最新接收到的rtp包的rtp时间戳和最新接收到rtp包的本地时间以及发送端rtp时间戳和ntp时间的关系,进而计算出传输延时。

第三步:GetInfo获取视频发送端和接收端信息video_info,流程如下:

VideoReceiveStream2::GetInfo()->

RtpVideoStreamReceiver2::GetSyncInfo()->

ModuleRtpRtcpImpl2::RemoteNTP()内容同音频

VCMTiming::TargetVideoDelay()获取到视频delay(即Info结构的current_delay_ms),该delay为

max(min_playout_delay_ms_, jitter_delay_ms_ RequiredDecodeTimeMs() render_delay_ms_)。

其中:

min_playout_delay_ms_为上一次同步计算得到的目标视频延时;

jitter_delay_ms_为视频的传输延时时间(1.2中已经介绍);

render_delay_ms_为固定10ms;

RequiredDecodeTimeMs()为解码时间。

第四步:通过获取到的video info更新视频测量时间信息,都是计算rtp时间戳与发送端ntp时间,计算流程跟音频没区别。

第五步:在得到音视频的发送时间以及接收时间后,通过ComputeRelativeDelay计算出视频相对与音频的延时relative_delay_ms,该时间可为大于0也可小于或等于0,即表示视频相对于音频是延时大还是延时小。

  • relative_delay_ms>0时,是视频慢,视频延时与基准(base_target_delay_ms,默认0)比较:video_delay_.extra_ms > base_target_delay_ms_时,减小视频延时,设置音频位基准延时,否则增大音频延时,设置视频为基准延时。
  • relative_delay_ms<0时,是音频慢,音频延时与基准比较:audio_delay_. extra_ms > base_target_delay_ms_时,减小音频延时,设置视频为基准,否则,增加视频延时,设置音频为基准延时。

具体在第六步中执行。

第六步:通过StreamSynchronization::ComputeDelays计算出音频和视频的目标延时,其中如果音视频的相对延时小于30ms,则忽略不做同步,音频和视频按照自身的延时去进行播放就可以了。另外有个相对延时收敛速度,当相对延时太大时,以最大80ms的速度快进或减速,避免变化太大频繁出现相对延时过大的问题。当视频延时相对于音频更大时,就减小视频延时,同时对音频增加一个延时,即需要让音频播放的慢一点,相反亦然。这样做是为了在保证流畅的时候,通过收敛,能达到降低延时的目的。

第七步:把第六步计算出来的音频和视频目标延时分别设置到各自的播放线程,各自按照设定的延时去播放就可以了。

- 延时优化 -

通过前面的音视频延时分析以及音视频同步实现我们可以了解到,在一定的网络条件以及音视频码率的情况下,想要实现更低的延时,可以从音视频同步以及音视频延时算法上下功夫。

3.1取消音/视频SenderReport

可以看到,音视频同步会受音频或视频任何一方的网络抖动带来的延时进而引起整体的延时加大,所以,在实际场景中,如果对延时的要求大于音视频同步的场景需求的情况下,我们可以禁用音视频同步,各种按照自己的jitter delay去渲染(当然这也不一定会减小延时,只是去消除彼此的影响)。

3.2降低网络抖动

通过计算延时的算法可以看出,我们可以降低网络抖动和传输延时来达到降低延时的目的。首先,在一定带宽情况下,视频帧的大小存在波动,I帧相对来说比较大,对于大帧来说,一下发送出去相对于小帧更容易造成网络丢包或者网络排队,对于这种情况,我们通常可以采用平滑发送(即单位时间内分次发送数据包)来减小发生的概率。其次,对于传输延时,减少网络排队与就近接入可以减小网络延时。

3.3平滑发送

平滑发送可以用来解决大帧对网络造成冲击问题,减少网络丢包、乱序、重传以及网络排队对jitterbuffer影响以及拥塞控制(带宽预测)的影响。在webrtc中jitter delay的主要原因是大帧的传输延时和网络排队延时,大帧的传输延时我们可以理解为pacing延时,因为在webrtc源码中,pacing发送是以目标带宽为基础的,假定带宽固定,那么较大帧就会在pacesender里面呆一定时间,造成延时;网络排队这个比较好理解,路上要传输的数据多了就得按顺序一个个的送,而且重传包的排队会影响帧的组成时间,对于网络排队,我们能做的没有太多,尽可能编出相对平稳且合适的码率吧。

那我们从上面的分析可以看到(以有限带宽看),平滑发送和低延时其实是矛盾的,但平滑发送对降低丢包和乱序又是有效的,在我们播放视频去取帧的时候,我们又会依赖帧的到达时间去做平滑播放,这样,我们就需要不断的去平衡平滑发送和到达延时。因此在实现过程中我们提供两种选择供业务选择:

  • 方案一:在帧间隔间保证前一帧完全发送出去,消除发送侧的延时,同时又兼顾到一定的发送平滑效果。
  • 方案二:在设定的最大延时(大于帧间隔时间)内保证之前的数据包全部发送出去,相对第一来说,对于大帧,该方式有更大的发送平滑空间,但引入了发送延时。

下面,我测试了个有较大大帧的h264发送对上面两个pacing方式进行测试,在有限带宽的情况下pacing对于丢包乱序的还是有一定作用的。Pacing发包的平滑效果图形如下:

图1. 禁用pacing

图2. 方案一

图3. 方案二

3.4 渲染延时

这里初步看还是有些优化空间的,具体这次就不讲了。这里有一点疑问,就是获取渲染期望时间与音视频同步时获取期望时间为什么不采用同样的滤波算法,渲染期望采用卡尔曼滤波,而同步采用线性滤波。

- 总结 -

上面介绍了关于WebRTC中延时相关的内容,有很多细节没有做详细的介绍(因为细节确实较多)需要大家去理解,比如,NACK对延时的影响(丢包多jitter delay会变大)、帧率对延时的影响(在计算噪声期望和方差时有影响系数)等。在卡尔曼滤波计算过程中有一些参数也是可以去做优化调整的,如,噪声系数、大帧的影响、均值加权算法(WebRTC中采用了大量的指数加权计算)等。

腾讯云音视频在音视频领域已有超过21年的技术积累,持续支持国内90%的音视频客户实现云上创新,独家具备 RT-ONE™ 全球网络,在此基础上,构建了业界最完整的 PaaS 产品家族,并以 All in One SDK 的创新方式为客户服务。腾讯云音视频为全真互联网时代,提供坚实的数字化助力。

0 人点赞