4. 固定码率与可变码率
做码率控制,说白了就是在跟一个东西打交道——缓冲区。
我刚开始接触视频编码时,总觉得码率控制就是算个目标码率,然后让编码器照着压就行了。结果第一次做实时流媒体项目,画面动不动就卡顿、花屏,甚至直接断流。查了半天,问题出在缓冲区上——要么数据撑爆了,要么缓冲区空了没东西可发。
这一章,我们就来聊聊缓冲区的几个经典模型,以及实际编码中最头疼的 VBV 上溢/下溢问题。
4.1 为什么需要缓冲区?
视频编码的输出,码率是剧烈波动的。I 帧可能瞬间产生几十 KB 甚至上百 KB 的数据,而 P 帧、B 帧可能只有几 KB。但传输通道(比如网络、硬盘写入)通常要求一个相对平稳的码率。
缓冲区就是用来做削峰填谷的。编码器先把数据吐到缓冲区,然后以恒定速率从缓冲区往外取数据发送。这样,网络侧看到的就是一个平滑的码流。
核心矛盾:编码器输出是突发的,传输通道是平稳的。缓冲区就是中间的蓄水池。
4.2 漏桶模型(Leaky Bucket)
漏桶模型是最直观的比喻。想象一个桶,底部有个小洞。你可以往桶里倒水(编码数据),但水只能从底部的小洞以固定速率漏出去(传输)。
- 桶的容量:缓冲区大小(Buffer Size)。
- 漏水速率:目标码率(Target Bitrate)。
- 倒入的水量:每帧编码后的数据量。
如果倒水太快,桶满了,水就会溢出——这就是缓冲区上溢。在编码中,上溢意味着数据丢失,画面会出问题。
如果倒水太慢,桶空了,底部的洞就没水可漏——这就是缓冲区下溢。下溢意味着传输通道空闲,带宽浪费了,播放端可能卡顿等待。
我个人习惯:在 H.264/H.265 编码器中,漏桶模型常用于描述 HRD(Hypothetical Reference Decoder)的行为。你设置的码率、缓冲区大小,最终都会映射到一个漏桶参数上。
4.3 令牌桶模型( Bucket)
漏桶模型有个问题:它太死板了。即使网络有空闲带宽,编码器也不能突发多传一点数据。令牌桶模型就灵活一些。
令牌桶的思路是:系统以恒定速率往桶里放令牌()。每个令牌代表一个字节的发送权限。编码器要发送数据,必须先拿到足够数量的令牌。桶里最多能攒一定数量的令牌(桶容量)。
- 令牌放入速率:平均码率。
- 桶容量:允许的突发大小(Burst Size)。
- 数据发送:消耗令牌。
这意味着,如果之前网络空闲,桶里攒了很多令牌,那么编码器可以突然发一大波数据(比如一个大的 I 帧),只要令牌够用。这更符合实际网络和编码器的行为。
| 特性 | 漏桶模型 | 令牌桶模型 |
|---|---|---|
| 输出速率 | 严格恒定 | 允许突发 |
| 对突发流量 | 直接丢弃或排队 | 允许,只要令牌够 |
| 适用场景 | 严格 CBR(恒定码率) | VBR(可变码率) |
| 实现复杂度 | 低 | 中 |
我曾经踩过的坑:在一个直播项目中,我用了漏桶模型做平滑,结果发现编码器输出的 I 帧太大,缓冲区频繁上溢。后来换成令牌桶模型,允许 I 帧突发,同时用 QP 回调控制 P/B 帧的码率,问题才解决。选模型一定要看场景。
4.4 VBV 缓冲区:解码器的视角
前面说的缓冲区是编码器侧的。但别忘了,解码器也有缓冲区——VBV(Video Buffering Verifier)。
VBV 是 H.26x 标准中定义的一个假想解码器模型。编码器在编码时,必须保证生成的码流能被这个假想解码器正确解码,不会出现缓冲区上溢或下溢。
说白了,编码器不仅要管自己怎么输出,还要管解码器能不能接得住。
4.5 VBV 上溢与下溢
这是实际编码中最容易出问题的地方,尤其是在硬件编码器或实时编码中。
4.5.1 VBV 上溢(Overflow)
解码器缓冲区满了,但新的数据还在往里塞。解码器来不及处理,数据丢失。
- 原因:编码器输出码率太高,或者缓冲区设置太小。
- 表现:画面花屏、马赛克、解码器报错。
- 解决:提高 QP(量化参数),降低码率;或者增大 VBV 缓冲区大小。
4.5.2 VBV 下溢(Underflow)
解码器缓冲区空了,但解码器还没到显示时间,没数据可解。解码器只能干等。
- 原因:编码器输出码率太低,或者缓冲区太大,数据没及时填满。
- 表现:播放卡顿、画面冻结。
- 解决:降低 QP,增加码率;或者插入填充数据(如 SPS/PPS/SEI)。
关键点:VBV 上溢和下溢是编码器必须避免的。标准规定,如果码流无法通过 VBV 校验,那就是不合法的码流。
4.6 码率控制如何与 VBV 协同?
码率控制算法(比如我们后面要讲的 CBR、VBR、ABR)在计算每帧的目标比特时,必须考虑 VBV 的当前状态。
一个典型的流程是:
- 编码器知道当前 VBV 的占用度(Buffer Level)。
- 根据目标码率和缓冲区大小,计算一个安全的目标比特范围。
- 如果 VBV 快满了,就降低码率(提高 QP)。
- 如果 VBV 快空了,就提高码率(降低 QP),或者插入填充数据。
// 伪代码:VBV 感知的码率控制
int calc_target_bits(float buffer_level, float buffer_size, float target_bitrate) {
float upper_threshold = buffer_size * 0.8; // 80% 警戒线
float lower_threshold = buffer_size * 0.2; // 20% 警戒线
int target_bits = (int)(target_bitrate / fps);
if (buffer_level > upper_threshold) {
// 缓冲区快满了,降低目标比特
target_bits = (int)(target_bits * 0.7);
} else if (buffer_level < lower_threshold) {
// 缓冲区快空了,增加目标比特
target_bits = (int)(target_bits * 1.3);
}
// 还要确保不超过最大/最小比特限制
target_bits = clamp(target_bits, MIN_BITS_PER_FRAME, MAX_BITS_PER_FRAME);
return target_bits;
}
我建议:在实际工程中,不要等到 VBV 满了或空了才调整。设置一个安全水位线(比如 20%~80%),在这个范围内做精细调节。超出范围时,做紧急干预。
4.7 核心知识结构图
下面这张图梳理了缓冲区模型与 VBV 的核心关系,你一看就明白。
4.8 实际工程中的避坑指南
- 缓冲区大小不是越大越好:大了虽然不容易上溢,但会增加延迟。直播场景下,延迟是命根子。
- 注意 I 帧的冲击:I 帧的数据量通常是 P 帧的 5~10 倍。如果不做特殊处理,很容易触发 VBV 上溢。我习惯在编码 I 帧前,先预留足够的缓冲区空间。
- 填充数据不是万能的:下溢时插入填充数据(如 SEI)可以临时解决问题,但会浪费带宽。长期来看,还是要靠码率控制算法把码率提上去。
- 硬件编码器的 VBV 行为可能不同:有些硬件编码器内部有私有缓冲区,VBV 的行为和软件模拟的不完全一样。一定要看芯片手册,或者实测验证。
我曾经遇到的一个诡异问题:在某款硬件编码器上,设置 VBV 缓冲区大小为 1000ms,结果编码出来的码流在标准解码器上频繁下溢。后来发现,硬件内部的 VBV 模型和标准 HRD 模型有差异,它把编码延迟也算进去了。最后我不得不把 VBV 大小设为 1200ms 才通过校验。
嗯,缓冲区模型这块,说白了就是理解数据怎么流、流多快、蓄水池多大。理解了这些,后面讲 CBR、VBR、ABR 时,你就能明白为什么同样的码率,不同算法表现差那么多。