SPS、PPS、VPS、Annex B、AVCC 到底是什么关系

前言:为什么很多人学了 H.264/H.265,还是一碰码流组织就发虚

如果你已经开始接触 H.264 / H.265,很快就会碰到下面这些词:

  • SPS
  • PPS
  • VPS
  • NALU
  • Annex B
  • AVCC
  • extradata

很多人第一次看时都会有一种很强的混乱感。

因为这些词表面上都和“码流”有关,但它们又不在同一个层次:

  • 有的是参数集
  • 有的是码流组织方式
  • 有的是容器里常见的存储形式
  • 有的是解码器初始化依赖的信息

于是工程里就很容易出现这些典型状态:

  • 明明都是 H.264,为什么这个流能解,那个流不能解
  • 为什么 MP4 里的 H.264 和裸流里的 H.264 长得不一样
  • 为什么有时候要把 AVCC 转成 Annex B
  • 为什么某些流缺了 SPS/PPS 就完全起不来
  • 为什么 FFmpeg 里会有 extradata、bitstream filter 这些东西

我觉得这里最容易乱的根本原因是:

很多人把“码流内容”和“码流组织方式”混在了一起。

所以这篇文章的目标,就是把这几件事拆清楚,再重新串起来。

重点讲明白:

  • SPS / PPS / VPS 到底各自是什么
  • 它们和普通视频数据 NALU 是什么关系
  • Annex B 和 AVCC 到底差在哪
  • 为什么 MP4、裸流、网络传输里经常需要做组织方式转换
  • FFmpeg 工程里这些概念会落在哪些位置

而且这篇会尽量多放结构图、时序图和代码,不只停留在术语解释上。


一、先给一句总结:SPS / PPS / VPS 是“参数信息”,Annex B / AVCC 是“码流组织方式”

如果你现在只想先抓住一句最关键的话,那就是:

SPS / PPS / VPS 解决的是“解码器需要先知道什么参数”,而 Annex B / AVCC 解决的是“这些 NALU 在字节流里怎么组织”。

这两类东西看起来总同时出现,但它们不是一层含义。

可以先粗分成:

第一类:参数集

  • SPS
  • PPS
  • VPS(H.265)

它们回答的是:

这段视频流是什么规格、该怎么初始化解码。

第二类:组织方式

  • Annex B
  • AVCC

它们回答的是:

这些 NALU 在字节流里是怎么切分和存放的。

这一步先分开,后面就会顺很多。


二、先把大框架放出来:一条 H.264/H.265 码流里到底有什么

先看一个总图。

flowchart TD
    A[视频码流] --> B[NALU 序列]
    B --> C1[参数集 NALU]
    B --> C2[图像数据 NALU]

    C1 --> D1[SPS]
    C1 --> D2[PPS]
    C1 --> D3[VPS(H265)]

    C2 --> E1[IDR / I 帧相关]
    C2 --> E2[P/B 帧相关 slice]

    B --> F[字节组织方式]
    F --> G1[Annex B]
    F --> G2[AVCC/HVCC]

这张图最重要的作用,是先让你知道:

一条视频码流里,不只有“画面数据”,还有“解释画面该怎么解”的参数信息。

而且这些 NALU 还会以不同方式被组织起来。


三、NALU 是什么,为什么它是理解一切的入口

如果不先理解 NALU,后面很多概念都会悬空。

3.1 NALU 的本质

NALU 可以粗略理解成:

视频码流中的一个基本数据单元。

也就是:

  • 一条 H.264/H.265 码流,并不是一整坨不可分的数据
  • 它内部其实是由一个个 NALU 组成

这些 NALU 里,有些装的是参数,有些装的是图像相关数据。

3.2 为什么 NALU 很重要

因为后面你看到的这些东西,本质上都在围绕 NALU 发生:

  • SPS / PPS / VPS 本身就是某些特定类型的 NALU
  • slice 数据也在 NALU 里
  • Annex B 和 AVCC 组织的,本质上也是 NALU

所以你可以把 NALU 先看成:

视频码流里的“基本块”。


四、SPS 到底是什么,它为什么像“全局说明书”

4.1 SPS 的本质

SPS 全称是 Sequence Parameter Set。

如果不按文档腔去记,我更建议你把它理解成:

一段视频序列的全局规格说明。

它通常会包含这类信息:

  • 分辨率相关信息
  • profile / level
  • 帧编号、参考相关约束
  • 色度格式相关信息
  • 一些解码器需要预先知道的全局参数

4.2 为什么没有 SPS,解码器通常起不来

因为解码器得先知道:

  • 这是个什么规格的视频
  • 宽高大概怎么解释
  • 后面 slice 应该按什么规则理解

如果连这些都不知道,后面的图像数据就很难正确还原。

所以从工程角度看,SPS 很像:

让解码器知道“这道题的题型是什么”的前置条件。


五、PPS 到底是什么,它为什么更像“当前解码规则补充”

5.1 PPS 的本质

PPS 全称是 Picture Parameter Set。

你可以把它理解成:

比 SPS 更贴近具体图像解码控制的一组参数。

它通常和:

  • slice 的解析规则
  • 熵编码模式
  • 某些参考和控制参数

更相关。

5.2 SPS 和 PPS 的关系怎么记

我更建议这样记:

  • SPS 更像全局说明书
  • PPS 更像当前图像解码时的具体规则补充

你不用强行把它背成特别死的标准定义,但一定要知道:

它俩都不是“画面内容本身”,而是在给解码过程提供必要上下文。


六、VPS 又是什么,为什么 H.264 没这个词,H.265 有

6.1 VPS 的本质

VPS 全称是 Video Parameter Set。

它主要出现在 H.265 / HEVC 中。

你可以把它理解成:

比 SPS 更上一层的、面向更高层视频配置组织的参数集。

6.2 为什么 H.265 里会多这一层

因为 H.265 的整体结构和扩展能力比 H.264 更复杂,参数组织层次也更细。

所以从工程理解角度可以先记成:

  • H.264 常说 SPS / PPS
  • H.265 常说 VPS / SPS / PPS

6.3 一句话直觉记忆

可以这样粗记:

  • VPS:更高层视频级参数
  • SPS:序列级参数
  • PPS:图像 / slice 解码控制相关参数

这个层级感先建立起来就够用了。


七、一张图把 VPS / SPS / PPS 的层级感放清楚

flowchart TD
    A[H.265 视频参数层级] --> B[VPS]
    B --> C[SPS]
    C --> D[PPS]
    D --> E[具体 slice / 图像数据]

而如果是 H.264,可以更接近:

flowchart TD
    A[H.264 参数层级] --> B[SPS]
    B --> C[PPS]
    C --> D[具体 slice / 图像数据]

这两张图的目的不是让你死背层级,而是帮助你形成一个理解:

参数集本质上在给图像数据提供“解释上下文”。


八、Annex B 到底是什么,为什么很多裸流都长这个样子

接下来进入另一个非常高频的坑点:

Annex B。

8.1 Annex B 的本质

Annex B 可以理解成:

一种以起始码分隔 NALU 的字节流组织方式。

也就是说,在 Annex B 里,一个 NALU 的边界通常靠起始码来表示,比如:

1
00 00 01

或者:

1
00 00 00 01

8.2 为什么它常见于裸流

因为它非常适合“顺着字节流往前扫”的场景。

例如:

  • 裸 H.264 / H.265 文件
  • 某些传输场景
  • 某些编码器直接吐出的数据

8.3 一张示意图看懂 Annex B

1
[start code][NALU1][start code][NALU2][start code][NALU3]...

所以你以后看到 Annex B,可以先直接记成:

靠起始码找 NALU 边界。


九、AVCC 到底是什么,为什么 MP4 里常见的是这种组织方式

9.1 AVCC 的本质

AVCC 可以理解成:

一种用长度前缀来表示 NALU 边界的组织方式。

也就是说,和 Annex B 不一样,它不是靠起始码,而是靠“前面先写长度”。

示意上更像:

1
[length][NALU1][length][NALU2][length][NALU3]...

9.2 为什么 MP4 里常见的是 AVCC

因为在 MP4 这类容器里,长度前缀方式通常更适合封装和索引管理。

同时,参数集(比如 SPS/PPS)往往不会每次都直接混在每个包里,而常常被单独提取到更适合初始化解码器的位置,比如:

  • 容器头信息
  • codec extradata

所以你在 MP4 里看到的 H.264,通常和裸流里的 H.264 长得不一样。

这不是 codec 变了,而是:

组织方式变了。


十、一张图把 Annex B 和 AVCC 的差别一次讲清楚

flowchart LR
    A[NALU 序列] --> B1[Annex B]
    A --> B2[AVCC]

    B1 --> C1[起始码分隔]
    B1 --> C2[常见于裸流 / 某些传输链路]

    B2 --> D1[长度前缀分隔]
    B2 --> D2[常见于 MP4 / 容器组织]

你可以直接把这张图压成一句话:

  • Annex B:看起始码找边界
  • AVCC:看长度前缀找边界

这已经能解决大部分第一层理解问题。


十一、SPS / PPS / VPS 和 Annex B / AVCC 是怎么交叉在一起的

这部分非常关键。

因为很多人学到这里最大的困惑就是:

参数集和组织方式到底是怎么同时存在的?

答案是:

参数集本身也是 NALU,而 Annex B / AVCC 决定这些 NALU 怎么被排进字节流。

换句话说:

  • SPS/PPS/VPS 是“内容类型”
  • Annex B/AVCC 是“排布方式”

所以它们不是互斥关系,而是两层叠在一起。

11.1 一张交叉关系图

flowchart TD
    A[NALU] --> B1[参数集 NALU]
    A --> B2[图像数据 NALU]

    B1 --> C1[SPS/PPS/VPS]
    B2 --> C2[Slice / IDR / 非IDR]

    A --> D[组织方式]
    D --> E1[Annex B]
    D --> E2[AVCC]

这张图一旦立住,后面很多事就容易理解了。


十二、为什么 MP4 里的 H.264/H.265 和裸流里的长得不一样

这个问题特别高频。

12.1 裸流里常见什么

裸流中,常见的是:

  • Annex B 组织方式
  • 参数集可能直接以内联 NALU 形式出现在码流里

12.2 MP4 里常见什么

MP4 里常见的是:

  • AVCC(H.264)或 HVCC(H.265)形式
  • 参数集常常被提取到 extradata / 容器头信息中
  • 每个 packet 里主要放具体图像相关 NALU

所以看起来会像:

  • 同样是 H.264
  • 但字节布局完全不一样

这就是为什么你经常会遇到:

  • 裸流能直接喂某些解码器
  • MP4 拆出来的 packet 却还要进一步处理

十三、FFmpeg 工程里这些东西会落在哪些地方

这部分最实用。

13.1 extradata 是什么

FFmpeg 里,extradata 很多时候就是在保存 codec 初始化所需的重要参数信息。

比如:

  • H.264 的 SPS / PPS
  • H.265 的 VPS / SPS / PPS

你可以先粗记成:

extradata 是给解码器“先打底”的那部分信息。

13.2 为什么 demux 出来的 packet 不一定能直接送解码器

因为:

  • 容器里可能是 AVCC/HVCC 风格
  • 解码器或某些后续链路可能更希望看到 Annex B 风格
  • 参数集可能在 extradata 里,而不在当前 packet 数据里

这时候就经常需要:

  • parser
  • bitstream filter
  • 组织方式转换

13.3 一张 FFmpeg 里的关系图

flowchart LR
    A[MP4/FLV/TS] --> B[Demux]
    B --> C[AVPacket]
    B --> D[extradata]
    D --> E[SPS/PPS/VPS]
    C --> F[AVCC/HVCC or Annex B]
    F --> G[必要时做 bitstream 转换]
    G --> H[送解码器]

这张图的核心是:

同一个 codec,在不同容器里出来的 packet 组织方式可能不同,而解码器初始化依赖的信息又可能在 extradata 里。


十四、为什么工程里经常要做 Annex B 和 AVCC 的转换

因为上下游预期不一致。

14.1 一个典型场景

比如:

  • 输入是 MP4
  • demux 出来是 AVCC 风格
  • 但后续 mux 到 TS 或某些网络链路时,更需要 Annex B 风格

这时就需要转换。

14.2 再比如

  • 某个编码器直接输出 Annex B
  • 但你要写进 MP4
  • MP4 更倾向于 AVCC/HVCC 组织方式

也要转换。

所以很多时候问题根本不是:

codec 对不对

而是:

码流组织方式对不对

这也是这篇文章最想帮你建立的工程意识之一。


十五、一段 FFmpeg 代码,怎么看 codec extradata

这段代码很有代表性。

1
2
3
4
5
6
7
8
9
10
AVCodecParameters *par = stream->codecpar;
printf("codec id: %d\n", par->codec_id);
printf("extradata size: %d\n", par->extradata_size);

if (par->extradata && par->extradata_size > 0) {
for (int i = 0; i < par->extradata_size && i < 32; ++i) {
printf("%02X ", par->extradata[i]);
}
printf("\n");
}

这段代码本身不复杂,但它的工程意义很大:

你要养成习惯,知道参数集信息未必在当前 packet 数据里,也可能提前放在 extradata 里。


十六、一段 FFmpeg 代码,怎么做 H.264 MP4 到 Annex B 的常见转换

FFmpeg 里常见会用到 bitstream filter,比如 h264_mp4toannexb

1
2
3
4
5
6
7
8
9
10
11
const AVBitStreamFilter *filter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;

av_bsf_alloc(filter, &bsf_ctx);
avcodec_parameters_copy(bsf_ctx->par_in, stream->codecpar);
av_bsf_init(bsf_ctx);

av_bsf_send_packet(bsf_ctx, pkt);
while (av_bsf_receive_packet(bsf_ctx, pkt) == 0) {
// 这里拿到的通常就是 Annex B 风格 packet
}

这段代码后面站着的实际问题就是:

  • 输入组织方式和输出需求不一致
  • 需要把 AVCC 风格转成 Annex B 风格

同理 H.265 也有对应的 bitstream filter。


十七、一张时序图看懂“容器 -> 解码器”之间为什么老要折腾一下

sequenceDiagram
    participant File as MP4文件
    participant Demux as Demux
    participant Extra as extradata
    participant BSF as Bitstream Filter
    participant Dec as Decoder

    File->>Demux: 读容器数据
    Demux->>Extra: 提取 SPS/PPS/VPS
    Demux->>BSF: 输出 AVCC/HVCC 风格 packet
    BSF->>Dec: 转成解码器期望的 Annex B / 合法输入
    Extra->>Dec: 初始化解码参数

这张时序图非常重要,因为它解释了一个工程现实:

解码前的工作,不只是“把 packet 喂进去”,而是先把初始化参数和组织方式都准备对。


十八、工程里最常见的几个坑

18.1 误区一:SPS/PPS/VPS 就是普通视频帧的一部分

不对。

它们也是 NALU,但它们承载的是参数信息,不是普通画面内容本身。

18.2 误区二:Annex B 和 AVCC 是两种不同编码格式

也不对。

它们不是不同 codec,而是同一 codec 下不同的字节流组织方式。

18.3 误区三:只要 codec 是 H.264,就一定能直接喂任何下游

不一定。

你还得看:

  • 参数集有没有准备好
  • 组织方式对不对
  • 上下游预期是不是一致

18.4 误区四:解码失败一定是编码器问题

也不一定。

工程里很多“解不出来”的问题,根因其实是:

  • SPS/PPS/VPS 缺失
  • AVCC / Annex B 混用
  • extradata 没传对
  • packet 边界没处理对

十九、面试里怎么讲,才不像只会背几个缩写

如果面试官问:

SPS、PPS、VPS、Annex B、AVCC 你怎么理解?

我建议你按下面这个结构讲。

19.1 先分层

SPS、PPS、VPS 是参数集,用来给解码器提供必要初始化和解码上下文;Annex B 和 AVCC 则是码流中 NALU 的两种常见组织方式。

19.2 再讲关系

参数集本身也是 NALU,而 Annex B / AVCC 决定这些 NALU 在字节流里如何分隔和存储。Annex B 依赖起始码,AVCC 依赖长度前缀。

19.3 最后讲工程意义

在工程里,裸流、MP4、TS、网络流等场景中,这些组织方式可能不同,所以经常需要借助 extradata、parser 或 bitstream filter 做适配,否则即使 codec 本身是一样的,也可能出现解码失败或上下游不兼容。

如果你能这么讲,基本就不是“我背过这些缩写”,而是真的知道这些东西在系统里各自负责什么。


总结:真正要分清的,不是几个缩写,而是“参数信息”和“组织方式”这两层

回到这篇文章最开始的问题:

SPS、PPS、VPS、Annex B、AVCC 到底是什么关系?

我觉得最稳的主线就是:

  • 一条 H.264/H.265 码流是由 NALU 组成的
  • 其中有些 NALU 装的是参数集(SPS/PPS/VPS)
  • 有些 NALU 装的是图像相关数据
  • Annex B 和 AVCC 不是参数内容,而是这些 NALU 的组织方式
  • FFmpeg 工程里,extradata、bitstream filter、demux / mux 都会围绕这两层做适配

只要这条线建立起来,后面你再看:

  • 裸流和 MP4 的差异
  • H.264 / H.265 packet 为什么长得不一样
  • 为什么某些流缺 SPS/PPS 就起不来
  • 为什么 FFmpeg 里老要 mp4toannexb

这些问题都会自然很多。

因为到那时你已经不再只是记住几个缩写,而是真的知道:

哪些是在告诉解码器“这是什么视频”,哪些是在告诉系统“这些 NALU 怎么排”。


SPS、PPS、VPS、Annex B、AVCC 到底是什么关系
https://breaker505.github.io/2026/04/07/sps-pps-vps-annexb-avcc-relationships/
作者
爱发呆的鱼
发布于
2026年4月7日
许可协议