WebRTC 源碼分析 四 擁塞控制

注意

  • 本文基于 WebRTC M89

帶寬探測

帶寬探測在帶寬大幅下降時被調(diào)用城须,相關(guān)的類有 ProbeController 和 ProbeBitrateEstimator。帶寬探測的原理是以 cluster_id 為單位以一定速率發(fā)送 RTP 包,收到反饋消息后根據(jù)發(fā)送/接收數(shù)據(jù)除以間隔時間計算出發(fā)送端/接收端的「帶寬」,最后取它們倆中的較小的值作為探測到的帶寬豌鹤。

帶寬探測包 feedback 消息處理的方法是 ProbeBitrateEstimator::HandleProbeAndEstimateBitrate。

擁塞控制

WebRTC 中移除了之前用過的 BBR 搂鲫,現(xiàn)在用的是 Google congestion control傍药。移除 BBR 的的原因個人理解是 BBR 在與基于丟包的擁塞控制算法競爭中處于劣勢。
Google congestion controller 中有兩種方式實現(xiàn)擁塞控制——基于丟包和基于延遲魂仍,基于丟包的擁塞控制算法默認不開啟拐辽,本文只分析基于延遲的擁塞控制算法既 DelayBasedBwe。
相關(guān)的類有:

  • GoogCcNetworkController
  • DelayBasedBwe
  • TrendlineEstimator
  • AimdRateControl

Google congestion control 從接收到 feedback 到將目標碼率應(yīng)用到 sender 擦酌、encoder 要經(jīng)歷以下幾個步驟:

  1. 根據(jù) feedback 計算時延梯度
  2. 時延梯度平滑處理后根據(jù)線性回歸(最小二乘法)計算出網(wǎng)絡(luò)趨勢
  3. 根據(jù)趨勢「探測」出網(wǎng)絡(luò)狀態(tài)
  4. 根據(jù)網(wǎng)絡(luò)狀態(tài)計算出目標帶寬
  5. 綜合 DelayBasedBwe 和 LinkCapacityTracker 計算出的帶寬計算出 TargetTransferRate俱诸。
  6. 將 TargetTransferRate 應(yīng)用到 sender 和 encoder 。

時延梯度

WebRTC 中計算延時不是按包計算的赊舶,而是通過將包分組睁搭,然后計算包組間的延時。
WebRTC 根據(jù)包發(fā)送時間來分組笼平,在包組中后續(xù)包距第一個包的發(fā)送時間差小于 5ms园骆。如果某個包和包組中首包的發(fā)送時間差大于 5ms,那么這個包就作為下一個包組的第一個包寓调。

brust

WebRTC 發(fā)送端實現(xiàn)了平滑發(fā)送锌唾,所以理論上不存在 brust,但是在 wifi 網(wǎng)絡(luò)下某些 wifi 設(shè)備的轉(zhuǎn)發(fā)模式是夺英,在某個固定時間片內(nèi)才有機會發(fā)送數(shù)據(jù)晌涕。在等待時間片的時候數(shù)據(jù)包堆積,在發(fā)送時形成 brust 痛悯,這個 brust 中所有的數(shù)據(jù)被視為一組余黎。

包組時延梯度

包組時間差是指不同包組網(wǎng)絡(luò)時間的的差值。
eg:
兩組包到達時間差為:

t(i) - t(i-1)

兩組包發(fā)送時間差為:

T(i) - T(i-1)

包組時延變化為:

d(i) = t(i) - t(i-1) - (T(i) - T(i-1))

這個時延變化將會用于 TrendlineEstimator载萌,用來時延增長趨勢惧财,判斷網(wǎng)絡(luò)擁塞狀況。

線性回歸

平滑處理

smoothed_delay_ = smoothing_coef_ * smoothed_delay_ +
                    (1 - smoothing_coef_) * accumulated_delay_;

這里平滑使用的是 moving average扭仁,smoothing_coef_ 默認值是 0.9垮衷。

最小二乘法計算網(wǎng)絡(luò)趨勢

正常情況下網(wǎng)絡(luò)時延:


正常

擁塞情況下網(wǎng)絡(luò)時延:


延遲

第一張圖為正常情況下,此時沒有擁塞斋枢,時延梯度 delta t = 0帘靡。圖二如無網(wǎng)絡(luò)擁塞包應(yīng)該按綠線時間到達,此時發(fā)生了網(wǎng)絡(luò)擁塞瓤帚,到達時間比預(yù)定時間晚描姚,此時 delta t 大于 0涩赢。

時延梯度經(jīng)過平滑計算后,WebRTC 用最小二乘法計算出一個斜率轩勘,此斜率代表了網(wǎng)絡(luò)趨勢筒扒。最小二乘法公式如下:

最小二乘法

代碼如下:

absl::optional<double> LinearFitSlope(
    const std::deque<TrendlineEstimator::PacketTiming>& packets) {
  RTC_DCHECK(packets.size() >= 2);
  // Compute the "center of mass".
  double sum_x = 0;
  double sum_y = 0;
  for (const auto& packet : packets) {
    sum_x += packet.arrival_time_ms;
    sum_y += packet.smoothed_delay_ms;
  }
  double x_avg = sum_x / packets.size();
  double y_avg = sum_y / packets.size();
  // Compute the slope k = \sum (x_i-x_avg)(y_i-y_avg) / \sum (x_i-x_avg)^2
  double numerator = 0;
  double denominator = 0;
  for (const auto& packet : packets) {
    double x = packet.arrival_time_ms;
    double y = packet.smoothed_delay_ms;
    numerator += (x - x_avg) * (y - y_avg);
    denominator += (x - x_avg) * (x - x_avg);
  }
  if (denominator == 0)
    return absl::nullopt;
  return numerator / denominator;
}

探測網(wǎng)絡(luò)狀態(tài)

在計算得到 trendline 值后 WebRTC 通過動態(tài)閾值 threshold_ 進行判斷擁塞程度,trendline 乘以周期包組個數(shù)再乘以一個放大系數(shù)就是 modified_trend绊寻。
threshold_ 是一個動態(tài)閥值花墩,動態(tài)調(diào)整是為了改變它對網(wǎng)絡(luò)狀態(tài)的敏感度。如果閥值是固定的澄步,它可能較大或較小冰蘑。較大的話它對于網(wǎng)絡(luò)變化不敏感,和基于丟包的擁塞控制競爭時可能會被餓死村缸。較小的話可能經(jīng)常檢測出網(wǎng)絡(luò)過載祠肥,頻繁切換網(wǎng)絡(luò)狀態(tài)。

計算出 modified_trend 后梯皿,WebRTC 使用一個有限狀態(tài)機進行狀態(tài)切換仇箱,代碼如下:

void TrendlineEstimator::Detect(double trend, double ts_delta, int64_t now_ms) {
  if (num_of_deltas_ < 2) {
    hypothesis_ = BandwidthUsage::kBwNormal;
    return;
  }
  // 使用放大系數(shù)是因為一般情況下,trend 較小东羹,不便于計算
  const double modified_trend =
      std::min(num_of_deltas_, kMinNumDeltas) * trend * threshold_gain_;
  prev_modified_trend_ = modified_trend;
  BWE_TEST_LOGGING_PLOT(1, "T", now_ms, modified_trend);
  BWE_TEST_LOGGING_PLOT(1, "threshold", now_ms, threshold_);
  if (modified_trend > threshold_) {
    if (time_over_using_ == -1) {
      // Initialize the timer. Assume that we've been
      // over-using half of the time since the previous
      // sample.
      time_over_using_ = ts_delta / 2;
    } else {
      // Increment timer
      time_over_using_ += ts_delta;
    }
    overuse_counter_++;
    if (time_over_using_ > overusing_time_threshold_ && overuse_counter_ > 1) {
      if (trend >= prev_trend_) {
        time_over_using_ = 0;
        overuse_counter_ = 0;
        hypothesis_ = BandwidthUsage::kBwOverusing;
      }
    }
  } else if (modified_trend < -threshold_) {
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwUnderusing;
  } else {
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwNormal;
  }
  prev_trend_ = trend;
  UpdateThreshold(modified_trend, now_ms);
}

目標帶寬計算

目標帶寬計算使用的是 AimdRateControl剂桥,Aimd 的全稱是 Additive Increase Multiplicative Decrease,意思是和式增加属提,積式減少权逗。

AimdRateControl::Update

DataRate AimdRateControl::Update(const RateControlInput* input,
                                 Timestamp at_time) {
  RTC_CHECK(input);

  // 初始化
  if (!bitrate_is_initialized_) {
    const TimeDelta kInitializationTime = TimeDelta::Seconds(5);
    RTC_DCHECK_LE(kBitrateWindowMs, kInitializationTime.ms());
    if (time_first_throughput_estimate_.IsInfinite()) {
      if (input->estimated_throughput)
        time_first_throughput_estimate_ = at_time;
    } else if (at_time - time_first_throughput_estimate_ >
                   kInitializationTime &&
               input->estimated_throughput) {
      current_bitrate_ = *input->estimated_throughput;
      bitrate_is_initialized_ = true;
    }
  }

  ChangeBitrate(*input, at_time);
  return current_bitrate_;
}

AimdRateControl::ChangeBitrate

void AimdRateControl::ChangeBitrate(const RateControlInput& input,
                                    Timestamp at_time) {
  ...
  // 根據(jù)網(wǎng)絡(luò)狀態(tài),切換碼率控制狀態(tài)
  ChangeState(input, at_time);

  // 碼率上限限制到目前的 1.5 倍
  const DataRate troughput_based_limit =
      1.5 * estimated_throughput + DataRate::KilobitsPerSec(10);

  switch (rate_control_state_) {
    case kRcHold:
      // 保持垒拢,直接返回
      break;

    case kRcIncrease:
       // 超出目標上界旬迹,復(fù)位
      if (estimated_throughput > link_capacity_.UpperBound())
        link_capacity_.Reset();

      // 當前碼率小于 limit火惊,且沒有發(fā)生 alr
      if (current_bitrate_ < troughput_based_limit &&
          !(send_side_ && in_alr_ && no_bitrate_increase_in_alr_)) {
        DataRate increased_bitrate = DataRate::MinusInfinity();
        if (link_capacity_.has_estimate()) {
          // 如果目標碼率超過 link_capacity 的 bound 就會復(fù)位
          // 當前碼率接近帶寬上限時求类,謹慎使用加性增加
          DataRate additive_increase =
              AdditiveRateIncrease(at_time, time_last_bitrate_change_);
          increased_bitrate = current_bitrate_ + additive_increase;
        } else {
          // 還未估計出 link_capacity,可以使用乘性增加
          DataRate multiplicative_increase = MultiplicativeRateIncrease(
              at_time, time_last_bitrate_change_, current_bitrate_);
          increased_bitrate = current_bitrate_ + multiplicative_increase;
        }
        new_bitrate = std::min(increased_bitrate, troughput_based_limit);
      }

      time_last_bitrate_change_ = at_time;
      break;

    case kRcDecrease: {
      DataRate decreased_bitrate = DataRate::PlusInfinity();


      // 為了避免自己產(chǎn)生 delay,使用 0.85 系數(shù)乘以當前吞吐量
      decreased_bitrate = estimated_throughput * beta_;
      if (decreased_bitrate > current_bitrate_ && !link_capacity_fix_) {
        if (link_capacity_.has_estimate()) {
          decreased_bitrate = beta_ * link_capacity_.estimate();
        }
      }
      if (estimate_bounded_backoff_ && network_estimate_) {
        decreased_bitrate = std::max(
            decreased_bitrate, network_estimate_->link_capacity_lower * beta_);
      }

      // 避免 over-using 狀態(tài)下的增加屹耐,新的碼率使用當前碼率和計算出的 decreased_bitrate 中的較小值
      if (decreased_bitrate < current_bitrate_) {
        new_bitrate = decreased_bitrate;
      }

      if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {
        if (!new_bitrate.has_value()) {
          last_decrease_ = DataRate::Zero();
        } else {
          last_decrease_ = current_bitrate_ - *new_bitrate;
        }
      }
      if (estimated_throughput < link_capacity_.LowerBound()) {
        // The current throughput is far from the estimated link capacity. Clear
        // the estimate to allow an immediate update in OnOveruseDetected.
        link_capacity_.Reset();
      }

      bitrate_is_initialized_ = true;
      link_capacity_.OnOveruseDetected(estimated_throughput);
      // Stay on hold until the pipes are cleared.
      rate_control_state_ = kRcHold;
      time_last_bitrate_change_ = at_time;
      time_last_bitrate_decrease_ = at_time;
      break;
    }
    default:
      assert(false);
  }

  current_bitrate_ = ClampBitrate(new_bitrate.value_or(current_bitrate_));
}

ChangeBitrate 中先根據(jù)網(wǎng)絡(luò)狀態(tài)計算出碼率控制狀態(tài)尸疆,再根據(jù)控制狀態(tài)計算目標碼率

ChangeState 是根據(jù)網(wǎng)絡(luò)狀態(tài)計算碼率控制狀態(tài),碼率控制狀態(tài)有三種:保持惶岭、增加寿弱、減少。
當Overuse發(fā)生時按灶,無論什么狀態(tài)都進入減少症革。
當Underuse發(fā)生時,無論什么狀態(tài)都進入保持狀態(tài)鸯旁。
當Normal發(fā)生時噪矛,在保持階段量蕊,將進入增長。
代碼如下:

AimdRateControl::ChangeState

void AimdRateControl::ChangeState(const RateControlInput& input,
                                  Timestamp at_time) {
  switch (input.bw_state) {
    case BandwidthUsage::kBwNormal:
      if (rate_control_state_ == kRcHold) {
        time_last_bitrate_change_ = at_time;
        rate_control_state_ = kRcIncrease;
      }
      break;
    case BandwidthUsage::kBwOverusing:
      if (rate_control_state_ != kRcDecrease) {
        rate_control_state_ = kRcDecrease;
      }
      break;
    case BandwidthUsage::kBwUnderusing:
      rate_control_state_ = kRcHold;
      break;
    default:
      assert(false);
  }
}

乘性增加

DataRate AimdRateControl::MultiplicativeRateIncrease(
    Timestamp at_time,
    Timestamp last_time,
    DataRate current_bitrate) const {
  double alpha = 1.08;
  if (last_time.IsFinite()) {
    auto time_since_last_update = at_time - last_time;
    // 時間差作為系數(shù)(不大于1.0)艇挨,1.08 作為底數(shù)
    alpha = pow(alpha, std::min(time_since_last_update.seconds<double>(), 1.0));
  }
  // 碼率增加值為 1000 和 current_bitrate * (alpha - 1.0) 中較大值
  DataRate multiplicative_increase =
      std::max(current_bitrate * (alpha - 1.0), DataRate::BitsPerSec(1000));
  return multiplicative_increase;
}

加性增加

DataRate AimdRateControl::AdditiveRateIncrease(Timestamp at_time,
                                               Timestamp last_time) const {
  double time_period_seconds = (at_time - last_time).seconds<double>();
  double data_rate_increase_bps =
      GetNearMaxIncreaseRateBpsPerSecond() * time_period_seconds;
  return DataRate::BitsPerSec(data_rate_increase_bps);
}

TargetTransferRate 計算

struct TargetTransferRate {
  Timestamp at_time = Timestamp::PlusInfinity();
  // The estimate on which the target rate is based on.
  NetworkEstimate network_estimate;
  DataRate target_rate = DataRate::Zero();
  DataRate stable_target_rate = DataRate::Zero();
  double cwnd_reduce_ratio = 0;
};

TargetTransferRate 中有兩個碼率值残炮,target_rate 和 stable_target_rate

更新目標碼率

WebRTC 會綜合 DelayBasedBwe 和 LinkCapacityTracker 計算出 TargetTransferRate,再據(jù)此計算出 target_bitrate 更新到 Sender缩滨、Endoder 模塊势就。

Audio 碼率更新

uint32_t AudioSendStream::OnBitrateUpdated(BitrateAllocationUpdate update) {
  RTC_DCHECK_RUN_ON(worker_queue_);

  // Pick a target bitrate between the constraints. Overrules the allocator if
  // it 1) allocated a bitrate of zero to disable the stream or 2) allocated a
  // higher than max to allow for e.g. extra FEC.
  auto constraints = GetMinMaxBitrateConstraints();
  update.target_bitrate.Clamp(constraints.min, constraints.max);
  update.stable_target_bitrate.Clamp(constraints.min, constraints.max);

  channel_send_->OnBitrateAllocation(update);

  // The amount of audio protection is not exposed by the encoder, hence
  // always returning 0.
  return 0;
}

void ChannelSend::OnBitrateAllocation(BitrateAllocationUpdate update) {
  // This method can be called on the worker thread, module process thread
  // or on a TaskQueue via VideoSendStreamImpl::OnEncoderConfigurationChanged.
  // TODO(solenberg): Figure out a good way to check this or enforce calling
  // rules.
  // RTC_DCHECK(worker_thread_checker_.IsCurrent() ||
  //            module_process_thread_checker_.IsCurrent());
  rtc::CritScope lock(&bitrate_crit_section_);

  // 編碼器設(shè)置目標碼率
  CallEncoder([&](AudioEncoder* encoder) {
    encoder->OnReceivedUplinkAllocation(update);
  });
  retransmission_rate_limiter_->SetMaxRate(update.target_bitrate.bps());
  configured_bitrate_bps_ = update.target_bitrate.bps();
}

Video 碼率更新

uint32_t VideoSendStreamImpl::OnBitrateUpdated(BitrateAllocationUpdate update) {
  RTC_DCHECK_RUN_ON(worker_queue_);
  RTC_DCHECK(rtp_video_sender_->IsActive())
      << "VideoSendStream::Start has not been called.";

  // When the BWE algorithm doesn't pass a stable estimate, we'll use the
  // unstable one instead.
  if (update.stable_target_bitrate.IsZero()) {
    update.stable_target_bitrate = update.target_bitrate;
  }
  // sender 更新目標碼率
  rtp_video_sender_->OnBitrateUpdated(update, stats_proxy_->GetSendFrameRate());
  ...
  // 編碼器更新目標碼率
  video_stream_encoder_->OnBitrateUpdated(
      encoder_target_rate, encoder_stable_target_rate, link_allocation,
      rtc::dchecked_cast<uint8_t>(update.packet_loss_ratio * 256),
      update.round_trip_time.ms(), update.cwnd_reduce_ratio);
  stats_proxy_->OnSetEncoderTargetRate(encoder_target_rate_bps_);
  return protection_bitrate_bps;
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市脉漏,隨后出現(xiàn)的幾起案子苞冯,更是在濱河造成了極大的恐慌,老刑警劉巖侧巨,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抱完,死亡現(xiàn)場離奇詭異,居然都是意外死亡刃泡,警方通過查閱死者的電腦和手機巧娱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烘贴,“玉大人禁添,你說我怎么就攤上這事〗白伲” “怎么了老翘?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長锻离。 經(jīng)常有香客問我铺峭,道長,這世上最難降的妖魔是什么汽纠? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任卫键,我火速辦了婚禮,結(jié)果婚禮上虱朵,老公的妹妹穿的比我還像新娘莉炉。我一直安慰自己,他們只是感情好碴犬,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布絮宁。 她就那樣靜靜地躺著,像睡著了一般服协。 火紅的嫁衣襯著肌膚如雪绍昂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音窘游,去河邊找鬼卖陵。 笑死,一個胖子當著我的面吹牛张峰,可吹牛的內(nèi)容都是我干的泪蔫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼喘批,長吁一口氣:“原來是場噩夢啊……” “哼撩荣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起饶深,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤餐曹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后敌厘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體台猴,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年俱两,在試婚紗的時候發(fā)現(xiàn)自己被綠了饱狂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡宪彩,死狀恐怖休讳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尿孔,我是刑警寧澤俊柔,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站活合,受9級特大地震影響雏婶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜白指,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一留晚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侵续,春花似錦倔丈、人聲如沸憨闰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹉动。三九已至轧坎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泽示,已是汗流浹背缸血。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工蜜氨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捎泻。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓飒炎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親笆豁。 傳聞我的和親對象是個殘疾皇子郎汪,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容