WebRTC GCC基于丢包动态阈值的带宽估计原理

前言

  • WebRTC GCC-基于丢包的码率估计原理一文发布已有三年之久,随着webrtc 代码的不断更新,拥塞控制部分谷歌也一直在更新。
  • 当前基于丢包的码率估计部分已经额外新拓展了两个分支,总共已有三套算法。
  • 其中最原始的算法基于静态丢包阈值进行计算,本文分析分析LossBasedBandwidthEstimation简称为V1版本,从代码来看主要是对丢包阈值进行了动态化处理。
  • 本文首先简单总结基于Base版本基于丢包的码率估计原理。

Base版本基于丢包的码率估计原理总结

  • 在分析之前先简单回顾最原始的版本的核心原理


    001.png
  • Gcc-analysis论文中有定义如上策略。
  • 当丢包率小于%2的时候码率按照1.05倍递增,其中代码实现中的递增系数为1.08,代码实现中基准值是在一秒内的最小码率的基础上进行递增。
  • 而当丢包率大于%10的时候按照newRate = rate * (1 - 0.5*lossRate)进行衰减,代码实现中需要考虑每次递减的时间间隔300ms+RTT
  • 当丢包率在%2~10%之间则维持不变。
  • 此类策略在高码率、且低延迟的场景中,存在十分大的缺陷,比如说20Mbps的实时码率,假设丢包率达到5%,并且RTT超过30Ms以上,压根无法保证低延迟,类似云游戏场景,对丢包的容忍度十分低。

LossBasedBandwidthEstimation 丢包率和Ack码率更新

002.png
  • 通过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次方的函数图像:
    003.png
  • 很明显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丢包率的码率。
    004.png
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.02config.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为因丢包导致的带宽损耗,举个例子假设loss0.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
  • 了解重传对带宽损失的思想后再去分析该模块相对就容易理解。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容