什么是PPS?
PPS 是 H.264 码流中的一种特定 NAL 单元(NAL Unit),其主要任务是提供与一幅或多幅图像 相关的参数信息。与 SPS 负责整个视频序列的全局配置不同,PPS 的作用范围相对较小,通常用于一个或多个 GOP(Group of Pictures)。这种层次化的参数管理机制,使得 H.264 具有极高的灵活性和鲁棒性。
- 层级管理: SPS 位于最高层级,定义了整个视频序列的宏观参数,如 profile、level、图像尺寸等。PPS 位于其下,定义了帧级的具体参数,如熵编码模式、量化参数、环路滤波等。
- 共享参数: PPS 的核心思想是参数共享。对于一个 GOP 中的所有图像,如果它们共享相同的解码参数,那么这些参数只需在 PPS 中定义一次,而不需要在每一帧的切片头(slice header)中重复携带。这大大减少了码流的开销,提高了编码效率。
- 动态更新: 虽然一个 PPS 可能适用于多个图像,但如果某些参数需要在编码过程中发生变化(例如,量化参数 Qp 的调整),编码器可以插入一个新的 PPS,从而实现参数的动态更新。这为编码器提供了精细控制码流的能力,以适应不同场景的需求。
PPS结构体
c++
// H.264 Picture Parameter Set (PPS)
typedef struct H264PPS {
int pic_parameter_set_id; // PPS 的 ID,范围通常 0~255,对应到具体的一组 PPS 参数
int seq_parameter_set_id; // 引用的 SPS ID,表示此 PPS 属于哪个 SPS
int entropy_coding_mode_flag; // 0 = CAVLC (Context-Adaptive VLC),1 = CABAC (Context-Adaptive Binary Arithmetic Coding)
int bottom_field_pic_order_in_frame_present_flag; // 帧内是否包含底场的 POC(Picture Order Count)
int num_slice_groups_minus1; // 切片组数量 - 1,=0 表示只有一个 slice group
int slice_group_map_type; // 切片组映射类型(决定分片方式)
int num_ref_idx_l0_default_active_minus1; // 默认参考帧列表 L0 的参考帧个数 - 1
int num_ref_idx_l1_default_active_minus1; // 默认参考帧列表 L1 的参考帧个数 - 1
int weighted_pred_flag; // P 帧是否启用加权预测
int weighted_bipred_idc; // B 帧加权预测模式:0=禁用,1=启用,2=隐式
int pic_init_qp_minus26; // 初始 QP(相对值),实际 QP = 26 + 该值
int pic_init_qs_minus26; // 初始 QS(用于 SP/SI slice)
int chroma_qp_index_offset; // 色度分量 QP 偏移量
int second_chroma_qp_index_offset; // 第二个色度分量 QP 偏移量(通常等于 chroma_qp_index_offset)
int deblocking_filter_control_present_flag; // 是否支持去块滤波控制
int constrained_intra_pred_flag; // 是否强制约束帧内预测(防止跨块预测)
int redundant_pic_cnt_present_flag; // 是否允许冗余片
// 扩展字段(可选)
int transform_8x8_mode_flag; // 是否启用 8x8 DCT 变换
int pic_scaling_matrix_present_flag;// 是否有缩放矩阵
int scaling_list_4x4[6][16]; // 4x4 变换缩放矩阵
int scaling_list_8x8[2][64]; // 8x8 变换缩放矩阵
} H264PPS;
字段说明(核心)
- pic_parameter_set_id
唯一标识 PPS,在解码时通过该 ID 找到对应的 PPS 参数。 - seq_parameter_set_id
表示该 PPS 依赖的 SPS(序列参数集),解码器需要先解析 SPS。 - entropy_coding_mode_flag
指定熵编码方式:0
→ CAVLC(简单一些,解码器必须支持),CAVLC 是一种基于变长码表的熵编码方法,计算量小,适用于 Baseline 和 Main Profile;1
→ CABAC(压缩率更高,但解码复杂),CABAC 是一种基于上下文的二进制算术编码,其压缩效率更高,但计算复杂度也更高,适用于 High Profile。
- num_slice_groups_minus1 / slice_group_map_type
用于 FMO(Flexible Macroblock Ordering),决定宏块分组方式。大部分码流设置为0
,即不使用 FMO。 - num_ref_idx_l0_default_active_minus1 / num_ref_idx_l1_default_active_minus1
定义 P/B 帧的默认参考帧数,影响解码器参考帧列表。 - weighted_pred_flag / weighted_bipred_idc
加权预测相关,主要用于提高 B/P 帧的压缩效率。 - pic_init_qp_minus26 / pic_init_qs_minus26 / chroma_qp_index_offset
量化参数设置:- QP(亮度量化参数);
- QS(SI/SP slice 使用的量化参数);
- Chroma 偏移(色度分量的量化偏移)。
- deblocking_filter_control_present_flag
是否开启去块滤波(Deblocking filter)。 - constrained_intra_pred_flag
是否限制帧内预测,通常用于误码鲁棒性。 - redundant_pic_cnt_present_flag
是否允许冗余切片,用于容错或错误恢复。 - transform_8x8_mode_flag / scaling_matrix
H.264 High Profile 扩展字段,是否启用 8x8 变换,以及自定义缩放矩阵。
PPS的编解码
编码端:
- 选择参数: 编码器根据用户配置和视频内容,选择合适的编码参数,如熵编码模式、量化参数等。
- 生成 SPS: 首先生成并发送 SPS NAL 单元,定义整个视频序列的全局参数。
- 生成 PPS: 随后生成并发送 PPS NAL 单元。PPS 内部包含了与帧级参数相关的字段,并引用了之前生成的 SPS。
- 编码视频数据: 编码器将视频帧编码成切片,并在每个切片头中插入
pic_parameter_set_id
,指向之前生成的 PPS。 - 动态更新: 如果编码参数发生变化,编码器会生成并发送一个新的 PPS,然后后续的切片将引用新的 PPS。
解码端:
- 接收码流: 解码器接收 H.264 码流。
- 解析 SPS 和 PPS: 解码器首先解析并缓存 SPS 和 PPS NAL 单元。SPS 和 PPS 通常会在码流的起始位置发送,或者在 seek(跳转)操作后重新发送。
- 解析切片: 当解码器接收到一个切片 NAL 单元时,它会首先解析切片头。
- 查找参数: 从切片头中提取
pic_parameter_set_id
,然后到缓存中查找对应的 PPS。 - 获取完整参数集: 通过 PPS 内部的
seq_parameter_set_id
,解码器找到对应的 SPS。此时,解码器就拥有了完整的、用于解码当前切片的参数集。 - 解码图像: 结合这些参数,解码器对切片数据进行熵解码、反量化、反变换、运动补偿和环路滤波等操作,最终重建出视频帧。
使用示例(c++)
c++
#include <cstdint>
#include <vector>
#include <stdexcept>
#include <cstring>
#include <iostream>
#include <array>
// ------------------------- BitReader (with emulation prevention removal) -------------------------
class BitReader {
public:
// 构造:传入 NAL RBSP(含 emulation prevention bytes 0x03)
BitReader(const std::vector<uint8_t>& nal_rbsp) {
buffer = removeEmulationPrevention(nal_rbsp);
bit_pos = 0;
}
// 读单个比特
uint32_t readBit() {
if (bit_pos >= buffer.size() * 8) throw std::out_of_range("readBit out of range");
size_t byte_idx = bit_pos / 8;
int offset = 7 - (bit_pos % 8);
uint8_t bit = (buffer[byte_idx] >> offset) & 0x01u;
++bit_pos;
return bit;
}
// 读 n bits(n <= 32)
uint32_t readBits(int n) {
if (n == 0) return 0;
if (n < 0 || n > 32) throw std::invalid_argument("readBits n invalid");
uint32_t v = 0;
for (int i = 0; i < n; ++i) {
v = (v << 1) | readBit();
}
return v;
}
// 读 ue(v) 无符号 Exp-Golomb
uint32_t readUE() {
int leadingZeroBits = -1;
for (uint32_t b = 0; b == 0; ++leadingZeroBits) {
b = readBit();
if (bit_pos > buffer.size() * 8 + 32) throw std::out_of_range("readUE overflow");
if (b == 1) break;
}
if (leadingZeroBits < 0) return 0;
uint32_t codeNum = (1u << leadingZeroBits) - 1 + readBits(leadingZeroBits);
return codeNum;
}
// 读 se(v) 有符号 Exp-Golomb
int32_t readSE() {
uint32_t codeNum = readUE();
int32_t val = (codeNum & 1u) ? (int32_t)((codeNum + 1) / 2) : -(int32_t)(codeNum / 2);
return val;
}
// 读 rbsp_byte_aligned 的剩余零位(用于结束对齐)
void rbspTrailingBitsAlign() {
// 读停止位 1,然后跳过剩余 0
readBit(); // should be 1
while (bit_pos % 8) {
uint32_t v = readBit();
if (v != 0 && bit_pos % 8 != 0) {
// 非 0 的填充,可能是 malformed
}
}
}
size_t bitsLeft() const {
return buffer.size() * 8 - bit_pos;
}
private:
std::vector<uint8_t> buffer;
size_t bit_pos;
// 删除 emulation prevention bytes (0x00 0x00 0x03)
static std::vector<uint8_t> removeEmulationPrevention(const std::vector<uint8_t>& src) {
std::vector<uint8_t> dst;
dst.reserve(src.size());
for (size_t i = 0; i < src.size(); ++i) {
// if pattern 0x00 0x00 0x03 -> skip 0x03
if (i + 2 < src.size() && src[i] == 0x00 && src[i + 1] == 0x00 && src[i + 2] == 0x03) {
dst.push_back(0x00);
dst.push_back(0x00);
i += 2; // 下一轮 i++ 会跳到 0x03 的后面
continue;
}
// also accomodate when 0x00 0x00 0x03 at the end partial matches - safe to just copy
dst.push_back(src[i]);
}
return dst;
}
};
// ------------------------- PPS 数据结构(覆盖常用字段) -------------------------
struct H264PPS {
uint32_t pic_parameter_set_id = 0;
uint32_t seq_parameter_set_id = 0;
bool entropy_coding_mode_flag = false;
bool bottom_field_pic_order_in_frame_present_flag = false;
uint32_t num_slice_groups_minus1 = 0;
uint32_t slice_group_map_type = 0;
// 针对 slice group map type 的可选字段我们会按需解析,但不在结构体展开太多
uint32_t num_ref_idx_l0_default_active_minus1 = 0;
uint32_t num_ref_idx_l1_default_active_minus1 = 0;
bool weighted_pred_flag = false;
uint8_t weighted_bipred_idc = 0;
int32_t pic_init_qp_minus26 = 0;
int32_t pic_init_qs_minus26 = 0;
int32_t chroma_qp_index_offset = 0;
int32_t second_chroma_qp_index_offset = 0;
bool deblocking_filter_control_present_flag = false;
bool constrained_intra_pred_flag = false;
bool redundant_pic_cnt_present_flag = false;
// 高级/可选
bool transform_8x8_mode_flag = false;
bool pic_scaling_matrix_present_flag = false;
// scaling lists: 6 * 4x4, 2 * 8x8 (按规范)
std::array<std::array<uint8_t, 16>, 6> scaling_list_4x4{};
std::array<std::array<uint8_t, 64>, 2> scaling_list_8x8{};
bool scaling_list_4x4_present[6] = {false,false,false,false,false,false};
bool scaling_list_8x8_present[2] = {false,false};
// 保留原始字段便于调试
std::vector<uint8_t> raw_rbsp;
};
// ------------------------- 解析 scaling list 的辅助函数 -------------------------
static void parseScalingList(BitReader& br, uint8_t *dst, int size, bool &presentFlag) {
// size 是元素数量(16 或 64)
// scaling_list() 参考标准:初始化 lastScale = 8, nextScale = 8;读取 deltaScale(se(v)),计算 nextScale = (lastScale + deltaScale + 256) % 256
// 如果 nextScale != 0, scale[i] = nextScale; else scale[i] = lastScale
presentFlag = true;
int lastScale = 8;
int nextScale = 8;
for (int j = 0; j < size; ++j) {
if (nextScale != 0) {
int32_t deltaScale = br.readSE();
nextScale = (lastScale + deltaScale + 256) % 256;
}
if (nextScale == 0) {
dst[j] = (uint8_t)lastScale;
} else {
dst[j] = (uint8_t)nextScale;
lastScale = nextScale;
}
}
}
// ------------------------- parsePPS 主函数 -------------------------
H264PPS parsePPS(const std::vector<uint8_t>& nal_rbsp_payload) {
BitReader br(nal_rbsp_payload);
H264PPS pps;
pps.raw_rbsp = nal_rbsp_payload;
pps.pic_parameter_set_id = br.readUE();
pps.seq_parameter_set_id = br.readUE();
pps.entropy_coding_mode_flag = br.readBit();
pps.bottom_field_pic_order_in_frame_present_flag = br.readBit();
pps.num_slice_groups_minus1 = br.readUE();
if (pps.num_slice_groups_minus1 > 0) {
pps.slice_group_map_type = br.readUE();
if (pps.slice_group_map_type == 0) {
for (uint32_t iGroup = 0; iGroup <= pps.num_slice_groups_minus1; ++iGroup) {
// run_length_minus1[iGroup]
(void)br.readUE();
}
} else if (pps.slice_group_map_type == 2) {
for (uint32_t iGroup = 0; iGroup <= pps.num_slice_groups_minus1; ++iGroup) {
// top_left, bottom_right
(void)br.readUE();
(void)br.readUE();
}
} else if (pps.slice_group_map_type == 3 ||
pps.slice_group_map_type == 4 ||
pps.slice_group_map_type == 5) {
// slice_group_change_direction_flag + slice_group_change_rate_minus1
(void)br.readBit();
(void)br.readUE();
} else if (pps.slice_group_map_type == 6) {
uint32_t pic_size_in_map_units_minus1 = br.readUE();
for (uint32_t i = 0; i <= pic_size_in_map_units_minus1; ++i) {
// slice_group_id[i]
// 读的比特数由 num_slice_groups_minus1 决定
int bits = 0;
uint32_t n = pps.num_slice_groups_minus1 + 1;
while ((1u << bits) < n) bits++;
if (bits > 0) (void)br.readBits(bits);
}
}
}
pps.num_ref_idx_l0_default_active_minus1 = br.readUE();
pps.num_ref_idx_l1_default_active_minus1 = br.readUE();
pps.weighted_pred_flag = br.readBit();
pps.weighted_bipred_idc = (uint8_t)br.readBits(2);
pps.pic_init_qp_minus26 = br.readSE();
pps.pic_init_qs_minus26 = br.readSE();
pps.chroma_qp_index_offset = br.readSE();
pps.second_chroma_qp_index_offset = br.readSE();
pps.deblocking_filter_control_present_flag = br.readBit();
pps.constrained_intra_pred_flag = br.readBit();
pps.redundant_pic_cnt_present_flag = br.readBit();
// high profile / future params: transform_8x8_mode_flag, pic_scaling_matrix_present_flag ...
// 这些字段在 PPS(rbsp) 的后续位置,当 slice_group... 等都解析完成后出现(按规范)
// 解析可选的扩展字段(存在于大部分编码器生成的 PPS)
if (br.bitsLeft() > 0) {
// Check if next bits correspond to more data (guarded)
// transform_8x8_mode_flag 可能出现在后面,仅当存在相应的位时读取(依据规范是紧跟一些 profile/sps 信息)
try {
// 尝试读取 transform_8x8_mode_flag(如果有)
pps.transform_8x8_mode_flag = br.readBit();
pps.pic_scaling_matrix_present_flag = br.readBit();
if (pps.pic_scaling_matrix_present_flag) {
// parse pic_scaling_list_present_flag for 6 + (2 if transform8x8) lists
for (int i = 0; i < 6; ++i) {
bool present = br.readBit();
if (present) {
parseScalingList(br, pps.scaling_list_4x4[i].data(), 16, pps.scaling_list_4x4_present[i]);
}
}
if (pps.transform_8x8_mode_flag) {
for (int i = 0; i < 2; ++i) {
bool present = br.readBit();
if (present) {
parseScalingList(br, pps.scaling_list_8x8[i].data(), 64, pps.scaling_list_8x8_present[i]);
}
}
}
}
} catch (...) {
// 如果读取失败(比特不够),忽略扩展字段(很多流不会包含)
}
}
// 最后 rbsp_trailing_bits (对齐)
//br.rbspTrailingBitsAlign();
return pps;
}
// ------------------------- 示例 DecoderContext 与 applyPPSToDecoder(伪代码风格) -------------------------
struct DecoderContext {
bool useCABAC = false;
bool deblockingEnabled = true;
int chromaQPOffset[2] = {0,0};
int initQP = 26;
bool transform8x8 = false;
// ... 其他上下文(参考帧列表、slice config 等)
};
void applyPPSToDecoder(const H264PPS& pps, DecoderContext& ctx) {
// entropy coding
ctx.useCABAC = pps.entropy_coding_mode_flag;
// initial QP
ctx.initQP = 26 + pps.pic_init_qp_minus26;
// chroma QP offsets
ctx.chromaQPOffset[0] = pps.chroma_qp_index_offset;
ctx.chromaQPOffset[1] = pps.second_chroma_qp_index_offset;
// deblocking
ctx.deblockingEnabled = pps.deblocking_filter_control_present_flag;
// transform 8x8
ctx.transform8x8 = pps.transform_8x8_mode_flag;
// 加权预测,参考数等对参考管理和运动补偿模块有影响(示例性 log)
if (pps.weighted_pred_flag) {
// 在 P slice 处理里要启用加权预测代码路径
std::clog << "[PPS] weighted_pred_flag = 1 (enable weighted pred for P)\n";
}
if (pps.weighted_bipred_idc) {
std::clog << "[PPS] weighted_bipred_idc = " << (int)pps.weighted_bipred_idc << "\n";
}
// scaling lists:如果提供,则用于重建逆量化矩阵(这里仅标记存在)
for (int i = 0; i < 6; ++i) {
if (pps.scaling_list_4x4_present[i]) {
std::clog << "[PPS] scaling_list_4x4 present for idx " << i << "\n";
// 你需把 pps.scaling_list_4x4[i] 转为解码器内部的 IQ matrix
}
}
for (int i = 0; i < 2; ++i) {
if (pps.scaling_list_8x8_present[i]) {
std::clog << "[PPS] scaling_list_8x8 present for idx " << i << "\n";
// 同上
}
}
// TODO: 根据实际解码器把这些参数映射到相应模块(CABAC tables, deblocking filter params,
// quantization matrices, ref list lengths 等)
}
// ------------------------- 使用示例 -------------------------
int main() {
// 假设这是 PPS NAL 的 RBSP payload(仅举例,非真实 PPS)
// real use: fill from NAL unit payload (exclude nal header)
std::vector<uint8_t> examplePPS = {
// 下面字节仅为示例,不表示有效 PPS
0x68, 0xCE, 0x06, 0xE2
};
try {
H264PPS pps = parsePPS(examplePPS);
DecoderContext ctx;
applyPPSToDecoder(pps, ctx);
std::cout << "pps.pic_parameter_set_id = " << pps.pic_parameter_set_id << "\n";
std::cout << "useCABAC = " << ctx.useCABAC << ", initQP = " << ctx.initQP << "\n";
} catch (const std::exception& e) {
std::cerr << "parsePPS error: " << e.what() << "\n";
}
return 0;
}