4. 拥塞控制算法概览

TCC 是 WebRTC 里我最喜欢的一个模块。为什么?因为它把拥塞控制从「猜」变成了「算」。早期 GCC 依赖接收端报告(RTCP RR)来估算带宽,延迟大、精度低。TCC 直接在传输层做文章,让发送端能实时感知每个数据包在网络里的命运。

说白了,TCC 就是给每个数据包发了一张「追踪单」。网络状况好不好,看一眼反馈就知道。

4.1 TCC 的工作原理

TCC 的核心思路很简单:发送端给每个 RTP 包打上一个传输序列号(Transport Sequence Number),接收端收到后,把这些序列号连同到达时间打包成 TransportFeedback 消息发回给发送端。发送端根据这些信息,计算出每个包的延迟和丢包情况,进而判断网络是否拥塞。

我习惯把 TCC 的工作流程拆成三个步骤:

  1. 打标记:发送端在 RTP 扩展头里写入递增的传输序列号。
  2. 收反馈:接收端定期(通常每 10-20ms)发送 TransportFeedback 消息。
  3. 算带宽:发送端解析反馈,计算延迟梯度和丢包率,调整发送码率。

这里有个关键点: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 协同工作流程 发送端 RTP + 传输序列号 接收端 收集到达时间 RTP 数据流 反馈生成器 打包 TransportFeedback RTCP TransportFeedback GCC 控制器 延迟 + 丢包分析 码率调整器 更新目标码率 反馈闭环 关键点 TCC 提供数据 GCC 做出决策 两者缺一不可

从图上你能看到,TCC 负责「采集数据」,GCC 负责「做决策」。具体来说:

  • TCC 提供延迟梯度:通过比较每个包的发送时间和到达时间,计算出网络排队延迟的变化趋势。
  • TCC 提供丢包率:通过传输序列号的连续性,精确判断哪些包丢了。
  • GCC 使用这些数据:调用 DelayBasedBwe 和 LossBasedBwe 两个模块,分别计算带宽估计值,最后取较小者作为最终码率。

避坑指南:我曾经遇到过一个诡异的问题——TCC 反馈正常,但 GCC 就是不调整码率。查了半天发现是 GCC 的 trendline 滤波器参数没调好。TCC 只是提供原始数据,GCC 怎么解读这些数据同样重要。后面章节我会专门讲 trendline 参数调优。

4.4 TCC 的带宽计算逻辑

发送端收到 TransportFeedback 后,会做两件事:

  1. 计算延迟梯度:对每个包,计算 到达时间 - 发送时间,然后看这个差值的变化趋势。如果持续增大,说明网络在排队,需要降码率。
  2. 计算丢包率:检查传输序列号是否有跳变。丢包率超过阈值(默认 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 的工作原理和消息结构,你才能在实际调优中知道问题出在「数据采集」还是「决策算法」上。