带宽估计与拥塞控制到底在干什么:实时音视频为什么必须边传边降边试探
前言:为什么实时音视频最怕的,不是带宽低,而是你根本不知道现在到底还能发多少
前面那篇《WebRTC 为什么难》里,我一直在反复强调一件事:
实时音视频不是静态系统,而是动态系统。
这句话背后,最典型、最硬核的一条主线,其实就是:
- 带宽估计(Bandwidth Estimation, BWE)
- 拥塞控制(Congestion Control)
很多人第一次看到这两个词时,会下意识把它们理解成比较抽象的网络优化概念。
比如:
- 带宽估计,就是“测网速”
- 拥塞控制,就是“别发太多”
这样理解不算完全错,但还是太浅了。
因为真实的实时音视频系统里,你面对的不是:
我有一条稳定 4Mbps 的链路,那我就一直按 4Mbps 发。
而更可能是:
- 刚才还能发 4Mbps,现在突然只剩 2.5Mbps
- 上一秒 RTT 还正常,这一秒突然抖了
- 码率没超很多,但队列开始积压了
- 丢包变多了,但到底是链路拥塞、Wi-Fi 抖动,还是接收端处理不过来
- 你要不要立刻降码率,降多少,什么时候再试探着升回去
你会发现,这根本不是“测一个值然后照着发”这么简单。
它真正要解决的问题是:
在一个不停变化、而且你永远看不全的网络里,尽量猜出当前还能安全发多少,并且在发错之前及时收手。
这件事特别像开车下雾天山路:
- 你看不远
- 路况一直变
- 开慢了效率低
- 开快了就可能冲出去
- 所以你只能边走边试,边看边调
这就是实时音视频里的带宽估计和拥塞控制。
而且这次我会按你特别强调的要求来,把核心代码示例放进去,不再只是嘴上讲流程。
重点讲清:
- 带宽估计到底在估什么
- 拥塞控制到底在控什么
- 为什么它们几乎决定了 WebRTC 体验上限
- 一个发送端到底是怎么根据反馈动态调码率的
- 接收端的到达时间、丢包、RTT 是怎么参与判断的
- 如果你自己实现一个简化版 BWE/CC 框架,核心代码骨架会长什么样
这篇我会继续放:
- 结构图
- 时序图
- 关键状态机图
- 核心伪代码 / C++ 风格示例
我希望这篇读完之后,你脑子里对“边传边降边试探”这件事,不再只是一个模糊口号,而是真能看到系统是怎么动起来的。
一、先给一句最重要的话:带宽估计不是在“测最大值”,而是在持续寻找当前网络还能承受的发送区间
如果让我先压一句最重要的话,我会这样说:
带宽估计不是在找一个永远正确的固定值,而是在持续逼近“当前网络还能承受、且不会明显恶化体验”的发送区间。
这句话特别重要。
因为很多人一说“带宽估计”,脑子里会自动出现测速软件。
但实时音视频根本不是那种场景。
测速软件更像是在做:
- 现在尽量压榨带宽上限是多少
而实时音视频更像在做:
- 在不把链路打爆的前提下,我现在大概还能安全发多少
- 如果网络变差了,我要尽快收
- 如果网络变好了,我也不能一下子冲太猛
所以它不是“测极限”,而是:
持续试探、持续修正、持续保守地靠近可用上限。
这也直接解释了为什么它总和拥塞控制绑在一起。
因为一旦估错,系统就不是“分数差一点”,而是:
- 延迟堆积
- 卡顿上升
- 丢包飙升
- 画质猛降
- 用户开始明显感知异常
二、先看全局:带宽估计和拥塞控制在整条 WebRTC 链路里的位置
先上总图。
flowchart LR
A[采集/编码] --> B[发送端码率控制]
B --> C[RTP发送]
C --> D[网络]
D --> E[接收端统计到达情况]
E --> F[RTCP/反馈信息]
F --> G[发送端带宽估计器]
G --> B
这张图最关键的意思是:
- 编码器不是自己想发多少就发多少
- 发送端码率不是写死的
- 接收端不是只负责播
- 网络反馈不是装饰品
整条链路真正形成的是一个闭环:
发 -> 过网 -> 被观察 -> 反馈回来 -> 再调发。
所以带宽估计和拥塞控制,本质上不是一个“附加优化模块”,而是:
WebRTC 动态系统的大脑之一。
三、带宽估计到底在估什么
这个问题如果不说透,后面都容易飘。
3.1 它估的不是裸带宽上限
真正工程上更有用的说法是:
它估的是当前这条实时链路,在现有网络状态、延迟预算和丢包情况之下,较合适的发送码率范围。
注意这里几个词:
- 当前
- 实时链路
- 较合适
- 范围
这几个词缺一个都不准。
因为你不是在实验室里测网卡上限,而是在实时业务里做决策。
3.2 这个估计值通常会受哪些东西影响
典型包括:
- 接收端包到达时间间隔
- 一段时间内的吞吐量
- 丢包率
- RTT 变化
- 排队延迟趋势
- 是否出现明显突发拥塞
所以它更像一个综合判断,而不是单指标结论。
四、拥塞控制到底在控什么
如果说带宽估计负责“猜现在还能发多少”,那拥塞控制负责的就是:
把这个猜测变成一个真实可执行、且不会太激进的发送行为。
它通常会控制这些东西:
- 目标码率
- 是否立刻降码率
- 是否允许缓慢升码率
- 是否请求编码器降分辨率 / 帧率
- pacing 发送节奏
- 突发数据是否需要抹平
也就是说,拥塞控制不是一句“慢点发”。
它更像一套行为策略:
- 什么时候该踩刹车
- 什么时候该轻踩油门
- 什么时候只能观察,别乱动
五、为什么实时音视频必须“边传边降边试探”
这是整篇最核心的主旨之一。
5.1 因为网络条件是时变的
今天你能跑 3Mbps,不代表 3 秒后还行。
比如这些场景都很常见:
- 手机从 Wi-Fi 切到蜂窝
- 同一 Wi-Fi 下别的设备突然占网
- 路由器缓冲开始膨胀
- 基站侧调度变化
- 上下行链路暂时不对称
这意味着固定码率策略很容易出事。
5.2 因为你看不到网络内部,只能通过症状反推
发送端并不能直接看到整条公网路径里发生了什么。
它看到的通常只是外在症状:
- 到达变慢了
- RTT 变大了
- 丢包多了
- 吞吐下降了
所以它做的其实不是“知道”,而是:
根据反馈去猜,再根据猜测去控。
5.3 因为过冲的代价特别大
如果你发猛了,最糟糕的不是平均吞吐差一点,而是会直接导致:
- 队列积压
- 延迟上涨
- jitter 变大
- 丢包增加
- 接收端 jitter buffer 被迫变厚
- 体验从“清晰”迅速变成“卡、糊、慢”
所以实时系统非常怕过冲。
这也是为什么它们通常:
- 降码率比较果断
- 升码率比较谨慎
这就形成了你看到的那种典型风格:
边传边看,出事就降,没事再慢慢试探。
六、一个典型发送端决策闭环长什么样
先看决策图。
flowchart TD
A[发送RTP数据] --> B[接收端统计到达情况]
B --> C[生成RTCP/反馈]
C --> D[发送端更新网络状态]
D --> E{检测到拥塞迹象?}
E -- 是 --> F[快速降目标码率]
E -- 否 --> G{网络稳定一段时间?}
G -- 是 --> H[缓慢升码率]
G -- 否 --> I[保持当前码率]
F --> J[通知编码器调整输出]
H --> J
I --> J
J --> A
这张图几乎就是一个简化版 WebRTC 码率控制核心。
重点不是它有多复杂,而是你要看见:
它是一个循环系统,而不是一次性配置。
七、先上第一段核心代码:一个最小带宽估计器的数据结构该怎么长
这一节开始上代码。
下面这个不是某个现成库源码,而是我给你整理出来的一个简化版工程骨架,重点是帮助你理解“系统到底怎么组织”。
1 | |
这段代码最该看什么
不是每个阈值本身,而是它体现出来的结构:
- 先收反馈
- 把反馈转成可判断的网络指标
- 根据指标判断当前是过载、欠载还是稳定
- 再产出新的目标码率
这就是一个带宽估计器最基础的骨架。
八、第二段核心代码:吞吐量、丢包率、延迟梯度这些指标到底怎么从反馈里算出来
这一步特别关键,因为很多文章喜欢直接讲“根据反馈调整码率”,但不写中间这层,读者其实还是没抓住系统。
8.1 计算吞吐量
1 | |
这里的直觉是什么
它不是在问“链路理论速度多少”,而是在问:
这一小段时间里,我实际看见有多少比特真正送到了接收侧。
这才是实时系统更关心的“有效吞吐”。
8.2 计算丢包率
1 | |
这里要注意什么
丢包率不是唯一依据,但它是非常敏感的拥塞信号之一。
不过真实工程里也不能一看丢包就武断认定“全是网络拥塞”,因为有时候还可能混入:
- 无线链路波动
- 接收端处理不过来
- 队列溢出
所以它通常是信号之一,不是唯一裁决者。
8.3 计算延迟梯度
这个指标很像“排队是不是在变严重”。
1 | |
这个指标为什么重要
因为很多拥塞在早期未必马上表现成高丢包,但会先表现成:
- 包越来越晚到
- 队列越来越长
- 单向延迟开始上爬
所以如果你只盯丢包,往往会反应偏慢。
九、第三段核心代码:为什么发送端通常“降得快,升得慢”
这是拥塞控制里特别核心的行为风格。
下面是一个简化版 AIMD 风格策略骨架。
1 | |
这段代码背后的工程直觉
为什么降得快
因为一旦检测到拥塞,继续犹豫的成本很高:
- 队列越积越大
- 时延继续涨
- 丢包可能变严重
- 用户体验会更快恶化
所以要果断收。
为什么升得慢
因为你并不知道“看起来没事”是否就代表还有大量余量。
如果升太猛,就很容易:
- 刚恢复一点,又马上打爆
- 形成抖动
- 系统在高低码率之间来回抽搐
所以升码率一般更像试探,而不是冲刺。
十、第四段核心代码:发送端怎么把新的目标码率真正作用到编码器上
这一层特别重要,因为很多文章讲到码率控制就停了,但系统真正落地时,得把结论传给编码器。
1 | |
这段代码最值钱的地方
你能直接看到:
带宽估计不是最后答案,它真正的意义,是驱动编码器和视频层级一起调整。
也就是说,拥塞控制最终通常要落到这些具体动作上:
- 改目标码率
- 改分辨率
- 改帧率
- 改编码复杂度
这才是真正能被用户体验感知到的结果。
十一、第五段核心代码:接收端反馈怎么组织,为什么它不是“随便回个统计值”
下面给一个简化版接收端反馈收集器。
1 | |
这里为什么重要
因为发送端看到的“网络状态”,本质上是接收端组织后的反馈视图。
如果接收端反馈做得太粗:
- 发送端判断会迟钝
- 误判会更多
- 码率调整会更抖
所以这层不是可有可无的小模块,而是闭环里的观测器。
十二、一张时序图看懂 BWE/CC 为什么像“边开边修的控制系统”
sequenceDiagram
participant S as Sender
participant N as Network
participant R as Receiver
participant FB as Feedback Logic
S->>N: RTP packets at current bitrate
N->>R: packets arrive with jitter/loss/delay
R->>FB: arrival stats / loss / timing
FB->>S: RTCP / transport feedback
S->>S: estimate bandwidth
S->>S: update congestion state
S->>S: reduce/increase target bitrate
S->>N: next RTP packets with new pacing/bitrate
这张图你如果看懂了,基本就知道为什么这套系统不可能只靠一个静态配置活下去。
十三、为什么很多时候“丢包触发降码率”还不够,你还得盯住延迟趋势
这是实际工程里非常关键的一点。
13.1 只盯丢包,常常会慢半拍
如果一个链路开始拥塞,真实发生的顺序常常更像:
- 队列先开始变长
- 延迟和 jitter 先升
- 再往后才开始明显丢包
也就是说,丢包很多时候是比较晚的症状。
如果你只等丢包高了才降,系统往往已经开始难受了。
13.2 所以很多控制器会把“排队趋势”看得很重
这也是为什么:
- arrival time
- one-way delay trend
- RTT gradient
这些指标在实时系统里特别有价值。
它们不是最终答案,但很像“拥塞前兆”。
十四、一个更像真实系统的状态机应该怎么想
下面给一个更接近工程思维的简化状态机。
stateDiagram-v2
[*] --> Hold
Hold --> Increase: 网络稳定且余量看起来足够
Hold --> Decrease: 检测到拥塞迹象
Increase --> Hold: 继续观察
Increase --> Decrease: 试探过猛出现拥塞
Decrease --> Hold: 收缩后等待稳定
这张图想表达的是:
- 不是每次反馈都必须动作
- 不是永远涨或者永远降
- 很多时候最好的决策恰恰是“先稳住,别乱动”
这点特别像真正做系统的人会有的判断。
十五、发送 pacing 为什么也很重要,它和单纯改码率不是一回事
很多人一说拥塞控制,只想到“改 bitrate”。
但真实工程里,pacing 也非常关键。
因为即便平均码率没超,如果你发得太突发,也可能把链路打出问题。
15.1 一个简化版 pacing 发送器骨架
1 | |
15.2 这段代码说明什么
它说明拥塞控制不只是“发多少”,还包括:
怎么把这些数据更平滑地送出去。
这对减少网络瞬时压力非常有帮助。
十六、为什么这个系统本质上永远在做“次优解”
这一点我特别想强调。
因为很多人学算法时容易潜意识觉得:
应该有一个正确码率,系统只要算出来就好了。
但真实网络不是数学题。
你拿到的永远都是:
- 延迟的滞后反馈
- 局部观测
- 噪声很大的信号
- 时刻在变的链路
所以带宽估计和拥塞控制本质上不是“求真值”,而是:
在不完整信息下做尽量不蠢的动态决策。
这也是为什么不同实现会有不同风格:
- 有的更激进
- 有的更保守
- 有的更偏延迟优先
- 有的更偏吞吐优先
它们很多时候不是谁绝对对,而是谁更适合当前场景。
十七、最后给一句更像工程师的话:BWE/CC 的真正目标,不是把带宽吃满,而是把体验打稳
写到这里,最想落下的一句话是:
实时音视频里的带宽估计与拥塞控制,最终目标不是吃满链路,而是让实时体验尽量稳定。
这个目标非常重要。
因为如果你把目标错设成“尽量打满带宽”,系统就会特别容易:
- 过冲
- 抖动
- 延迟膨胀
- 频繁降级
- 体验不稳定
而如果你把目标设成:
- 尽量稳住可交流体验
- 在安全区间里慢慢逼近上限
- 出现风险时及时刹车
那这套系统的很多设计选择就都能解释通了。
所以真正成熟的实时系统,不会像测速软件那样兴奋地冲极限,而更像一个老练司机:
- 看不清时先收
- 稳住了再探
- 宁可少赚一点,也别翻车
十八、总结
最后把这篇收成几句话。
1. 带宽估计不是测固定网速,而是在持续逼近当前可安全发送区间
它关心的是实时体验下的可用发送范围,不是理论最大值。
2. 拥塞控制不是一句“少发点”,而是一整套动态行为策略
它会决定:
- 降不降码率
- 升不升码率
- 升降速度多快
- 是否改分辨率 / 帧率
- pacing 怎么做
3. 真正的闭环是:发 -> 观察 -> 反馈 -> 再调发
这也是 WebRTC 像动态控制系统的根本原因。
4. 核心代码最值得看的,是系统结构而不是阈值本身
也就是:
- 如何组织反馈
- 如何计算指标
- 如何判断拥塞
- 如何把目标码率作用到编码器
- 如何做 pacing
5. BWE/CC 的最终目标不是把带宽吃满,而是把实时体验打稳
这才是更像工程师的理解。
如果你愿意,我下一篇建议直接接:
《NACK、PLI、FEC 到底怎么配合:丢包之后,实时音视频系统到底怎么补救》
这篇会把“反馈回来以后怎么救”这条线单独讲透,而且我也会继续放核心代码示例。