10、码率控制与拥塞控制:基于丢包的码率控制、GCC 算法、REMBB 与 Transport-CC

码率控制和拥塞控制,说白了就是让视频通话在有限的网络带宽下,尽量保持清晰流畅。你想想看,网络就像一根水管,有时候水压大,有时候水压小。我们的任务就是根据水管的状态,动态调整水龙头开多大。

我在做实时通信的早期,曾经遇到过这样一个场景:用户网络从 4G 切换到 WiFi,瞬间带宽翻了好几倍,但视频反而卡住了。为什么?因为码率没跟上,发送端还在用低速模式。后来我才意识到,码率控制不是简单的加减法,而是一套完整的反馈与决策系统。

10.1 基于丢包的码率控制

这是最经典的拥塞控制方法。它的逻辑很简单:如果发现丢包了,就认为网络拥堵了,于是降低发送码率。如果没丢包,就尝试增加码率。

具体来说,WebRTC 中有一个叫 REMB(Receiver Estimated Maximum Bitrate) 的机制。接收端会统计丢包率,然后根据丢包率计算出一个推荐码率,再通过 RTCP 反馈给发送端。

核心公式(简化版):

  • 丢包率 < 2%:网络良好,可以尝试增加码率(比如增加 8%)
  • 丢包率 2% ~ 10%:网络轻度拥塞,保持当前码率
  • 丢包率 > 10%:网络严重拥塞,需要降低码率(比如降低 20%)

我的经验: 纯丢包反馈有一个致命问题——它反应太慢了。丢包发生后,要等到下一个 RTCP 报告周期(通常 1~2 秒)才能感知到。对于快速变化的网络,这就像用后视镜开车。

10.2 GCC 算法:Google 的拥塞控制方案

GCC(Google Congestion Control)是目前 WebRTC 默认使用的拥塞控制算法。它比纯丢包反馈高明得多,因为它同时考虑了 丢包延迟 两个维度。

GCC 的核心思路是:

  • 基于丢包的控制器:负责长期趋势判断,决定码率的上限
  • 基于延迟的控制器:负责短期波动响应,决定码率的实时调整

这两个控制器协同工作,就像汽车的油门和刹车。延迟控制器负责微调,丢包控制器负责大方向。

10.3 延迟梯度与到达时间滤波器

GCC 中有一个非常巧妙的设计——到达时间滤波器。它不直接看延迟大小,而是看延迟的变化趋势。

举个例子:

  • 如果延迟从 20ms 涨到 30ms,再涨到 40ms,说明网络正在变拥堵
  • 如果延迟稳定在 30ms 左右,说明网络状态平稳
  • 如果延迟从 40ms 降到 20ms,说明网络正在释放资源

这个滤波器会计算每个数据包的到达时间差,然后判断网络是否处于 过度使用(overuse)正常(normal)欠使用(underuse) 状态。

我曾经踩过的坑: 延迟梯度算法对时钟同步非常敏感。如果发送端和接收端的时钟不同步,计算出来的延迟梯度全是错的。我遇到过因为 NTP 服务没配置好,导致 GCC 误判网络拥堵,码率被压到极低的情况。后来我们强制要求所有设备使用同一个 NTP 服务器,问题才解决。

10.4 REMBB 与 Transport-CC

REMB 是 WebRTC 早期的码率反馈机制,它通过 RTCP 的 REMB 报文,把接收端估算的码率告诉发送端。但 REMB 有一个问题:它只能反馈一个总体的码率建议,无法精确到每个数据流。

Transport-CC(Transport-wide Congestion Control) 是 REMB 的升级版。它不再依赖接收端计算码率,而是让接收端把每个数据包的到达时间信息反馈给发送端,由发送端自己来计算码率。

这样做的好处是:

  • 发送端拥有全局视角,可以同时控制多个视频流、音频流
  • 反馈更精细,可以精确到每个数据包
  • 对接收端的计算要求更低

简单对比:

特性 REMB Transport-CC
反馈粒度 整体码率 每个数据包
计算位置 接收端 发送端
延迟敏感度 较高 较低
多流支持

10.5 GCC 的整体工作流程

我把 GCC 的流程画了一张图,方便你理解:

GCC 拥塞控制工作流程 发送端 视频编码 + 发送 接收端 解码 + 统计 网络 丢包 / 延迟 / 抖动 RTP 数据包 RTP 数据包 丢包检测 延迟梯度计算 REMB / Transport-CC 码率决策 反馈信息 调整编码参数 丢包路径 延迟路径 反馈路径

这张图展示了 GCC 的完整闭环:

  1. 发送端通过 RTP 发送数据包
  2. 接收端检测丢包和计算延迟梯度
  3. 接收端通过 REMB 或 Transport-CC 反馈给发送端
  4. 发送端根据反馈信息做出码率决策
  5. 调整后的码率再次进入下一轮循环

10.6 实际调优建议

我在多个项目中调过 GCC 参数,这里分享几个实用经验:

1. 初始码率不要设太高

很多新手喜欢把初始码率设得很高,觉得这样画面清晰。但 GCC 的机制是:如果初始码率太高,网络瞬间拥堵,丢包率飙升,码率会被快速压下来,反而导致画面抖动。我一般建议初始码率设为目标码率的 60%~70%。

2. 延迟梯度阈值要因地制宜

GCC 默认的延迟梯度阈值是针对有线网络优化的。在移动网络下,延迟波动本来就大,用默认阈值会导致频繁误判。我曾经在 4G 网络下测试,把阈值从 10ms 调整到 25ms,卡顿率降低了 40%。

3. 注意 Transport-CC 的反馈频率

Transport-CC 虽然精细,但反馈报文太多也会占用带宽。我见过一个项目,反馈报文占了总带宽的 5%,这在低带宽场景下是不可接受的。建议根据实际带宽动态调整反馈频率,比如带宽低于 500kbps 时,反馈间隔从 20ms 改为 50ms。

10.7 总结

码率控制与拥塞控制是实时通信的基石。从简单的丢包反馈,到 GCC 的延迟梯度 + 丢包双维度控制,再到 Transport-CC 的精细化反馈,每一步都是对网络不确定性的对抗。

我个人习惯在项目中同时保留 REMB 和 Transport-CC 两种方案,用 Transport-CC 作为主力,REMB 作为降级方案。这样即使 Transport-CC 在某些老旧设备上不支持,也能保证基本功能可用。

嗯,这一章的内容就到这里。记住一句话:拥塞控制不是让网络变好,而是让应用适应网络。理解了这一点,你就掌握了实时通信的精髓。

公众号:蓝海资料掘金营,微信 deep3321