3、发送端码率控制:基于丢包的带宽估计原理与实现

聊到 WebRTC 的发送端码率控制,很多人第一反应就是 GCC(Google Congestion Control)。没错,GCC 确实是核心。但今天我想先聚焦一个最基础、也最容易被忽视的模块——基于丢包的带宽估计。

说白了,这就是 WebRTC 的「保底方案」。当网络开始丢包,发送端必须快速反应,否则视频就会卡成幻灯片。我在项目中遇到过不少次,明明带宽够用,就因为丢包估计策略没调好,画质反而被压得太低。嗯,这里面的门道不少。

3.1 为什么需要丢包反馈?

你想想看,发送端怎么知道网络状况?它又没长眼睛。唯一的信息来源就是接收端的反馈。

WebRTC 提供了两种反馈机制:

  • RTCP RR(Receiver Report):周期性报告,包含丢包率、抖动、往返时延等。但周期太长(默认 1-5 秒),反应慢。
  • RTCP NACK(Negative ACKnowledgment):接收端发现丢包后立即发送,告诉发送端「我少了哪个包」。实时性更好。

我个人习惯把丢包反馈看作「网络疼痛信号」。丢包率越高,说明网络越「疼」,发送端就得降速。但怎么降、降多少,这就是算法的事了。

核心观点:丢包率是带宽估计的「硬约束」。无论其他算法(如基于延迟的估计)给出多高的带宽,一旦丢包率超标,最终码率必须降下来。

3.2 基于丢包的带宽估计模型

WebRTC 的丢包带宽估计,核心是一个公式:

new_bitrate = current_bitrate × (1 - 0.5 × loss_rate)

当 loss_rate 为 0 时,码率不变。当 loss_rate 为 10% 时,码率降为原来的 95%。当 loss_rate 达到 20% 时,码率直接打九折。

等等,这看起来很简单对吧?但实际工程里,有几个关键点:

  • 丢包率计算窗口:不能只看瞬时值。我一般用 1-2 秒的滑动窗口,避免单个丢包导致码率剧烈抖动。
  • 丢包率阈值:不是所有丢包都要降码率。WebRTC 默认阈值为 2%,低于这个值认为网络正常。
  • 恢复策略:丢包率下降后,码率不能立刻恢复,需要「慢启动」式地试探性增加。

我的经验:曾经有个项目,用户反馈视频经常突然变模糊。排查后发现是丢包率阈值设得太低(0.5%),Wi-Fi 的偶尔丢包就触发了降码率。后来调到 2%,问题解决。阈值不是死的,得根据实际网络环境调。

3.3 算法流程与状态机

WebRTC 的丢包控制其实是一个有限状态机。我画了张图,帮你理清逻辑:

基于丢包的带宽估计状态机 正常 丢包率 < 2% 降速 丢包率 ≥ 2% 恢复 丢包率 < 2% 丢包率 ≥ 2% 丢包率 < 2% 码率恢复至目标值 状态说明: 1. 正常状态:码率按需调整,丢包率低于阈值,网络健康。 2. 降速状态:检测到丢包,按公式降低码率,直到丢包消失。 3. 恢复状态:丢包消失后,以较慢速度试探性增加码率。 4. 回到正常:码率恢复至目标值,重新进入正常状态。 注意:恢复速度通常为降速速度的 1/3,避免「震荡」。

3.4 关键参数与调优

WebRTC 的丢包控制有几个可调参数。我整理了一张表,方便你对照:

参数名 默认值 作用 调优建议
loss_rate_threshold 0.02 (2%) 触发降码率的丢包率阈值 Wi-Fi 环境可适当提高至 3-5%
bitrate_reduction_factor 0.5 丢包率在降码率公式中的权重 对丢包敏感的场景可提高至 0.8
recovery_rate 0.1 恢复阶段码率增加的速度因子 保守场景可降至 0.05
window_size_ms 1000 丢包率计算的滑动窗口大小 实时性要求高可缩至 500ms

注意:调参不是越多越好。我曾经在一个项目中把 bitrate_reduction_factor 调到 1.0,结果丢包 5% 时码率直接腰斩,视频质量反而更差。参数之间是联动的,改一个就得重新测整体效果。

3.5 代码实现要点

WebRTC 的丢包控制代码主要在 modules/remote_bitrate_estimator/ 目录下。核心类是 RemoteBitrateEstimatorAbsSendTime。但说实话,直接读源码有点痛苦。我挑几个关键点说说:

// 伪代码:丢包率计算与码率调整
void RemoteBitrateEstimator::UpdateBitrate(uint32_t loss_rate) {
  // 1. 计算滑动窗口内的丢包率
  float smoothed_loss = CalculateSmoothedLoss(loss_rate);

  // 2. 判断是否触发降码率
  if (smoothed_loss > kLossRateThreshold) {
    // 3. 按公式降低码率
    uint32_t new_bitrate = current_bitrate_ * (1 - kReductionFactor * smoothed_loss);
    // 4. 确保码率不低于最小值
    new_bitrate = std::max(new_bitrate, kMinBitrate);
    current_bitrate_ = new_bitrate;
  } else if (in_recovery_mode_) {
    // 5. 恢复模式:缓慢增加码率
    uint32_t increase = static_cast<uint32_t>(current_bitrate_ * kRecoveryRate);
    current_bitrate_ = std::min(current_bitrate_ + increase, target_bitrate_);
  }
}

这段代码看着简单,但实际工程里坑不少:

  • 丢包率平滑:直接用瞬时值会抖动。我习惯用指数加权移动平均(EWMA),权重 α 取 0.3-0.5。
  • 码率下限:不能无限降。WebRTC 默认下限是 30 kbps,但实际场景建议根据分辨率调整,比如 360p 视频至少 200 kbps。
  • 恢复模式判断:丢包率连续低于阈值 3-5 个周期才进入恢复,避免「刚降就升」的震荡。

避坑指南:我曾经遇到一个诡异问题——丢包率明明很高,但码率就是不降。查了半天发现是接收端的 RTCP RR 包被防火墙拦截了。发送端收不到反馈,自然就不会降码率。所以,先确认反馈链路是否通畅,再调算法。

3.6 与其他模块的协作

基于丢包的带宽估计不是孤立的。它和基于延迟的估计(Trendline 滤波器)共同构成 GCC 的「双引擎」。我简单说说协作逻辑:

  • 正常情况:以基于延迟的估计为主,丢包估计作为辅助。
  • 丢包突发:丢包估计接管控制权,快速降码率。
  • 延迟与丢包同时存在:取两者中较低的码率作为最终值。

你想想看,如果只依赖丢包估计,网络延迟升高但没丢包时,码率不会降,视频就会越来越卡。反过来,只依赖延迟估计,遇到突发丢包时反应太慢。两者互补,才是 GCC 的精髓。