H.264 frame_num 语义及其相关应用

1. 概述

H.264 中,有一个很重要的计数变量:frame_num。此值无法由其它值推导出来,而是在码流中直接硬编码该数值,并且每一帧都会携带。
H.264 语义中很多其它的变量都是依赖 frame_num 推导出来的,所以本篇博客主要记录一下 frame_num 的语义以及在 x264 中,其它依赖此变量的典型用法。

2. frame_num

frame_num 的值编码在每帧 slice_header::frame_num 字段,此变量是一个循环计数变量,循环计数意味着有一个最大值,而这个最大值就编码在 sps::log2_max_frame_num_minus4 字段上。
值得注意的是,frame_num 一般有 3 种情况下会等于 0:

  • 序列第一帧
  • 每个 IDR 帧 (当然序列第一帧一定是 IDR 帧)
  • 计数到了最大值,然后从 0 开始

H.264 中,并不是每一帧的 frame_num 都会递增 1,而是取决于当前帧是否可以用作参考帧被后续帧参考,例如如下序列(按编码顺序列出,参考毕厚杰书):

图像类型 是否用作参考 frame_num
I 0
P 1
B 2
P 2
B 3
P 3
B 4
P 4
... ... ...

可以看到,当前帧 frame_num = 上一个可参考帧的 frame_num + 1
这个公式有两个值得注意的点:

  • 非参考帧也有有效的 frame_num 值被编码到码流中,主要是因为所有帧都需要计算 POC 值,以便从编码顺序还原出显示顺序
  • 每个可参考帧的 frame_num 值都是不同的,因为只有可参考帧才会加入到 DPB 列表中。而 DPB 列表里面每一帧的 frame_num 值都一定是不同的,以便区分

3. mmco 语义

mmco (Memory Management Control Operation) 即用于管理 DPB 列表的一组语义规范。
首先要明确一下执行 mmco 的时机,对于解码器来说,即:

  • 开始解码前,会通过解析 slice header,获取 mmco 语义
  • 解码完一帧后,解码下一帧之前,如果当前帧是可参考帧,那么会执行当前帧码流所携带的 mmco 语义,修改 DPB 列表;如果非参考,那么 DPB 本轮解码后不会发生变化
  • mmco 执行完毕后,如果当前帧是可参考帧,则加入到 DPB 的尾部

mmco 的语法比较复杂,功能也很多,但是这里只介绍 memory_management_control_operation == 1 时的语法,这也是 x264 唯一支持的自适应 mmco 操作:

  • 此时 mmco 的作用是:从 DPB 中移除一个或多个短期参考帧
  • mmco 此时是一个 do while 循环,每次循环可以移除一个短期参考帧 (读取到 memory_management_control_operation == 0 时退出循环)
  • 循环体中,会读取并计算出码流所携带的 frame_num 值,然后在 DPB 中找到 frame_num 相等的那一帧,然后删除掉

这里提一下,除了 mmco,也还有其它删除 DPB 中帧的语义:

  • 先进先出,即 DPB 中的帧数量达到 sps::max_num_ref_frames 字段规定的最大帧数量时,会移除最早的一帧
  • 遇到 IDR,DPB 执行清空操作

另外还有一点,刚刚提到 do while 循环会读取并计算码流中所携带的 frame_num 值,实际上是不准确的:

  • 码流在 mmco 中并不直接编码 frame_num,而是编码一个 difference_of_pic_nums_minus1 字段
  • difference_of_pic_nums_minus1 计算公式为:difference = ((target_frame_num - current_frame_num + max_frame_num) % max_frame_num) - 1
  • current_frame_num 即当前帧的 frame_num 值,target_frame_num 即要删除的短期参考帧的 frame_num 值
  • max_frame_num 主要是为了处理回绕问题,例如直接 target_frame_num - current_frame_num 就有可能得到一个负数值

可以看到,mmco 语义强依赖于 frame_num,此数值相当于帧的唯一标识,frame_num 不准确,将会导致解码错误。

4. poc 语义

由于引入了 B 帧,且在解码端接收到的帧顺序是解码顺序,并不是播放顺序,因此需要另一个标识播放顺序的字段,让解码器解码完毕后,能够正确还原出每一帧的播放前后顺序(按编码顺序列出):

图像类型 是否用作参考 frame_num poc
I 0 0
P 1 8
B 2 6
B 3 2
B 3 4
P 3 10
... ... ... ...

可见,如果没有 poc 用于标识播放顺序,那么仅仅依靠 frame_num 是无法还原出正确的播放顺序的。
注意,这里的 poc 与 pts 时间戳不是同一个作用,poc 是 h.264 内部主要用于解码后还原播放顺序;pts 时间戳主要用于播放渲染时控制渲染节奏

码流中可以携带 poc 字段的相关信息,其中 sps::pic_order_cnt_type 字段用于控制码流中编码 poc 字段的方式:

  • 支持 0、1、2 三种不同的 poc 编码方式
  • 其中 x264 只支持 0 和 2 两种方式,当没有 B 帧时,采用方式 2;当有 B 帧时,采用方式 0
  • 方式 0 中,把 poc 值的低位(LSB)编码到 slice_header 中,高位(MSB)由解码器自行计算。低位的最大值编码在 sps::log2_max_pic_order_cnt_lsb_minus4 字段中。高低位的计算参考:https://winddoing.github.io/post/c614239b.html
  • 方式 2 中,码流不编码 poc 信息,解码器直接由 frame_num 计算得到

在 x264 中,遇到 IDR 时 poc 值清零,其余时刻总是等于按显示顺序,当前帧与 IDR 之间的帧数量 x2,x2 的原因可能是考虑到为了支持场编码,上下场需要一个独立的 poc 值。

5. reorder 语义

reorder 即重排序语义,首先要明确一下重排序发生的时机。假设当前是 P 帧:

  • 开始解码前,会通过解析 slice header,获取前向参考帧的 reorder 语义
  • 开始解码前,取出 DPB 中的所有帧(只考虑短期参考帧),按照 frame_num 值(准确说是 pic_num 值),从大到小进行排序,得到一个默认排序后的列表 list0
  • 开始解码前,根据 reorder 语义,对排序后的列表 list0 中的一个或多个元素进行重新排序
  • 然后开始正式解码

如果是 B 帧,会复杂一些:

  • slice header 中会额外携带后向参考帧的 reorder 语义
  • 开始解码前,会先对 DPB 中的所有帧,按照 frame_num 计算 poc 值
  • list0 默认排序,小于当前帧 poc 的可参考帧,降序排列在开头;大于的,升序排列在后面
  • list1 默认排序,大于当前帧 poc 的可参考帧,升序排列在开头;小于的,降序排列在后面
  • 与 P 帧类似,开始执行 reorder 语义,然后开始正式编码

可见,需要先进行默认排序,然后再进行重排序,重排序不是必须的,而是在默认排序的基础上,进行调整。
关于参考帧默认排序,参考毕厚杰书-7.6 节和 https://www.cnblogs.com/TaigaCon/p/3715276.html。

哪些场景需要重排序?

具体的重排序语义完全依赖于 frame_num,可参考:https://www.cnblogs.com/moonwalk/p/18562007

posted @ 2025-03-26 17:41  重返科韵路  阅读(5)  评论(0)    收藏  举报