WebRTC系列分享 第五期 | WebRTC QoS方法之Sender Side BWE实现系列解析WebRTC用于提升QoS的方法https://mp.weixin.qq.com/s/tBSYT_KKokeQCd_HWindFA

背景介绍

BWE(Bandwidth Estimation)可能是WebRTC视频引擎中最关键的模块了。BWE模块决定视频通讯中可以发送多大码率视频不会使网络拥塞,防止视频通讯质量下降。

早期的带宽评估算法比较简单,大多是基于丢包来估计,基本的策略是逐步增加发送的数据量,直到检测到丢包为止。为了让发送端获悉网络上的丢包信息,可以使用标准的RTCP的RR来发送周期性的报告。

现代的带宽评估算法则可以在网络链路发生丢包以前就监测到网络拥塞,它可以通过侦测数据包接收的时延来预测未来可能的拥塞。它是基于链路上的路由器都有一定的缓存,在数据包开始被丢弃之前,先发生数据在缓存里堆积的事件,所以时延相比于丢包,对拥塞的反应更加灵敏。

最新版本webrtc使用trendline算法实现网络拥塞预估,早期版本使用KalmanFilter算法。两种算法都是基于接收端的网络延迟进行码率估计。早期的KalmanFilter算法是在接收端根据网络延时,计算出合适的带宽,通过REMB RTCP报文反馈给发送端,让发送端按照该码率发送视频数据。trendline算法对此进行了优化,在发送端根据网络延时,计算合适的带宽。

实现原理

发送端码控模块结构,如上图所示。

一、remb bitrate

仅做向下兼容使用。兼容老版本的KalmanFilter算法。

二、基于丢包拥塞控制

发送端基于丢包率控制发送码率,其基本思想是:丢包率反映网络拥塞状况。如果丢包率很小或者为0,说明网络状况良好,在不超过预设最大码率的情况下,可以增大发送端码率;反之如果丢包率变大,说明网络状况变差,此时应减少发送端码率。在其它情况下,发送端码率保持不变。

webrtc中发送端收到RTCP RR报文并解析得到丢包率后,根据下图公式计算发送端码率:当丢包率大于0.1时,说明网络发生拥塞,此时降低发送端码率;当丢包率小于0.02时,说明网络状况良好,此时增大发送端码率;其他情况下,发送端码率保持不变。

三、基于延时拥塞控制

基于延迟的拥塞控制是通过每组包的到达时间的延迟差(delta delay)的增长趋势来判断网络是否过载,如果过载进行码率下调,如果处于平衡范围维持当前码率,如果是网络承载不饱满进行码率上调。这里有几个关键技术:包组延迟评估(InterArrival)、滤波器趋势判断(TrendlineEstimator)、过载检测(OveruseDetector)和码率调节(AimdRateControl)。

1、组包延时

音频报文发送的时间间隔是按照一帧的打包时长发送的,但是视频却不能按照实际帧率发送,因为一帧视频有可能分别封装在几个RTP报文,若这个视频帧的RTP报文一起发送到网络上,必然会导致网络瞬间拥塞。以25fps为例,若这帧视频的RTP报文,能够在40ms之内发送给接收端,接收端既可以正常工作,也缓冲了网络拥塞的压力。所以计算视频的包延时,不能用单包计算,需要按照一组包(一帧视频)计算。Trendline滤波器需要的三个参数:发送时刻差值(delta_timestamp)、到达时刻差值(delta_arrival)和包组数据大小差值(delta_size)。从上图可以得出: 

2、Trendline滤波趋势判断

如果平稳网速下传输数据的延迟时间就是数据大小除以速度,如果这数据块很大,超过恒定网速下延迟上限,这意味着要它要占用其他后续数据块的传输时间,那么如此往复,网络就产生了延迟和拥塞。Trendline filter通过到达时间差、发送时间差和数据大小来得到一个趋势增长值,如果这个值越大说明网络延迟越来越严重,如果这个值越小,说明延迟逐步下降。计算公式如下:

先计算单个包组传输增长的延迟,可以记作:

然后做每个包组的叠加延迟,可以记作:

在通过累积延迟计算一个均衡平滑延迟值,alpha=0.9可以记作:

然后统一对累计延迟和均衡平滑延迟再求平均,分别记作:

我们将第i个包组的传输持续时间记作:

趋势斜率分子值为:

趋势斜率分母值为:

最终的趋势值为:

3、过载检测

在计算得到trendline值后WebRTC通过动态阈值gamma_1进行判断拥塞程度,trendline乘以周期包组个数就是m_i,以下是判断拥塞程度的伪代码:

通过以上伪代码就可以判断出当前网络负载状态是否发生了过载,如果发生过载,WebRTC是通过一个有限状态机来进行网络状态迁徙,关于状态机细节可以参看下图:

从上图可以看出,网络状态机的状态迁徙是由于网络过载状态发生了变化,所以状态迁徙作为了aimd带宽调节的触发事件,aimd根据当前所处的网络状态进行带宽调节,其过程是处于Hold状态表示维持当前码率,处于Decr状态表示需要进行码率递减,处于Incr状态需要进行码率递增。那他们是怎么递增和递减的呢?WebRTC引入了aimd算法解决这个问题。

4、AIMD码率调节

aimd的全称是Additive Increase Multiplicative Decrease,意思是:和式增加,积式减少。aimd controller是TCP底层的码率调节概念,但是WebRTC并没有完全照搬TCP的机制,而是设计了套自己的算法,用公式表示为:

如果处于Incr状态,增加码率的方式分为两种:一种是通信会话刚刚开始,相当于TCP慢启动,它会进行一个倍数增加,当前使用的码率乘以系数,系数是1.08;如果是持续在通信状态,其增加的码率值是当前码率在一个RTT时间周期所能传输的数据速率。
如果处于Decrease状态,递减原则是:过去500ms时间窗内的最大acked bitrate乘上系数0.85,acked bitrate通过feedback反馈过来的报文序号查找本地发送列表就可以得到。
aimd根据上面的规则最终计算到的码率就是基于延迟拥塞评估到的bwe bitrate码率。

webrtc具体实现

一、基于延时拥塞控制

BaseChannel::ProcessPacket
->WebRtcVideoChannel::OnRtcpReceived
->Call::DeliverPacket
->Call::DeliverRtcp
->VideoSendStream::DeliverRtcp
->VideoSendStreamImpl::DeliverRtcp
->ModuleRtpRtcpImpl::IncomingRtcpPacket
->RTCPReceiver::IncomingPacket
->RTCPReceiver::TriggerCallbacksFromRtcpPacket
->SendSideCongestionController::OnTransportFeedback

result = delay_based_bwe_->IncomingPacketFeedbackVector更新接收端时延信息到DelayBasedBwe类。
该类使用InterArrival、TrendlineEstimator、OveruseDetector、AimdRateControl等类计算BWE值。详细实现,请参见:

Delay_based_bwe.cc
inter_arrival.cc
trendline_estimator.cc
overuse_detector.cc
Aimd_rate_control.cc

bitrate_controller_->OnDelayBasedBweResult生效rendline计算BWE值,到SendSideBandwidthEstimation。函数调用栈如下:

BitrateControllerImpl::OnDelayBasedBweResult
->SendSideBandwidthEstimation::UpdateDelayBasedEstimate
->SendSideBandwidthEstimation::CapBitrateToThresholds

二、基于丢包拥塞控制

BaseChannel::ProcessPacket
->WebRtcVideoChannel::OnRtcpReceived
->Call::DeliverPacket
->Call::DeliverRtcp
->VideoSendStream::DeliverRtcp     
->VideoSendStreamImpl::DeliverRtcp
->ModuleRtpRtcpImpl::IncomingRtcpPacket
->RTCPReceiver::IncomingPacket
->RTCPReceiver::TriggerCallbacksFromRtcpPacket
->BitrateControllerImpl::RtcpBandwidthObserverImpl::OnReceivedRtcpReceiverReport
->BitrateControllerImpl::OnReceivedRtcpReceiverReport
->SendSideBandwidthEstimation::UpdateReceiverBlock
->SendSideBandwidthEstimation::UpdateEstimate

具体调整函数如下:

low_loss_threshold_ =  kDefaultLowLossThreshold = 0.02f;
high_loss_threshold_ =  kDefaultHighLossThreshold = 0.1f;

void SendSideBandwidthEstimation::UpdateEstimate(int64_t now_ms) {
  uint32_t new_bitrate = current_bitrate_bps_;
  // We trust the REMB and/or delay-based estimate during the first 2 seconds if
  // we haven't had any packet loss reported, to allow startup bitrate probing.
  if (last_fraction_loss_ == 0 && IsInStartPhase(now_ms)) {
    new_bitrate = std::max(bwe_incoming_, new_bitrate);
    new_bitrate = std::max(delay_based_bitrate_bps_, new_bitrate);

    if (new_bitrate != current_bitrate_bps_) {
      min_bitrate_history_.clear();
      min_bitrate_history_.push_back(
          std::make_pair(now_ms, current_bitrate_bps_));
      CapBitrateToThresholds(now_ms, new_bitrate);
      return;
    }
  }
  UpdateMinHistory(now_ms);
  if (last_packet_report_ms_ == -1) {
    // No feedback received.
    CapBitrateToThresholds(now_ms, current_bitrate_bps_);
    return;
  }
  int64_t time_since_packet_report_ms = now_ms - last_packet_report_ms_;
  int64_t time_since_feedback_ms = now_ms - last_feedback_ms_;
  if (time_since_packet_report_ms < 1.2 * kFeedbackIntervalMs) {
    // We only care about loss above a given bitrate threshold.
    float loss = last_fraction_loss_ / 256.0f;
    // We only make decisions based on loss when the bitrate is above a
    // threshold. This is a crude way of handling loss which is uncorrelated
    // to congestion.
    if (current_bitrate_bps_ < bitrate_threshold_bps_ ||
        loss <= low_loss_threshold_) {
      // Loss < 2%: Increase rate by 8% of the min bitrate in the last
      // kBweIncreaseIntervalMs.
      // Note that by remembering the bitrate over the last second one can
      // rampup up one second faster than if only allowed to start ramping
      // at 8% per second rate now. E.g.:
      //   If sending a constant 100kbps it can rampup immediatly to 108kbps
      //   whenever a receiver report is received with lower packet loss.
      //   If instead one would do: current_bitrate_bps_ *= 1.08^(delta time),
      //   it would take over one second since the lower packet loss to achieve
      //   108kbps.
      new_bitrate = static_cast<uint32_t>(
          min_bitrate_history_.front().second * 1.08 + 0.5);

      // Add 1 kbps extra, just to make sure that we do not get stuck
      // (gives a little extra increase at low rates, negligible at higher
      // rates).
      new_bitrate += 1000;
    } else if (current_bitrate_bps_ > bitrate_threshold_bps_) {
      if (loss <= high_loss_threshold_) {
        // Loss between 2% - 10%: Do nothing.
      } else {
        // Loss > 10%: Limit the rate decreases to once a kBweDecreaseIntervalMs
        // + rtt.
        if (!has_decreased_since_last_fraction_loss_ &&
            (now_ms - time_last_decrease_ms_) >=
                (kBweDecreaseIntervalMs + last_round_trip_time_ms_)) {
          time_last_decrease_ms_ = now_ms;

          // Reduce rate:
          //   newRate = rate * (1 - 0.5*lossRate);
          //   where packetLoss = 256*lossRate;
          new_bitrate = static_cast<uint32_t>(
              (current_bitrate_bps_ *
               static_cast<double>(512 - last_fraction_loss_)) /
              512.0);
          has_decreased_since_last_fraction_loss_ = true;
        }
      }
    }
  } else if (time_since_feedback_ms >
                 kFeedbackTimeoutIntervals * kFeedbackIntervalMs &&
             (last_timeout_ms_ == -1 ||
              now_ms - last_timeout_ms_ > kTimeoutIntervalMs)) {
    if (in_timeout_experiment_) {
      LOG(LS_WARNING) << "Feedback timed out (" << time_since_feedback_ms
                      << " ms), reducing bitrate.";
      new_bitrate *= 0.8;
      // Reset accumulators since we've already acted on missing feedback and
      // shouldn't to act again on these old lost packets.
      lost_packets_since_last_loss_update_Q8_ = 0;
      expected_packets_since_last_loss_update_ = 0;
      last_timeout_ms_ = now_ms;
    }
  }

  CapBitrateToThresholds(now_ms, new_bitrate);
}

三、最终确定Send Side BWE值

void SendSideBandwidthEstimation::CapBitrateToThresholds(int64_t now_ms,
                                                         uint32_t bitrate_bps) {
  if (bwe_incoming_ > 0 && bitrate_bps > bwe_incoming_) {
    bitrate_bps = bwe_incoming_;
  }
  if (delay_based_bitrate_bps_ > 0 && bitrate_bps > delay_based_bitrate_bps_) {
    bitrate_bps = delay_based_bitrate_bps_;
  }
  if (bitrate_bps > max_bitrate_configured_) {
    bitrate_bps = max_bitrate_configured_;
  }
  if (bitrate_bps < min_bitrate_configured_) {
    if (last_low_bitrate_log_ms_ == -1 ||
        now_ms - last_low_bitrate_log_ms_ > kLowBitrateLogPeriodMs) {
      LOG(LS_WARNING) << "Estimated available bandwidth " << bitrate_bps / 1000
                      << " kbps is below configured min bitrate "
                      << min_bitrate_configured_ / 1000 << " kbps.";
      last_low_bitrate_log_ms_ = now_ms;
    }
    bitrate_bps = min_bitrate_configured_;
  }

  if (bitrate_bps != current_bitrate_bps_ ||
      last_fraction_loss_ != last_logged_fraction_loss_ ||
      now_ms - last_rtc_event_log_ms_ > kRtcEventLogPeriodMs) {
    event_log_->LogLossBasedBweUpdate(bitrate_bps, last_fraction_loss_,
                                      expected_packets_since_last_loss_update_);
    last_logged_fraction_loss_ = last_fraction_loss_;
    last_rtc_event_log_ms_ = now_ms;
  }
  current_bitrate_bps_ = bitrate_bps;
}

四、更新码率值到pacer、fec、encode模块

void SendSideCongestionController::MaybeTriggerOnNetworkChanged() {
  uint32_t bitrate_bps;
  uint8_t fraction_loss;
  int64_t rtt;
  bool estimate_changed = bitrate_controller_->GetNetworkParameters(
      &bitrate_bps, &fraction_loss, &rtt);
  if (estimate_changed) {
    pacer_->SetEstimatedBitrate(bitrate_bps);
    probe_controller_->SetEstimatedBitrate(bitrate_bps);
    retransmission_rate_limiter_->SetMaxRate(bitrate_bps);
  }

  bitrate_bps = IsNetworkDown() || IsSendQueueFull() ? 0 : bitrate_bps;

  if (HasNetworkParametersToReportChanged(bitrate_bps, fraction_loss, rtt)) {
    int64_t probing_interval_ms;
    {
      rtc::CritScope cs(&bwe_lock_);
      probing_interval_ms = delay_based_bwe_->GetExpectedBwePeriodMs();
    }
    {
      rtc::CritScope cs(&observer_lock_);
      if (observer_) {
        observer_->OnNetworkChanged(bitrate_bps, fraction_loss, rtt,
                                    probing_interval_ms);
      }
    }
  }
}

函数详细调用关系,可以参考《webrtc QOS方法四.1(Sender Side BWE函数实现调用关系图)》

附录

一、概念说明

由于webrtc里面包含GCC、Sendside-BWE两种拥塞控制算法。M55之前用的是GCC,M55之后用的是Sendside-BWE。里面有很多概念梳理如下,以免混淆,不方便理解代码。

 GCCSendside-BWE
码控计算模块接收端发送端
RTP头部扩展AbsSendTimeTransportSequenceNumber
接收端关键对象RemoteBitrateEstimatorAbsSendTimeRemoteEstimatorProxy
网络延时滤波器Kalman FilterTrendline Filter
接收端反馈RTCP报文REMBTransportCC

二、BWE三个典型的算法

Google Congest Control(draft-ietf-rmcat-gcc-02)、

爱立信的SCEAM(GitHub - EricssonResearch/scream: SCReAM - Mobile optimised congestion control algorithm)

MIT的SPROUT(http://aim.nms.lcs.mit.edu/papers/nsdi13-sprout.pdf)。

Mozilla的这篇文章讲述了拥塞控制算法演变的历史(What is RMCAT congestion control, and how will it affect WebRTC? - Advancing WebRTC)

参考

http://www.freehacker.cn/media/tcc-vs-gcc/?utm_source=tuicool&utm_medium=referral

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐