2. WebRTC基础回顾
GCC(Google Congestion Control)是WebRTC最核心的码率控制算法。说实话,很多做WebRTC开发的朋友一听到GCC就觉得头大——公式多、概念抽象。但你别怕,今天我们只讲上半部分:延迟检测和卡尔曼滤波。把这些搞懂了,GCC的骨架你就拿下了。
1. 为什么需要基于延迟的拥塞检测?
先问一个问题:网络拥塞了,最直接的信号是什么?
很多人会回答「丢包」。没错,丢包确实是拥塞的标志。但你想过没有——等你发现丢包的时候,网络已经堵死了。就像高速公路上,等你看到前面车都停下来了才踩刹车,已经晚了。
GCC高明的地方在于:它通过观察数据包到达时间的微小变化,提前判断网络是不是「快要堵了」。这个思路,说白了就是「看趋势,不看结果」。
核心思想:当网络开始拥塞时,数据包在路由器队列中的排队时间会逐渐增加。这个排队时延的增加,比丢包发生得更早。GCC就是抓住这个「提前量」来做码率调整。
我在项目中遇到过好几次这样的情况:用纯丢包控制的方案,画面经常突然卡死。换成GCC的延迟检测后,码率在丢包发生前就已经平滑降下来了。用户体验提升非常明显。
2. 到达时间差(Inter-arrival)计算
延迟检测的第一步,是计算数据包的到达时间差。这里有个关键概念——GCC不是看单个包的延迟,而是看一组包的延迟变化趋势。
2.1 包组的概念
GCC把发送端发出的数据包按时间窗口分成一个个「包组」(Packet Group)。怎么分?规则很简单:
- 如果两个包的发送时间间隔小于某个阈值(通常是5ms),它们属于同一个包组
- 如果间隔超过阈值,就开启一个新的包组
为什么要分组?因为单个包的延迟抖动太大,没法用。分组之后,统计特性更稳定。
2.2 到达时间差的计算公式
对于每个包组,GCC计算两个关键值:
- 发送时间差:当前包组与上一个包组的发送时间之差,记作 Δt_s
- 到达时间差:当前包组与上一个包组的到达时间之差,记作 Δt_r
然后,单次延迟变化量 d(i) 就是:
d(i) = Δt_r(i) - Δt_s(i)
这个 d(i) 的含义很直观:
- 如果 d(i) > 0,说明包组在网络上多花了时间——网络可能开始拥塞了
- 如果 d(i) < 0,说明包组跑得比预期快——网络比较空闲
- 如果 d(i) ≈ 0,说明网络状态平稳
我的经验:实际部署时,d(i) 的噪声非常大。单看一次 d(i) 的值基本没有意义。GCC的厉害之处在于,它用卡尔曼滤波器对 d(i) 序列做平滑估计,提取出真正的趋势信号。
3. 卡尔曼滤波器在GCC中的应用
嗯,说到卡尔曼滤波,很多人的第一反应是「线性代数」「状态空间」「协方差矩阵」……打住。我们换个角度理解。
3.1 卡尔曼滤波器的直觉理解
你想想看,我们测量到的 d(i) 包含两部分:
- 真实信号:网络排队时延的变化趋势
- 测量噪声:时钟偏差、操作系统调度抖动、网络突发等随机因素
卡尔曼滤波器要做的,就是从「带噪声的观测值」中,把「真实信号」估计出来。它就像一个智能的加权平均器——当测量值可信时,多相信测量值;当测量值噪声大时,多相信历史预测值。
3.2 GCC中的状态模型
GCC把网络排队时延建模为一个简单的线性系统:
状态方程:x(i) = x(i-1) + w(i)
观测方程:d(i) = x(i) + v(i)
其中:
- x(i) 是第 i 个包组时刻的真实排队时延变化率(我们要估计的)
- w(i) 是过程噪声(网络状态本身的波动)
- v(i) 是测量噪声(前面说的各种随机因素)
这个模型简单到只有一维状态,但足够用了。为什么?因为GCC只关心排队时延的变化趋势,不关心绝对时延值。
3.3 卡尔曼滤波的迭代过程
卡尔曼滤波就两个步骤,不断循环:
- 预测:根据上一时刻的状态,预测当前时刻的状态
- 更新:用当前时刻的测量值,修正预测值
具体到GCC的实现:
| 步骤 | 公式 | 含义 |
|---|---|---|
| 预测状态 | x̂(i|i-1) = x̂(i-1) | 排队时延变化率不会突变,用上一时刻的估计值作为预测 |
| 预测误差协方差 | P(i|i-1) = P(i-1) + Q | 预测的不确定性随时间增加,Q是过程噪声方差 |
| 卡尔曼增益 | K(i) = P(i|i-1) / (P(i|i-1) + R) | 决定「相信测量值」还是「相信预测值」的权重 |
| 更新状态 | x̂(i) = x̂(i|i-1) + K(i) * (d(i) - x̂(i|i-1)) | 用测量残差修正预测值 |
| 更新误差协方差 | P(i) = (1 - K(i)) * P(i|i-1) | 更新后的不确定性降低 |
注意:这里的 Q 和 R 是两个超参数。Q 越大,滤波器响应越快但越容易受噪声影响;R 越大,滤波器越平滑但响应越慢。我在调优时,通常把 Q 设得偏小一些(0.01~0.1),让滤波器先「稳」住,再根据实际网络环境微调。
4. GCC延迟检测的整体流程
把上面这些串起来,GCC的延迟检测流程就是:
- 接收端收到RTP包,按发送时间分组成包组
- 计算每个包组的到达时间差 d(i)
- 卡尔曼滤波器对 d(i) 序列进行滤波,得到平滑后的排队时延变化率 m(i)
- 根据 m(i) 的值,判断当前网络状态:
- m(i) > 阈值 → 过载(Overuse)
- m(i) < 负阈值 → 欠载(Underuse)
- 其他 → 正常(Normal)
- 状态机根据连续的状态判断结果,触发码率调整
这个流程,我画了一张图帮你理解:
5. 卡尔曼滤波器的参数调优
在实际工程中,卡尔曼滤波器的表现很大程度上取决于两个参数:Q 和 R。我踩过不少坑,分享几个经验:
5.1 过程噪声 Q
Q 控制滤波器对网络变化的响应速度。Q 越大,滤波器越「敏感」。我建议:
- WiFi环境:Q 设 0.01~0.05,因为WiFi抖动大,需要平滑
- 有线网络:Q 设 0.05~0.1,响应可以快一些
- 4G/5G:Q 设 0.02~0.08,视信号强度动态调整
5.2 测量噪声 R
R 控制滤波器对测量值的信任程度。GCC中 R 是动态估计的——根据最近一段时间 d(i) 的方差来计算。这样做的好处是:网络噪声大的时候,滤波器自动变得更平滑。
我曾经在一个项目中,把 Q 设成了 0.5,结果卡尔曼滤波器完全跟着噪声跑,码率像过山车一样忽高忽低。后来把 Q 降到 0.02,配合动态 R 估计,效果立刻稳定了。记住:卡尔曼滤波器的核心是「平滑」,不是「跟踪」。
6. 小结
这一章我们讲了GCC延迟检测的三个核心环节:
- 包组:把RTP包按5ms窗口分组,得到稳定的到达时间差 d(i)
- 卡尔曼滤波:从带噪声的 d(i) 中提取排队时延变化率 m(i)
- 状态判断:根据 m(i) 与阈值的比较,输出 Overuse/Normal/Underuse 状态