本节摘要:H.264/HEVC 用 NAL 单元切分码流,AV1 用 OBU 切分码流。两者都解决"把压缩数据切成可寻址的单元"这个问题,但 OBU 把自描述性做到了字节级:类型、扩展标志、长度全在头部声明,解析器不需要猜。本节给出两种封装的逐项对照与一段可上手的十六进制验证方法。
NAL(Network Abstraction Layer)的设计初衷是面向网络传输:单元之间用起始码 00 00 01 分隔( Annex-B 格式)或用长度前缀分隔(MP4 封装),单元类型编码在头部的几个比特里。这个设计在流媒体场景工作良好,但也有代价——起始码可能与载荷内容冲突,需要防竞争字节(emulation prevention)兜底;封装格式不同,分隔方式就不同,解析器要适配两套逻辑。
OBU 的思路是把"自描述"进行到底。每个 OBU 头至少一字节,包含类型、扩展标志、是否有长度字段等位级声明;如果声明了带长度,紧跟一个可变长编码的载荷长度。解析逻辑因此只有一条路径:读头、读长、跳载荷,不依赖外部封装格式,也不需要防竞争处理。
| 对照项 | NAL(H.264/HEVC) | OBU(AV1) |
|---|---|---|
| 单元分隔 | 起始码或长度前缀,随封装变化 | 头内声明,封装无关 |
| 类型声明 | 头部 5 位枚举 | 4 位类型 + 扩展位 + 长度标志 |
| 防冲突机制 | 起始码防竞争字节 | 无需(长度自描述) |
| 时间分层 | 头内 temporal_id(扩展) | 扩展头携带 temporal_id/spatial_id |
| 顶层单元示例 | SPS/PPS/切片 | Sequence Header/Frame/Tile Group/Metadata |
概念讲十遍不如看一遍字节。下面是一个真实 AV1 码流开头附近的十六进制片段(示意值,任何 AV1 文件都可以这样分析):
0A 0B 0A 01 08 0C 00 1A 0A ... │ │ │ └─ 载荷长度 = 11(可变长编码) └─ OBU 头 0x0A = 0000 1010 ├─ forbidden_bit = 0 ├─ type = 1 (OBU_SEQUENCE_HEADER) ├─ extension_flag = 0 └─ has_size_field = 1(自描述长度)
用 FFmpeg 的码流探针也能看到同样信息:
# 列出码流中的 OBU 序列与帧信息 ffmpeg -v trace -i output.ivf 2>&1 | findstr /C:"OBU" | more # 或用 ffprobe 确认容器与编解码器识别 ffprobe -v error -show_entries stream=codec_name,profile,pix_fmt output.mkv
对照 H.264 的经验:你会花同样的动作去找 67(SPS)、68(PPS)、65(IDR 切片)这类 NAL 头字节。差别在于 H.264 的 NAL 头只有两个比特的有效语义位加五个类型位,剩余信息要从 SPS/PPS 里"跨单元"拼出来;OBU 则尽量把声明留在本单元内。
AV1 把"一个显示时刻的全部数据"定义为 Temporal Unit(TU),一个 TU 内含时间定界符 OBU、可选的序列头 OBU、一个或多个 Frame OBU 与元数据 OBU。H.264 没有对应的显式概念——一帧的多个切片靠 first_mb_in_slice 等语法元素隐式关联,接入点靠 IDR 帧标记。对照之下 AV1 的显式 TU 带来两个工程红利:
⚠️ 注意一个易错点:OBU 序列头只在首个 TU 强制出现,后续可在分辨率或参数变化时重复。自己写解析器时不要假设"每个 TU 都有序列头",也不要假设"序列头只出现一次"。
把 NAL 与 OBU 的差异收敛成一句话:NAL 把复杂性留给封装与组合,OBU 把复杂性一次性收进标准。前者的好处是 2003 年就能跑在当时的传输设施上,后者的好处是 2018 年可以直接按现代流媒体的需求设计,不必背历史包袱。这不是谁更聪明的问题,而是出生年代的问题——也再次呼应第 1 章的结论:每代标准都是对自身时代约束的回应。
下一节把"说明书"展开:序列头与帧头各自声明哪些元数据,H.264 的 SPS/PPS 在 AV1 里对应什么。
问:实际部署里 AV1 用什么容器装 OBU?
答:WebM(MKV 家族)是天然搭档,MP4 家族的后续版本也定义了 AV1 的封装样本条目。流媒体传输场景里,OBU 序列被切成分段(如 DASH/CMAF 的分片),Temporal Unit 边界与分片边界对齐即可。对照 H.264 时代 Annex-B 与 MP4 两套分隔方式的"双轨烦恼",AV1 的封装适配面窄得多——这正是自描述设计的红利。
问:自己写解析器,最容易在哪个字段上翻车?
答:可变长长度字段(leb128 编码)与"序列头可能重复出现"这两处。前者编码器合法地使用多字节表示小数值,解析器按固定宽度读就出错;后者让状态机不能假设"全局参数只在流头出现一次"。先把这两点处理对,解析器就立住了大半。
问:OBU 会带来额外的封装开销吗?
答:每单元一至两字节的头加长度字段,开销在千分之一量级,对视频码流可忽略。换来的是免起始码扫描、免防竞争处理、容器无关解析——工程账怎么算都划算。