注意
- 本文基于 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)歷以下幾個步驟:
- 根據(jù) feedback 計算時延梯度
- 時延梯度平滑處理后根據(jù)線性回歸(最小二乘法)計算出網(wǎng)絡(luò)趨勢
- 根據(jù)趨勢「探測」出網(wǎng)絡(luò)狀態(tài)
- 根據(jù)網(wǎng)絡(luò)狀態(tài)計算出目標帶寬
- 綜合 DelayBasedBwe 和 LinkCapacityTracker 計算出的帶寬計算出 TargetTransferRate俱诸。
- 將 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;
}