🎬 第九章 H.264编解码基础
📄 一句话概括

一段 H.264 裸码流的顺序很朴素:起始码、SPS、PPS、IDR 片、后续片——其中起始码是解码器找回边界的唯一线索,所以数据里绝不允许出现它。

📝 本节和前面几节的关系

9.3.1 给了五级骨架,这一节把它们按顺序摆成一段真实码流。

一、一段码流的顺序

把一段最简单的 H.264 裸码流按顺序写出来: 起始码 0x000001——每个 NAL 单元的分隔标记; SPS——全局参数,整段只要一份; PPS——图像参数,整段只要一份; IDR 片——第一个可独立解码的画面; 非 IDR 片——后续的 P/B 片,要靠前面的帧。 顺序本身没有商量余地:参数必须在用它的数据之前。

二、起始码与【防止竞争】

起始码 0x000001 是这一节最需要留意的一处设计: 解码器在码流里顺序扫描,见到 0x000001 就认为【一个新 NAL 开始了】; 那么如果真正的数据里也出现这串字节,解码器就会误判边界; 所以编码时要做防止竞争(emulation prevention):数据里出现连续两个 0x00 时,插入一个 0x03 把它打断; 解码时再把 0x03 去掉,还原出原始数据——这一步叫去竞争; 所以你看到的裸码流是【变换过】的,不等于原始字节。 这是很多【自己写解析器】的人最先踩的坑。

三、两种边界标记方式

同样是划边界,H.264 有两套写法,用在不同封装里: 起始码方式(Annex B)——用 0x000001 或 0x00000001 分隔,常见于 TS、裸码流、RTP 负载; 长度前缀方式(AVCC)——每个 NAL 前面写一个长度字段,常见于 MP4、MKV 这类带索引的封装; 两者装的是同一批 NAL 单元,只是【分隔方式】不同; 所以把 MP4 里的 H.264 取出来塞进 TS 时,必须做一次格式转换——这就是实际工作中 ffmpeg 的 -bsf h264_mp4toannexb 干的事。

四、为什么转封装有时也要动数据

这一条正好回答了第 5.2.1 节留下的一个疑问:转封装不是【一个字节都不碰】吗? 纯换容器时确实只搬包——因为包的边界信息在容器里已经记好了; 但当两边的边界标记方式不同(MP4 的长度前缀 vs TS 的起始码),就必须重写每个 NAL 的分隔; 这时虽然没碰音频视频的编码内容,但数据确实被改写过了; 所以严谨的说法是:转封装不改编码,但可能改变数据的组织方式。 这也是【转封装比转码快得多、但不是零成本】的原因。

五、看码流的实操顺序

把这一节用到实际排查上,顺序是固定的: 第一步看有没有 SPS/PPS——没有就直接判定文件不可解; 第二步看第一个片是不是 IDR——不是的话拖动会失败; 第三步看类型序列是否规整——乱了说明码流被裁坏了; 用 3.4 节的码流分析器或十六进制工具都能看出这几条。 先看结构、再怀疑内容——这是处理码流问题的通用策略。

六、动手 Demo:一段 H.264 码流长什么样

把 NAL 单元按顺序摆出来,就是一段最朴素的 H.264 码流。

seq = [("起始码 0x000001", "每个 NAL 单元的分隔标记"),
       ("SPS", "全局参数,整段只要一份"),
       ("PPS", "图像参数,整段只要一份"),
       ("IDR 片", "第一个可独立解码的画面"),
       ("非 IDR 片", "后续的 P/B 片,要靠前面的帧")]
for i, (name, note) in enumerate(seq, 1):
    print("%d. %-16s %s" % (i, name, note))

实跑输出:

--------------------------------------------------------------------
一段 H.264 码流长什么样
--------------------------------------------------------------------

把 NAL 单元按顺序摆出来,就是一段最朴素的 H.264 码流。

  顺序     内容                作用
  ----------------------------------------------------------
  1        起始码 0x000001     每个 NAL 单元的分隔标记
  2        SPS                 全局参数,整段只要一份
  3        PPS                 图像参数,整段只要一份
  4        IDR 片              第一个可独立解码的画面
  5        非 IDR 片           后续的 P/B 片,要靠前面的帧

  两个实战要点:
  **一是起始码 0x000001**——解码器靠它找 NAL 边界;
  所以真正的数据里不能出现这个字节序列,编码时要**防止竞争**;
  **二是 SPS/PPS 必须在 IDR 之前**——否则解码器不知道分辨率与熵编码方式,一个宏块也解不了。

起始码 0x000001 是解码器找回边界的唯一线索, 所以真正的数据里不能出现这个字节序列,编码时要防止竞争。 SPS/PPS 必须在 IDR 之前,否则解码器一个宏块也解不了。

30 秒实验

  1. 用 ffmpeg -i in.mp4 -c copy -bsf:v h264_mp4toannexb out.ts 转一次封装。
  2. 注意参数里 -c copy 没有变——变的只是 NAL 的分隔方式。

常见误区

  1. 以为起始码只是【好看的分隔线】。它是解码器找边界的唯一依据。
  2. 以为转封装绝对不会改数据。边界标记方式不同时必须重写分隔。
  3. 以为 SPS/PPS 可以随便挪。它们必须在使用它们的片之前。
自测三问(点开对答案) 1. 一段 H.264 裸码流的基本顺序是什么? —— 起始码、SPS、PPS、IDR 片、后续非 IDR 片。 2. 防止竞争要解决什么问题? —— 数据里出现 0x000001 会被误认为 NAL 边界,需插入 0x03 打断。 3. MP4 与 TS 里的 NAL 分隔方式有何不同? —— MP4 用长度前缀,TS 用起始码;互转时需要重写分隔。

相关笔记