H.265 码流结构
好的,我们来详细解析HEVC码流(Bitstream)的层次结构,并深入辨析VPS、SPS和PPS这三个关键参数集之间的区别。
作为一名C++开发工程师,你可以将HEVC码流结构理解为一个精心设计的数据结构,它通过层次化的信令来高效地组织和传输视频数据。
1. HEVC码流的基本构成:NAL单元 (NAL Unit)
整个HEVC码流是由一个个独立的网络抽象层单元(Network Abstraction Layer Unit, NAL Unit)组成的序列。每个NAL Unit都是一个数据包,包含一个NAL头(NAL Header)和一个原始字节序列载荷(Raw Byte Sequence Payload, RBSP)。
NAL头(通常1-2个字节)至关重要,它定义了RBSP中数据的类型。NAL单元主要分为两大类:
- VCL (Video Coding Layer) NAL单元:包含实际的视频像素压缩数据,即切片(Slice)数据。这些是码流的主体,承载着图像信息。
- Non-VCL NAL单元:不包含像素数据,而是承载着解码VCL数据所需的元数据(Metadata)或参数信息。我们重点讨论的VPS、SPS、PPS就属于这一类。
2. 参数集金字塔:VPS, SPS, PPS
HEVC设计了一套金字塔式的参数集结构,从最宏观的视频全局信息,到单个序列、单个图像,再到图像的某个部分,层层递进。这种分层设计极大地提高了信令的效率,因为大部分不常变化的信息只需要在码流的开头发送一次。
ASCII图表:参数集层次关系
代码段
                      +-----------------------------+
                      |    VPS (Video Param Set)    |  <-- 视频全局参数 (最高层)
                      | (管理一个或多个序列)        |
                      +--------------+--------------+
                                     |
                +--------------------+--------------------+
                |                                         |
      +---------v----------+                    +---------v----------+
      |  SPS_0 (Seq Param Set) |                    |  SPS_1 (Seq Param Set) |  <-- 序列级参数
      | (e.g., 1920x1080)    |                    | (e.g., 1280x720)     |
      +---------+----------+                    +--------------------+
                |
    +-----------+------------+
    |                        |
+---v-----+            +---v-----+
| PPS_0   |            | PPS_1   |  <-- 图像级参数
| (WPP on)|            |(WPP off)|
+---+-----+            +---+-----+
    |                      |
    |  +-------------------+
    |  |
+---v--v---+  +--------+  +--------+
| Slice_A  |  | Slice_B|  | Slice_C|  <-- 切片级数据 (VCL)
+----------+  +--------+  +--------+
(使用PPS_0)   (使用PPS_1)  (使用PPS_0)
下面我们来详细解释每一层的作用和区别。
2.1. VPS (Video Parameter Set) - 视频参数集
- 层级: 最高层,管理整个视频序列集合。这是HEVC相对于H.264新增的概念。
- 作用域: 作用于一个或多个视频序列(CVS, Coded Video Sequence)。
- 核心功能:
    - 多层扩展支持: VPS的主要设计目的是为了支持多层视频编码,例如可伸缩性视频编码(SVC)、多视角视频编码(MV-HEVC)等。它定义了视频流包含多少层、层与层之间的依赖关系、每层使用哪个SPS等宏观信息。
- 全局信令: 提供一个全局的入口点,解码器首先寻找并解析VPS,然后根据VPS的指引去寻找它需要的SPS。
 
- 通俗理解: 在一个简单的单层视频流中,VPS的作用就像一个“项目清单”,它告诉你:“这个视频项目只有一个序列,它的具体配置请参考ID为0的SPS文件。” 对于复杂的流,它会说:“这个项目包含一个基础层和一个增强层,基础层请参考SPS_0,增强层请参考SPS_1。”
2.2. SPS (Sequence Parameter Set) - 序列参数集
- 层级: 第二层,在VPS之下。功能上与H.264的SPS类似。
- 作用域: 作用于一个完整的编码视频序列(CVS)。只要这些参数不变,后续的图像都属于同一个序列。
- 核心功能: 定义了一个视频序列中长期不变的核心参数。一旦这些参数需要改变(例如,分辨率变化),就必须启用一个新的SPS,开始一个新的序列。
- 包含的关键信息:
    - Profile, Level, Tier: 描述码流的档次、级别和层级,限定解码复杂度。
- 图像分辨率 (Pic_width, Pic_height)。
- 色度格式 (Chroma_format_idc): 如4:2:0, 4:2:2等。
- 像素位深 (Bit_depth): 如8-bit, 10-bit。
- 最大参考帧数量 (Max ref frames)。
- CTU的最大尺寸等。
 
- 通俗理解: SPS就像一个项目的“核心配置文件”。它规定了这个项目(视频序列)的基础框架,比如分辨率是1080p,色彩空间是4:2:0,这些都是轻易不会改变的。
2.3. PPS (Picture Parameter Set) - 图像参数集
- 层级: 第三层,在SPS之下。功能上与H.264的PPS类似。
- 作用域: 作用于一个序列中的一张或多张图像。
- 核心功能: 定义了那些可能在图像之间发生变化的编码工具和参数。一个SPS可以被多个PPS引用,这允许编码器在不改变视频基本属性(如分辨率)的前提下,灵活地为不同的图像(或图像类型)选择不同的编码策略。
- 包含的关键信息:
    - 并行处理开关: 是否启用Tiles、是否启用WPP。
- 环路滤波控制: 是否启用SAO、去块滤波的控制参数(如deblocking_filter_override_enabled_flag)。
- 初始量化参数 (Initial QP)。
- 量化矩阵 (Scaling Lists) 的使用开关及数据。
- 依赖的SPS ID: 指明当前PPS是基于哪个SPS的配置。
 
- 通俗理解: PPS就像是项目的“构建配置”(例如Debug vs. Release)。在同一个项目框架下(SPS),你可以为某些图像选择一种“高性能”配置(如关闭WPP以追求极致压缩率),为另一些图像选择“高效率”配置(如开启WPP以并行处理)。
3. 总结与类比
| 参数集 | 作用域 | 核心信息 | C++项目开发类比 | 
|---|---|---|---|
| VPS | 整个视频(可含多层/多序列) | 多层/多视角信息、SPS索引 | 解决方案文件 (.sln): 管理一个或多个项目。 | 
| SPS | 一个视频序列(分辨率等不变) | 图像分辨率、Profile/Level、位深 | 项目文件 (.vcxproj): 定义项目的核心属性(目标平台、字符集等)。 | 
| PPS | 一个序列中的一张或多张图像 | 编码工具开关(WPP/Tiles/SAO)、QP、量化矩阵 | 项目配置 (Debug/Release): 定义编译选项、优化开关等。 | 
| Slice Header | 图像的一个切片(VCL) | 切片类型(I/P/B)、实际QP、参考帧列表 | 源文件 (.cpp): 在某个项目配置下被实际编译的代码单元。 | 
导出到 Google 表格
4. 典型的码流结构示例
一个典型的直播或点播码流,其NAL单元的顺序通常是这样的:
[VPS] [SPS] [PPS] [IDR Slice] [P Slice] [P Slice] [B Slice] ...
- 解码器工作流程:
    - 首先接收并解析VPS、SPS、PPS。这些参数集被存储起来。
- 当接收到一个Slice NAL单元时,解码器解析其Slice Header。
- Slice Header中会包含一个pps_id,解码器用这个ID找到并激活对应的PPS。
- 被激活的PPS中又包含了sps_id,解码器再用这个ID找到并激活对应的SPS。
- 只有在所有需要的参数集都激活后,解码器才能正确地解码这个Slice的像素数据。
 
这种设计使得在传输过程中,参数集可以被周期性地重复发送,以增强鲁棒性(例如,允许用户中途加入直播流也能快速获得解码所需参数)。