4. TCC(Transport-wide Congestion Control)详解
TCC 是 WebRTC 里我最喜欢的一个模块。为什么?因为它把拥塞控制从「猜」变成了「算」。早期 GCC 依赖接收端报告(RTCP RR)来估算带宽,延迟大、精度低。TCC 直接在传输层做文章,让发送端能实时感知每个数据包在网络里的命运。
说白了,TCC 就是给每个数据包发了一张「追踪单」。网络状况好不好,看一眼反馈就知道。
4.1 TCC 的工作原理
TCC 的核心思路很简单:发送端给每个 RTP 包打上一个传输序列号(Transport Sequence Number),接收端收到后,把这些序列号连同到达时间打包成 TransportFeedback 消息发回给发送端。发送端根据这些信息,计算出每个包的延迟和丢包情况,进而判断网络是否拥塞。
我习惯把 TCC 的工作流程拆成三个步骤:
- 打标记:发送端在 RTP 扩展头里写入递增的传输序列号。
- 收反馈:接收端定期(通常每 10-20ms)发送 TransportFeedback 消息。
- 算带宽:发送端解析反馈,计算延迟梯度和丢包率,调整发送码率。
这里有个关键点:TCC 用的是传输层序列号,不是 RTP 序列号。RTP 序列号可能因为重传、FEC 而乱序,但传输序列号严格按发送顺序递增。这样反馈回来的信息才是真实的网络传输顺序。
核心优势:TCC 的反馈粒度是单个数据包,而不是像 RTCP RR 那样按 RTT 周期报告。这意味着发送端能在 1-2 个 RTT 内感知到拥塞,而不是等好几秒。
4.2 TransportFeedback 消息结构
TransportFeedback 消息是 TCC 的「情报员」。它通过 RTCP 通道传输,格式定义在 draft-holmer-rmcat-transport-wide-cc-extensions 中。我直接给你看结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P| FMT=15 | PT=205 | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of packet sender |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of media source |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| base sequence number | packet status count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| reference time | fb pkt. count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| packet chunk | packet chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| packet chunk | recv delta | recv delta |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段含义如下:
| 字段 | 说明 |
|---|---|
| base sequence number | 本消息覆盖的第一个传输序列号 |
| packet status count | 本消息包含的包状态数量 |
| reference time | 基准时间戳(24 位,单位 64ms) |
| fb pkt. count | 反馈包计数,用于检测丢失 |
| packet chunk | 包状态块(每个包 1-2 位,表示丢失/已接收/小增量/大增量) |
| recv delta | 接收时间增量(单位 250μs 或 1ms) |
嗯,这里要注意:packet chunk 是压缩的关键。每个包的状态只用 1-2 位表示,如果连续多个包状态相同,还能用游程编码进一步压缩。我曾经在项目里遇到过反馈消息过大导致带宽占用高的问题,后来发现是 chunk 编码没优化好。
我的经验:TCC 反馈消息的发送间隔建议设为 10-20ms。太频繁会增加带宽开销,太稀疏则拥塞响应变慢。我一般用 15ms,兼顾精度和效率。
4.3 TCC 与 GCC 的协同
GCC(Google Congestion Control)是 WebRTC 的拥塞控制框架,TCC 是它的「眼睛」。GCC 本身包含基于延迟的拥塞检测和基于丢包的拥塞检测,但早期版本依赖接收端计算并报告。TCC 把计算搬到了发送端,让 GCC 能更灵活地调整策略。
它们是怎么配合的?我画了张图:
从图上你能看到,TCC 负责「采集数据」,GCC 负责「做决策」。具体来说:
- TCC 提供延迟梯度:通过比较每个包的发送时间和到达时间,计算出网络排队延迟的变化趋势。
- TCC 提供丢包率:通过传输序列号的连续性,精确判断哪些包丢了。
- GCC 使用这些数据:调用
DelayBasedBwe和LossBasedBwe两个模块,分别计算带宽估计值,最后取较小者作为最终码率。
避坑指南:我曾经遇到过一个诡异的问题——TCC 反馈正常,但 GCC 就是不调整码率。查了半天发现是 GCC 的 trendline 滤波器参数没调好。TCC 只是提供原始数据,GCC 怎么解读这些数据同样重要。后面章节我会专门讲 trendline 参数调优。
4.4 TCC 的带宽计算逻辑
发送端收到 TransportFeedback 后,会做两件事:
- 计算延迟梯度:对每个包,计算
到达时间 - 发送时间,然后看这个差值的变化趋势。如果持续增大,说明网络在排队,需要降码率。 - 计算丢包率:检查传输序列号是否有跳变。丢包率超过阈值(默认 2-5%)时,触发丢包响应。
这里有个细节:TCC 的延迟计算用的是「相对延迟」,不是绝对延迟。它关注的是延迟的变化量,而不是延迟本身。为什么?因为绝对延迟受物理距离影响很大,但变化量才能反映网络拥塞。
我举个例子:
包1:发送时间 100ms,到达时间 150ms,延迟 50ms
包2:发送时间 110ms,到达时间 165ms,延迟 55ms
包3:发送时间 120ms,到达时间 180ms,延迟 60ms
延迟梯度:+5ms → +5ms → 持续增大 → 拥塞信号
如果延迟梯度持续为正,GCC 就会认为网络正在变拥塞,开始降码率。反之,如果延迟梯度为负或零,说明网络有富余带宽,可以尝试升码率。
我的建议:TCC 的反馈频率和 GCC 的响应速度需要匹配。如果反馈太慢,GCC 会反应迟钝;如果反馈太快,GCC 可能过度反应。我一般把反馈间隔设为 15ms,GCC 的更新周期设为 20ms,这样刚好能对齐。
4.5 TCC 的优缺点
| 优点 | 缺点 |
|---|---|
| 反馈粒度细,每个包都有状态 | 需要接收端和发送端都支持 TCC 扩展 |
| 延迟低,1-2 个 RTT 内响应 | 反馈消息本身会占用带宽(约 5-10kbps) |
| 与 GCC 天然集成,配置简单 | 对时钟同步要求高,时间戳不准会影响判断 |
| 支持精确的丢包检测 | 在无线网络下,延迟抖动可能被误判为拥塞 |
嗯,最后一点我要多说两句。在 Wi-Fi 或 4G/5G 网络下,延迟抖动很常见。TCC 可能会把这种抖动误判为拥塞,导致码率不必要地下降。我一般会配合 trendline 的阈值调整来缓解这个问题,具体参数后面章节会讲。
总的来说,TCC 是 WebRTC 拥塞控制的基石。没有它,GCC 就像瞎子摸象。理解了 TCC 的工作原理和消息结构,你才能在实际调优中知道问题出在「数据采集」还是「决策算法」上。