跳到主要内容

WebRTC 全景实战 (10):带宽估计与拥塞控制(GCC)

Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡
Rainy
19 MIN READ... VIEWS

"拥塞控制不是可选项——它是实时通信能否在公网上存活的核心。" — webrtcH4cKS

Ch8 RTCP 介绍了 TWCC、REMB 等反馈机制。本章深入 Google Congestion Control(GCC)——WebRTC 发送端如何根据网络状况动态调整码率。GCC 由 Google 在 2010 年代为 Hangouts / Meet 打磨,Serge Lachapelle 在 Curious 历史访谈 中回忆:Marratech 时代网络质量波动极大,自适应码率是从「能通」到「能用」的分水岭。

非标准但工业界通用

GCC 的 IETF 草案(draft-ietf-rmcat-gcc)从未最终发布为 RFC。BWE 领域存在多种实现(GCC、NADA、SCReAM),但 GCC + TWCC 是 Chrome/WebRTC 生态的事实标准。

配套 Lab:基于 examples/webrtc-lab/client/ch02-p2p-basic 扩展带宽监控脚本;信令服务见 examples/webrtc-lab/signaling/server.js


本篇术语表

术语英文解释
BWEBandwidth Estimation带宽估计——推断当前网络能承载多少发送码率
GCCGoogle Congestion ControlGoogle 提出的发送端拥塞控制算法,Delay + Loss 双模块
TWCCTransport-wide Congestion Control逐包传输层拥塞反馈,RTCP transport-cc
REMBReceiver Estimated Maximum Bitrate接收端直接告知最大码率(Legacy,goog-remb
TMMBRTemporary Maximum Media Stream Bit Rate Request接收端请求降低某 SSRC 码率(Legacy)
Delay-based BWE通过包到达间隔变化检测队列积压
Loss-based BWE通过丢包率检测拥塞
AIMDAdditive Increase Multiplicative Decrease加性增、乘性减——拥塞控制经典策略
Overuse DetectorGCC 中检测网络过载的模块
Trendline Filter趋势线滤波器Delay-based BWE 核心:拟合延迟梯度斜率
Pacing发包整形平滑 RTP 发送间隔,避免 burst 触发丢包
Probing带宽探测周期性试探性增码,发现剩余带宽
Target Bitrate目标码率GCC 输出,编码器实际跟随的发送速率上限
Pacing Rate整形速率通常略高于 Target Bitrate(~1.05x)
Quality Limitation质量限制getStatsqualityLimitationReason 指示降质原因
NACKNegative ACK丢包重传请求,增加发送压力
FlexFECFlexible FEC前向纠错冗余,占用额外带宽预算
rmcatRTP Media Congestion Avoidance TechniquesIETF 拥塞控制工作组

一、GCC 在媒体路径中的位置

GCC 是发送端算法:接收端只负责收集包到达时间并通过 RTCP 反馈;真正的拥塞判断和码率决策在发送端完成。这与 TCP 的接收端窗口不同——WebRTC 选择发送端控制是为了让 SFU 能统一调度多路订阅者的下行带宽。

Marratech 早期在瑞典企业内网用固定码率推流,公网扩展后 Serge Lachapelle 团队发现:没有发送端拥塞控制的 RTC 在真实互联网上不可运维。今天 Meet、Teams、LiveKit 的 SFU Forwarder 都在发送路径上复用同一套 GCC 逻辑。


二、历史脉络:从 REMB 到 TWCC

Serge Lachapelle 在 Google 收购 Marratech 后主导 WebRTC 标准化。Marratech 早期视频会议在瑞典企业网内运行良好,但扩展到公网后,固定码率导致大量「能连上但马赛克」的体验。GCC 的设计目标很明确:

  1. 低延迟:不能像 TCP 那样把包堆在发送缓冲区
  2. 快速响应:200ms 内检测到拥塞并开始降码
  3. 公平性:与其他 TCP/UDP 流共存
  4. SFU 友好:发送端控制,SFU 可为每个订阅者独立 BWE

2.1 GCC 与 rmcat 家族对比

算法控制点反馈WebRTC 采用
GCC发送端TWCC + LossChrome/Firefox/Safari 默认
NADA发送端ECN + 延迟研究/部分实验
SCReAM发送端自包含反馈物联网/低功耗场景
REMB接收端建议RTCP REMBLegacy,已淘汰

WebRTC 选择 GCC 不是因为它是唯一正确的算法,而是因为它在 Meet 规模下被验证过,且与 TWCC 配合最好。


三、GCC 双模块架构

模块检测信号响应策略典型触发场景
Delay-based包到达间隔增大(队列积压)快速乘性降码Wi-Fi 竞争、4G 基站拥塞
Loss-based丢包率超过阈值(~2%)阶梯式降码物理链路丢包、TURN 过载
合并策略取两者较小值保守估计避免拥塞崩溃(congestion collapse)
Probing周期性试探性增码发现剩余带宽网络恢复后快速回升画质

3.1 Delay-based BWE 原理

核心公式直觉:到达间隔 − 发送间隔 = 队列增长速率。持续为正表示网络正在排队,GCC 必须降码;持续为负表示网络有余量,可以 AIMD 增码。

Trendline Filter 对延迟样本做线性回归,斜率持续为正触发 overusing 状态:

3.2 Loss-based BWE 原理

当丢包率超过阈值时,Delay-based 可能尚未触发(例如随机无线丢包),Loss-based 模块作为安全网:

3.3 Probing 带宽探测

网络从拥塞恢复后,GCC 不会立刻跳回最高码率,而是通过 Probing 阶梯试探:

Probing 与 Simulcast 层切换(Ch11)联动:带宽回升时先升 rid=m,再升 rid=h


四、TWCC vs REMB 深度对比

特性TWCCREMB
粒度逐包延迟反馈接收端码率上限
控制点发送端估计接收端建议
SFU 友好✅ SFU 可代发 TWCC❌ 接收端 BWE 在 SFU 场景失效
标准化RFC 8888 相关Legacy,逐步淘汰
SDP 协商a=rtcp-fb:* transport-cca=rtcp-fb:* goog-remb
浏览器支持Chrome/Firefox/Safari 现代版仅旧版兼容
反馈频率每 10–100ms 一批不定期
为什么 SFU 必须用 TWCC?

在 SFU 架构中,接收端 BWE(REMB)看到的是 SFU 到客户端的最后一段链路,无法感知发布者到 SFU 的上行拥塞。TWCC 让发送端(可以是浏览器或 SFU Forwarder)各自做 BWE,Simulcast 层选择(Ch11)才有准确输入。

4.1 SDP 中的 TWCC 协商

a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=rtcp-fb:96 transport-cc
a=rtcp-fb:111 transport-cc

extmap 为 RTP 头扩展分配 transport-wide sequence number;rtcp-fb:transport-cc 声明支持 TWCC 反馈。若协商失败,GCC 退化为仅 Loss-based BWE,画质自适应能力大幅下降。

4.2 验证 TWCC 是否生效

// examples/webrtc-lab/client/ch02-p2p-basic 扩展
async function checkTwccNegotiated(pc) {
const stats = await pc.getStats();
let hasTransportCc = false;
stats.forEach((r) => {
if (r.type === "codec" && r.mimeType?.includes("video")) {
// 间接验证:outbound-rtp 有 targetBitrate 说明 GCC 在运行
}
if (r.type === "transport" && r.selectedCandidatePairChanges !== undefined) {
hasTransportCc = true;
}
});
// 更直接:chrome://webrtc-internals → Graphs → Goog-cc
const sdp = pc.localDescription?.sdp || "";
return sdp.includes("transport-cc");
}

五、码率控制与编码器集成

5.1 动态限制码率

// examples/webrtc-lab/client/ch02-p2p-basic 扩展
const sender = pc.getSenders().find((s) => s.track?.kind === "video");
const params = sender.getParameters();

if (!params.encodings.length) params.encodings = [{}];
params.encodings[0].maxBitrate = 500_000; // 500 kbps 硬上限
params.encodings[0].maxFramerate = 15;
params.encodings[0].scaleResolutionDownBy = 2; // 分辨率降级

await sender.setParameters(params);

Simulcast 场景下每个 encoding 独立设置 maxBitrate(见 Ch9):

const params = sender.getParameters();
params.encodings = [
{ rid: "h", maxBitrate: 1_500_000, active: true },
{ rid: "m", maxBitrate: 500_000, active: true },
{ rid: "l", maxBitrate: 150_000, active: true },
];
await sender.setParameters(params);

GCC 会在 maxBitrate 之下进一步自适应——maxBitrate 是天花板,不是目标值。

5.2 音频与视频码率分配

媒体典型码率GCC 行为
Opus 语音32–64 kbps相对稳定,DTX 静音时接近 0
Opus 立体声64–128 kbps不受视频拥塞大幅影响
VP8/VP9 视频150k–3MGCC 主控对象
屏幕共享500k–5McontentHint: "detail" 倾向保分辨率

5.3 Pacing 与发送平滑

没有 Pacing 时,视频编码器按帧输出 burst(如 30fps 每 33ms 一次大 burst),容易填满路由器队列触发 Delay-based 降码。GCC 的 Pacing Rate 通常略高于 Target Bitrate(~1.05x),平滑发包间隔。

5.4 degradationPreference 与 GCC 协作

const params = sender.getParameters();
params.degradationPreference = "maintain-framerate";
// maintain-framerate: 先降分辨率
// maintain-resolution: 先降帧率
// balanced: 均衡
await sender.setParameters(params);

degradationPreference 决定单流内降质策略;Simulcast 则由 SFU 做层切换,两者不要混用对抗。


六、getStats 中的拥塞信号

6.1 完整监控脚本

// examples/webrtc-lab/client/ch02-p2p-basic 扩展
async function collectCongestionMetrics(pc) {
const stats = await pc.getStats();
const metrics = {
outbound: [],
candidatePair: null,
remoteInbound: [],
transport: null,
};

stats.forEach((r) => {
if (r.type === "outbound-rtp" && r.kind === "video") {
metrics.outbound.push({
rid: r.rid,
ssrc: r.ssrc,
targetBitrate: r.targetBitrate,
bytesSent: r.bytesSent,
framesEncoded: r.framesEncoded,
qualityLimitationReason: r.qualityLimitationReason,
qualityLimitationDurations: r.qualityLimitationDurations,
encoderImplementation: r.encoderImplementation,
retransmittedPacketsSent: r.retransmittedPacketsSent,
});
}
if (r.type === "candidate-pair" && r.state === "succeeded" && r.nominated) {
metrics.candidatePair = {
availableOutgoingBitrate: r.availableOutgoingBitrate,
availableIncomingBitrate: r.availableIncomingBitrate,
currentRoundTripTime: r.currentRoundTripTime,
bytesSent: r.bytesSent,
bytesReceived: r.bytesReceived,
localCandidateType: r.localCandidateType,
remoteCandidateType: r.remoteCandidateType,
};
}
if (r.type === "remote-inbound-rtp") {
metrics.remoteInbound.push({
packetsLost: r.packetsLost,
fractionLost: r.fractionLost,
roundTripTime: r.roundTripTime,
jitter: r.jitter,
});
}
if (r.type === "transport") {
metrics.transport = {
bytesSent: r.bytesSent,
bytesReceived: r.bytesReceived,
dtlsState: r.dtlsState,
};
}
});

return metrics;
}

// 每 2 秒采样,计算码率变化率
let prevBytes = 0;
setInterval(async () => {
const m = await collectCongestionMetrics(pc);
const totalBytes = m.outbound.reduce((s, o) => s + (o.bytesSent || 0), 0);
const bitrate = ((totalBytes - prevBytes) * 8) / 2; // bps over 2s window
prevBytes = totalBytes;
console.table(m.outbound);
console.log("Instant bitrate:", Math.round(bitrate / 1000), "kbps");
console.log("Candidate pair:", m.candidatePair);
}, 2000);

6.2 关键字段解读

字段类型含义告警条件
targetBitrateoutbound-rtpGCC 当前目标码率持续低于 maxBitrate 的 30%
qualityLimitationReasonoutbound-rtpnone / bandwidth / cpu / otherbandwidth 表示 GCC 主动降质
qualityLimitationDurationsoutbound-rtp各原因累计时长bandwidth 占比 > 50%
availableOutgoingBitratecandidate-pair估计可用出站带宽与 targetBitrate 差距 > 50%
currentRoundTripTimecandidate-pair当前 RTT> 300ms 交互延迟明显
fractionLostremote-inbound-rtp最近报告周期丢包率> 0.02 (2%)
retransmittedPacketsSentoutbound-rtpNACK 重传包数持续增长说明丢包严重

6.3 chrome://webrtc-internals 图表对照

图表名对应 GCC 模块用途
Goog-cc Target Bitrate合并输出主监控曲线
Goog-cc Delay-based EstimateDelay-based BWE队列积压诊断
Goog-cc Loss-based EstimateLoss-based BWE丢包拥塞诊断
Outbound RTP Video Bitrate实际发送与 Target 对比
Packets LostLoss-based 输入丢包趋势

七、SFU 场景下的 GCC

在 SFU 架构中,GCC 出现在三个位置:

  1. 发布者 → SFU:浏览器上行 BWE,决定发送哪些 Simulcast 层
  2. SFU → 订阅者:SFU Forwarder 为每个订阅者独立做下行 BWE 和层选择
  3. 订阅者接收:浏览器可选择性做额外适配

LiveKit 的 Forwarder 实现了完整的 TWCC + LayerSelector 管线,详见 LiveKit 介绍Ch12 SFU 架构

7.1 上行 vs 下行 BWE 独立性

常见误区:发布者网络好 ≠ 订阅者能看到高清。SFU 为每个订阅者独立做下行 BWE。


八、应用层与 GCC 的边界

不要手动「猜测」带宽

应用层自行实现 BWE 并与 GCC 对抗是常见反模式。正确做法是设置合理的 maxBitrate 边界,让 GCC 在边界内自适应。SFU 层选择应读取 TWCC 反馈而非自行估算。

应用层该做应用层不该做
设置 maxBitrate / maxFramerate 上限自行计算延迟梯度
选择 Simulcast 层数绕过 GCC 强制固定码率
根据 qualityLimitationReason 提示用户setParameters 高频对抗
屏幕共享设更高 maxBitrate用 DataChannel 灌满带宽
// 正确:根据 GCC 信号做 UI 提示
collector.onMetrics = (m) => {
const video = m.outbound.find((o) => o.kind === "video");
if (video?.qualityLimitationReason === "bandwidth") {
showBanner("网络带宽不足,已自动降低画质");
}
if (video?.qualityLimitationReason === "cpu") {
showBanner("设备性能不足,建议关闭高清或换设备");
}
};

九、常见陷阱

#陷阱现象修复
1未协商 TWCC码率固定,拥塞时大量丢包SDP 添加 transport-cc
2maxBitrate 设太低画质始终模糊检查 setParameters 是否覆盖了默认值
3混淆 targetBitratemaxBitrate误以为已达上限targetBitrate ≤ maxBitrate 是正常的
4TURN relay 未计带宽服务器带宽爆满Ch14 TURN 容量规划
5多 Tab 共享上行互相抢带宽单页面单 PeerConnection
6CPU 降质误判为带宽qualityLimitationReason=cpu降分辨率而非降码率
7忽略 Pacing周期性卡顿浏览器内置,无需手动干预
8NACK 风暴重传包占比 > 20%降码率 + 检查网络丢包
9屏幕共享用语音码率上限文字模糊屏幕共享单独设 2–5Mbps 上限
10SFU 忽略 per-subscriber BWE低带宽端卡顿Forwarder 独立 TWCC

十、实战 Lab

Lab 1:DevTools 带宽限制

# 启动 P2P Demo
cd examples/webrtc-lab/signaling && npm start
# 另开终端
npx serve examples/webrtc-lab/client/ch02-p2p-basic
  1. 打开 Chrome DevTools → Network → Throttling → Add custom profile: 500 kbps
  2. 发起视频通话,运行 collectCongestionMetrics 脚本
  3. 观察 targetBitrate 从 ~1.5Mbps 降至 ~400kbps
  4. 观察 qualityLimitationReason 变为 bandwidth

Lab 2:TWCC 反馈可视化

  1. 打开 chrome://webrtc-internals
  2. 选择活跃的 PeerConnection → Graphs 标签
  3. 勾选 Goog-cc 相关图表(Target Bitrate、Delay-based estimate)
  4. 对比限速前后曲线变化

Lab 3:Simulcast 层与 GCC 联动

  1. 开启 Simulcast 三档(Ch9 代码
  2. 限速 300kbps,观察 getStats 中仅 rid=l 的 outbound-rtp 活跃
  3. 取消限速,观察 rid=h 恢复

Lab 4:丢包注入

# Linux/macOS 需 root
sudo tc qdisc add dev en0 root netem loss 5%
# 通话中观察 fractionLost 与 targetBitrate 下降
sudo tc qdisc del dev en0 root

Lab 5:对比 REMB 时代行为

阅读 webrtcH4cKS — Bandwidth Estimation 中 Fippo 的实验,理解为何 REMB 在 SFU 场景失效。

Lab 6:CPU 降质 vs 带宽降质

  1. 开启 1080p 30fps 通话
  2. 用 Chrome Task Manager 观察 GPU/CPU
  3. qualityLimitationReason=cpu,降低 scaleResolutionDownBy
  4. qualityLimitationReason=bandwidth,检查网络限速

Lab 7:码率变化率计算

  1. 运行 §6.1 脚本的 bitrate 计算
  2. 在限速切换瞬间观察码率下降斜率
  3. 记录从 1.5Mbps 到 400kbps 的收敛时间(通常 2–5 秒)

十一、本章小结

概念要点
GCCDelay + Loss 双模块,取 min 合并
TWCC现代标准,发送端 BWE,SFU 友好
REMBLegacy,接收端建议,逐步淘汰
Probing网络恢复后阶梯试探回升
监控targetBitrate + qualityLimitationReason + availableOutgoingBitrate
实践maxBitrate 边界,让 GCC 自适应,不要对抗

下一篇(Ch11)Simulcast 与 SVC


系列导航

章节主题状态
0架构全景与协议栈地图✅ 已发布
1浏览器媒体 API 与设备管理✅ 已发布
2第一个 P2P 视频通话✅ 已发布
3信令服务器设计与会话状态机✅ 已发布
4SDP 解剖与媒体协商✅ 已发布
5ICE、STUN、TURN 与 NAT 穿透✅ 已发布
6Data Channel 与 SCTP over DTLS✅ 已发布
7DTLS 握手与 SRTP 加密体系✅ 已发布
8RTP/RTCP 媒体传输与 QoS✅ 已发布
9音视频编解码与 Simulcast 入门✅ 已发布
10带宽估计与拥塞控制 GCC✅ 已发布
11Simulcast、SVC 与选择性订阅✅ 已发布
12SFU/MCU/Mesh 架构与 Pion 实战✅ 已发布
13调试工具链与可观测性✅ 已发布
14TURN 集群部署与多区域扩展✅ 已发布
15Capstone 生产级视频会议系统✅ 已发布

References

Logo
RainLib

探索技术、设计与分布式系统的边界。构建面向未来的开发者工具。

留言与建议

© 2026 RainLib. 为未来构建。(Built for the Future)
版权所有。
系统正常