前言
- WebRTC GCC-基于丢包的码率估计原理一文发布已有三年之久,随着webrtc 代码的不断更新,拥塞控制部分谷歌也一直在更新。
- 当前基于丢包的码率估计部分已经额外新拓展了两个分支,总共已有三套算法。
- 其中最原始的算法基于静态丢包阈值进行计算,本文分析分析
LossBasedBandwidthEstimation
简称为V1版本,从代码来看主要是对丢包阈值进行了动态化处理。 - 本文首先简单总结基于Base版本基于丢包的码率估计原理。
Base版本基于丢包的码率估计原理总结
-
在分析之前先简单回顾最原始的版本的核心原理
- 在
Gcc-analysis
论文中有定义如上策略。 - 当丢包率小于
%2
的时候码率按照1.05
倍递增,其中代码实现中的递增系数为1.08
,代码实现中基准值是在一秒内的最小码率的基础上进行递增。 - 而当丢包率大于
%10
的时候按照newRate = rate * (1 - 0.5*lossRate)
进行衰减,代码实现中需要考虑每次递减的时间间隔300ms+RTT
- 当丢包率在
%2~10%
之间则维持不变。 - 此类策略在高码率、且低延迟的场景中,存在十分大的缺陷,比如说
20Mbps
的实时码率,假设丢包率达到5%
,并且RTT
超过30Ms
以上,压根无法保证低延迟,类似云游戏场景,对丢包的容忍度十分低。
LossBasedBandwidthEstimation 丢包率和Ack码率更新
- 通过
tcc feedback
报文对LossBasedBandwidthEstimation
模块中的average_loss_(平均丢包率)、average_loss_max_(平均最大丢包率)、以及acknowledged_bitrate_max_(最大应答)码率
进行实时更新。
LossBasedBandwidthEstimation 平均丢包率更新
void LossBasedBandwidthEstimation::UpdateLossStatistics(
const std::vector<PacketResult>& packet_results,
Timestamp at_time) {
// 无反馈直接返回
if (packet_results.empty()) {
RTC_DCHECK_NOTREACHED();
return;
}
int loss_count = 0;
for (const auto& pkt : packet_results) {
loss_count += !pkt.IsReceived() ? 1 : 0;
}
// 计算丢包率(当前丢包个数/当前反馈总个数)
last_loss_ratio_ = static_cast<double>(loss_count) / packet_results.size();
// 计算距离上次tcc反馈所流逝的时间间隔
const TimeDelta time_passed = last_loss_packet_report_.IsFinite()
? at_time - last_loss_packet_report_
: TimeDelta::Seconds(1);
// 更新上次丢包反馈时间为当前tcc反馈时间
last_loss_packet_report_ = at_time;
has_decreased_since_last_loss_report_ = false;
// 对丢包率进行指数平滑,默认平滑窗口为800ms,其中time_passed(两次tcc feedback的间隔越大)
// 则当前的平均丢包率越逼近与当前的丢包率
average_loss_ += ExponentialUpdate(config_.loss_window, time_passed) *
(last_loss_ratio_ - average_loss_);
if (average_loss_ > average_loss_max_) {
average_loss_max_ = average_loss_;
} else {
// 对最大平均丢包率进行指数平滑,同理两次tcc feedback的间隔越大,平均最大丢包率越逼近于当前的平均丢包率
average_loss_max_ +=
ExponentialUpdate(config_.loss_max_window, time_passed) *
(average_loss_ - average_loss_max_);
}
}
- 其中
ExponentialUpdate()
函数的实现如下:
/**
*参数interval:为两次tcc feedback的时间间隔
*参数window:默认800ms
*/
double ExponentialUpdate(TimeDelta window, TimeDelta interval) {
// Use the convention that exponential window length (which is really
// infinite) is the time it takes to dampen to 1/e.
if (window <= TimeDelta::Zero()) {
return 1.0f;
}
return 1.0f - exp(interval / window * -1.0);
}
- 首先回顾已
e^x
次方的函数图像:
- 很明显
ExponentialUpdate
函数为一个指数递减函数,当interval
越大(表示两次tcc feedback
的间隔越大),则该函数的返回值会越大,则平均丢包率越逼近于本次tcc feedback
计算出来的丢包率。
LossBasedBandwidthEstimation Ack码率更新
void LossBasedBandwidthEstimation::UpdateAcknowledgedBitrate(
DataRate acknowledged_bitrate,
Timestamp at_time) {
const TimeDelta time_passed =
acknowledged_bitrate_last_update_.IsFinite()
? at_time - acknowledged_bitrate_last_update_
: TimeDelta::Seconds(1);
acknowledged_bitrate_last_update_ = at_time;
// 更新最大ack码率
if (acknowledged_bitrate > acknowledged_bitrate_max_) {
acknowledged_bitrate_max_ = acknowledged_bitrate;
} else {
// 同理当time_passed越大的时候这个ack码率的最大值会越逼近当前tcc feedback的码率值
acknowledged_bitrate_max_ -=
ExponentialUpdate(config_.acknowledged_rate_max_window, time_passed) *
(acknowledged_bitrate_max_ - acknowledged_bitrate);
}
}
LossBasedBandwidthEstimation 计算基于丢包的码率
- 如果把该模块当初一个小黑盒,那么基于
Ack
码率,调用Update()
函数最终会输出一个lost
丢包率的码率。
void SendSideBandwidthEstimation::UpdateEstimate(Timestamp at_time) {
...
if (LossBasedBandwidthEstimatorV1ReadyForUse()) {
DataRate new_bitrate = loss_based_bandwidth_estimator_v1_.Update(
at_time, min_bitrate_history_.front().second, delay_based_limit_,
last_round_trip_time_);
UpdateTargetBitrate(new_bitrate, at_time);
return;
}
...
}
/**
* min_bitrate:为1秒内最小码率
* wanted_bitrate: 为基于延迟delay_based算出来的码率信息(也是基于twcc+aimd)模块估算出来的
*/
DataRate LossBasedBandwidthEstimation::Update(Timestamp at_time,
DataRate min_bitrate,
DataRate wanted_bitrate,
TimeDelta last_round_trip_time) {
// 这里应该为初始状态,未收到feedback之前
if (loss_based_bitrate_.IsZero()) {
loss_based_bitrate_ = wanted_bitrate;
}
// Only increase if loss has been low for some time.
// 是否增加带宽使用平均最大丢包率和阈值进行比较
const double loss_estimate_for_increase = average_loss_max_;
// Avoid multiple decreases from averaging over one loss spike.
// 降码率的条件取当前丢包率和平均码率的最小值
const double loss_estimate_for_decrease =
std::min(average_loss_, last_loss_ratio_);
// 允许降低码率的条件为首先:上一次feedback未降低码率、其次:两次feedback之间的间隔为当前rtt + 300ms
// 这个300毫秒个人觉得对于高码率的应用场景有点太高了
const bool allow_decrease =
!has_decreased_since_last_loss_report_ &&
(at_time - time_last_decrease_ >=
last_round_trip_time + config_.decrease_interval);
// If packet lost reports are too old, dont increase bitrate.
// 两次twcc feedback的反馈间隔在6秒内,则认为这个lost_report是有效的(6秒内对于高码率场景是不是太久了点?)
const bool loss_report_valid =
at_time - last_loss_packet_report_ < 1.2 * kMaxRtcpFeedbackInterval;
// 1) 平均丢包率的最大值比reset阈值要小,则认为网络可能不拥塞了,这里直接取delay_based的码率
if (loss_report_valid && config_.allow_resets &&
if (loss_report_valid && config_.allow_resets &&
loss_estimate_for_increase < loss_reset_threshold()) {
loss_based_bitrate_ = wanted_bitrate;
} else if (loss_report_valid &&
loss_estimate_for_increase < loss_increase_threshold()) {
// Increase bitrate by RTT-adaptive ratio.
//2)平均丢包率最大值比loss_increase_threshold阈值小则增加码率,以GetIncreaseFactor()作为系数
// 当前1秒内最小码率作为base进行递增,并且递增规则是和RTT相关的,其中这个GetIncreaseFactor(config_, last_round_trip_time)
// 的返回值在[1.02,1.08]之间,当RTT越大,这个因子越逼近1.02,也就是缓慢增加,当RTT越小则越逼近1.08,也就是快速增加
// 而config_.increase_offset为1kbps,是一个补偿
DataRate new_increased_bitrate =
min_bitrate * GetIncreaseFactor(config_, last_round_trip_time) +
config_.increase_offset;
// The bitrate that would make the loss "just high enough".
// 确保递增的码率在预设的范围内new_increased_bitrate_cap = 0.5kbps * (1/average_loss_max_)^2
// 这个丢包率越小,可递增到的码率值会越大,假设0.001的丢包率,那么能增加到的码率值为500Mbps...
const DataRate new_increased_bitrate_cap = BitrateFromLoss(
loss_estimate_for_increase, config_.loss_bandwidth_balance_increase/*0.5kbps*/,
config_.loss_bandwidth_balance_exponent/*0.5*/);
// 所以这里会限制最大能增加的范围,也就是说丢包率越低,会越接近于GetIncreaseFactor计算出来的结果
new_increased_bitrate =
std::min(new_increased_bitrate, new_increased_bitrate_cap);
loss_based_bitrate_ = std::max(new_increased_bitrate, loss_based_bitrate_);
} else if (loss_estimate_for_decrease > loss_decrease_threshold() &&
allow_decrease) {
// The bitrate that would make the loss "just acceptable".
//3)当前最小丢包值比loss_decrease_threshold阈值大则进行带宽递减
// new_decreased_bitrate_floor = 4kbps * (1/loss_estimate_for_decrease)^2
// 假设10%的丢包率,那么最低能降到400Kbps,丢包率越大能降低到的程度就会越大,最终new_decreased_bitrate_floor就会越小
const DataRate new_decreased_bitrate_floor = BitrateFromLoss(
loss_estimate_for_decrease, config_.loss_bandwidth_balance_decrease/*4kbps*/,
config_.loss_bandwidth_balance_exponent/*0.5*/);
// decreased_bitrate()为0.99倍的ack最大码率,这里是取0.99 * ack_max和new_decreased_bitrate_floor的最大值
DataRate new_decreased_bitrate =
std::max(decreased_bitrate(), new_decreased_bitrate_floor);
// 如果新递减后的码率比loss_based_bitrate_要小,设置loss_based_bitrate_为最小值
if (new_decreased_bitrate < loss_based_bitrate_) {
time_last_decrease_ = at_time;
has_decreased_since_last_loss_report_ = true;
loss_based_bitrate_ = new_decreased_bitrate;
}
}
return loss_based_bitrate_;
}
- 原理上事实上和
Base
版本基本一致。 - 当平均丢包的最大值小于
loss_increase_threshold()
的时候进行码率递增。 - 当平均丢包的最大值小于
loss_reset_threshold()
的时候码率维持不变。 - 当平均最小丢包率大于
loss_decrease_threshold()
的时候进行码率递减。
GetIncreaseFactor递增因子计算原理
// Increase slower when RTT is high.
double GetIncreaseFactor(const LossBasedControlConfig& config, TimeDelta rtt) {
// Clamp the RTT
// 如果当前rtt小于200ms,则取rtt为200Ms
if (rtt < config.increase_low_rtt) {
rtt = config.increase_low_rtt;
} else if (rtt > config.increase_high_rtt) {//800ms
// 如果当前rtt大于200ms,则取rtt为800ms
rtt = config.increase_high_rtt;
}
// 这里其实就是限制rtt的范围为[increase_low_rtt, increase_high_rtt]
// 默认实现不成立,假设强制设置不成立,则返回config.min_increase_factor,默认为1.02
auto rtt_range = config.increase_high_rtt.Get() - config.increase_low_rtt;
if (rtt_range <= TimeDelta::Zero()) {
RTC_DCHECK_NOTREACHED(); // Only on misconfiguration.
return config.min_increase_factor;
}
// modify rtt - 200
auto rtt_offset = rtt - config.increase_low_rtt;
// relative_offset限制在[0,1.0]之间
auto relative_offset = std::max(0.0, std::min(rtt_offset / rtt_range, 1.0));
// 1.08 - 1.02 = 0.06
auto factor_range = config.max_increase_factor - config.min_increase_factor;
// 1.02 + 0.06 * (1 - relative_offset) ,其中relative_offset为rtt_offset / rtt_range小于1
return config.min_increase_factor + (1 - relative_offset) * factor_range;
}
-
config.min_increase_factor
默认为1.02
,config.max_increase_factor
默认为1.08
。 - 从上述实现来看,递增的规则为,最小系数为
1.02
,最大为1.08
,当RTT
越小这个增加因子会越逼近于1,08
也就是码率增加得越快。 - 当
RTT
越大则越逼近1.02
,也就是码率增加得相对越缓慢一些。
BitrateFromLoss带宽增加或减少阈值计算原理
DataRate BitrateFromLoss(double loss,
DataRate loss_bandwidth_balance,
double exponent) {
if (exponent <= 0) {
RTC_DCHECK_NOTREACHED();
return DataRate::Infinity();
}
// 这里注意,如果丢包率小于十万分之1,那么返回正无穷,这样每次带宽增加会按照[1.02,1.08]*(一秒内最小码率)递增
if (loss < 1e-5)
return DataRate::Infinity();
return loss_bandwidth_balance * pow(loss, -1.0 / exponent);
}
-
loss
为丢包率。 -
loss_bandwidth_balance
为因丢包导致的带宽损耗,举个例子假设loss
为0.05
,当前带宽为bitrate
,那么重传导致的带宽损耗为bitrate * 0.05
。 - 这里的思想就是每次重传引入的带宽损耗为
loss_bandwidth_balance = bitate * loss
。 - 那么
N
次重传引入的带宽损耗为loss_bandwidth_balance = bitate * loss^N
次方。 - 有了上述的思想,再来反推,已知
loss(丢包率)
、loss_bandwidth_balance(带宽损耗)
、exponent(重传次数的倒数)
来求当前的bitrate(当前带宽信息)
。 - 反推公式就为
bitate = loss_bandwidth_balance * pow(loss, -1.0 / exponent) = loss_bandwidth_balance * (1/loss)^(1/exponent)
。 - 有了如上的推导和思路的理解后再回过头分析,代码增加和降低的逻辑就不难分析了。
LossBasedBandwidthEstimation 动态丢包率阈值的计算原理
double LossFromBitrate(DataRate bitrate,
DataRate loss_bandwidth_balance,
double exponent) {
if (loss_bandwidth_balance >= bitrate)
return 1.0;
return pow(loss_bandwidth_balance / bitrate, exponent);
}
- 已知
bitrate(目标码率)
、loss_bandwidth_balance(带宽损耗码率)
、exponent损耗次数的倒数
,求丢包率 - 上节提到
N
次重传引入的带宽损耗为loss_bandwidth_balance = bitate * loss^N
次方。 - 反过来
loss = (loss_bandwidth_balance / bitate)^(1/N) = std::pow(loss_bandwidth_balance / bitrate, 1/N)
,其中1/N = exponent
。
double LossBasedBandwidthEstimation::loss_reset_threshold() const {
// (0.1 / bitrate)^(1/2),表示的是在目标码率为loss_based_bitrate_,两次损耗带宽为0.1kbps情况下的损耗率(丢包率)
return LossFromBitrate(loss_based_bitrate_,
config_.loss_bandwidth_balance_reset,
config_.loss_bandwidth_balance_exponent);
}
- 假设按照
30fps
,每帧一个包,每个包的大小1000
字节,也就是8000bit
来算,也就是目标码率为8000 * 30 = 240kbps
,这样算出来的丢包率大约为0.02041241452
也就是2%
的丢包率。意思是当平均最大丢包率小于这个值的时候,保持码率不变。 - 而从上述公式来看,当实时码率也就是
loss_based_bitrate_
越大,这个丢包率的阈值越小,对丢包率的容忍度越低,这个看上起是符合预期的
double LossBasedBandwidthEstimation::loss_increase_threshold() const {
//(0.5 / bitrate)^(1/2),表示的是在目标码率为loss_based_bitrate_,两次损耗带宽为0.5kbps情况下的损耗率(丢包率)
return LossFromBitrate(loss_based_bitrate_,
config_.loss_bandwidth_balance_increase,
config_.loss_bandwidth_balance_exponent);
}
- 对于码率增加的动态丢包阈值,也是一样的,当实时码率越大,那么要想增加码率,则期望的丢包率越小越有可能,同样是码率越大,丢包的容忍度越低。
double LossBasedBandwidthEstimation::loss_decrease_threshold() const {
//(4 / bitrate)^(1/2),表示的是在目标码率为loss_based_bitrate_,两次损耗带宽为4kbps情况下的损耗率(丢包率)
return LossFromBitrate(loss_based_bitrate_,
config_.loss_bandwidth_balance_decrease,
config_.loss_bandwidth_balance_exponent);
}
- 而对于递减逻辑来看,实时码率越大,算出来的丢包率阈值也同样是越小,也就是说当码率越高,那丢包率稍微上去就有可能触发带宽递减逻辑,同样是码率越大,丢包容忍度越低。
总结
- 从
LossBasedBandwidthEstimation
模块实现来看,其根本的码率升降和维持逻辑和原始Base
版本是差不多的。 - 不同点在于在丢包率阈值的决策上使用了动态计算来进行处理,同时这个动态丢包率阈值的计算借助了每次重传带宽损失的思想。
- 在目标码率为
bitrate
的情况下,假设丢包率为lost
,那么第一次重传的带宽损失为bitrate * lost
,而第二次重传是基于上次丢失包的情况下进行重传,所以第N
次的重传带宽损失为bitrate * lost^N
。 - 了解重传对带宽损失的思想后再去分析该模块相对就容易理解。