Introduction to VBV
VBV的原理
在使用视频编码器(如 x265)时,通常会设置 VBV 来限制峰值码率,例如 --vbv-maxrate=500kbps --vbv-bufsize=500kbit。在这种配置下,允许的峰值码率就是 500kbps 吗?字面上看似乎是,但实际上(以 1 秒为窗口)允许的峰值码率是 500kbps × 1s + 500kbit = 1000kbit,也就是 1Mbps。为什么会这样,我们来深入分析下。
VBV是编码器内部的一个模块,用来模拟解码器的CPB(Coded Picture Buffer)的状态;通常VBV是按照max-rate接收码率,按照1/fps的间隔从buffer中取出一帧,用来解码;每帧的尺寸是不一样的;如果用Level表示VBV当前Buffer的fullness,Level -= frame_size; Level += max-rate/fps;
他就像一个水池,一个进水管按照固定的速率往水池中灌水,同时,会用一个容器从水池中舀水出去,每次容器的大小也不相同;Level就是vbv buffer中的水位,如果水位低,容器的尺寸比较大,那么此时就会舀不出来,这种情况我们就叫Underflow;如果水位很高,舀水的容器还很小,每次舀的水很少,此时水会溢出,这种情况就叫overflow; CBR下会要求不要overflow,也不要underflow;而CRF+VBV的码控下,则一般只限制不要Underflow;在实际使用的时候,我们一般重点考虑underflow;假如时间窗口是1s,假如vbv buffer是全满,L = Buf_sz; 因此这一秒可以使用的比特数,就是Buf_sz + max_rate * 1s; 所以理论上来讲,这一秒可以使用的码率就是 Buf_sz + max_rate;
在实际编码器开发的时候,如果在配置中使用了VBV,那么vbv的管理就很重要;常见的错误,就是为了避免vbv underflow,大幅度提高某一帧的QP,导致这一帧的QP非常大,质量很差;在实际的使用中,需要估计下未来帧的复杂度,为复杂的帧来预留出VBV Buffer,也就是在复杂帧到来之前,提前增加VBV的Level,这样可以分配出更多的bit给复杂的帧,从而保证了复杂帧的质量;
进水匀速(max-rate),每个移除时刻瞬间舀走一帧;帧越大,水位掉得越深,触底即 underflow。图示为解码端 CPB fullness(编码端 VBV 的 Level 与之同构)。
以上就是vbv的基本原理,以下介绍下x265里面的设置。
x265 的 VBV 参数
记住上面那个水池模型(水位 Level、进水 max-rate、每帧舀走 frame_bits、约束 0 ≤ Level ≤ Buf_sz),x265 的每个参数其实都是在调这个模型的某个部分。
--vbv-maxrate(kbits/sec) —— 进水速率,也就是更新式里的 max-rate。必须和 --vbv-bufsize 同时非零才会启用 VBV(CRF 模式下两者都得给)。默认 0(关闭)。
--vbv-bufsize(kbits) —— 水池容量,即水位上界 Buf_sz。bufsize / maxrate 就是“能缓冲几秒”,也直接决定端到端延迟:buffer 越大,能容忍的瞬时大帧越大,但延迟越高。默认 0(关闭)。
--vbv-init —— 水位的初值(0~1 的比例,或 kbits)。开始编码时水池有多满,直接决定第一帧能多大:init 越高、起始水位越高,第一帧(通常是最大的 I 帧)能借的水越多。默认 0.9。
--vbv-end / --vbv-end-fr-adj —— 编码结束时水位必须回到的末值,以及从第几帧开始往这个末值收。用于分段(chunked)编码:让每段结束的水位 = 下一段开始的水位,多段拼接才不会违反 VBV。默认 0。
--min-vbv-fullness(默认 50)/ --max-vbv-fullness(默认 80) —— 把工作区间从完整的 [0, Buf_sz] 人为收窄到 [min%, max%],rate control 会尽量让水位停在这个区间里。
为什么收窄区间会影响压缩效率?区间越宽,rate control 借水 / 存水的自由度越大,越能把 bit 调度给真正需要的帧:
- 调低 min-fullness → 允许水位掉得更低 → 给复杂帧预留更多“可借的水” → 压缩效率更好,但更靠近 underflow,牺牲 VBV conformance(x265 标为 experimental)。
- 调高 max-fullness → 允许水位涨得更高 → 能多存一些“信用”留给后面的帧 → 压缩效率更好。
最后看这个模型在 x265 源码里怎么落地。m_bufferFillFinal 就是我们说的水位 Level:
1 | // Initial |
初始化时 m_bufferFillFinal = m_bufferSize × vbvBufferInit,正是“用 init 比例设定起始水位”;每帧更新 -= frame_bits(舀走这一帧)、+= bufferRate(这一拍进的水,bufferRate = maxrate / fps)—— 和前面推导的不变量更新式一字不差。