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 的丢包控制其实是一个有限状态机。我画了张图,帮你理清逻辑:
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 的精髓。