在 H.264 (也称为 AVC, Advanced Video Coding) 视频编码标准中,SPS (Sequence Parameter Set) 是一个至关重要的 NALU (Network Abstraction Layer Unit) 类型,它承载着整个视频序列共有的全局性配置信息。你可以把它理解为视频文件的“基因”,它定义了视频流的基础结构和解码规则。在解码器开始解析视频数据之前,它必须先获取并理解 SPS 中的内容,否则后续的图像数据将无法正确还原。
H264中的NALU Type
NALU Type (十进制) | NALU Type (十六进制) | 名称 | 描述 |
---|---|---|---|
0 | 0x00 | 未定义 | 保留。 |
1 | 0x01 | 非 IDR 图像中的非分片 | 包含一个非 IDR (Instantaneous Decoding Refresh) 图像的编码分片。 |
2 | 0x02 | 非 IDR 图像中的 A 型分片 | 包含一个非 IDR 图像的编码分片 A。 |
3 | 0x03 | 非 IDR 图像中的 B 型分片 | 包含一个非 IDR 图像的编码分片 B。 |
4 | 0x04 | 非 IDR 图像中的 C 型分片 | 包含一个非 IDR 图像的编码分片 C。 |
5 | 0x05 | IDR 图像中的分片 | 包含一个 IDR (Instantaneous Decoding Refresh) 图像的编码分片。IDR 帧标志着一个序列的开始,解码器可以在此点清空参考帧缓冲区,用于流的随机访问。 |
6 | 0x06 | 补充增强信息 (SEI) | SEI (Supplemental Enhancement Information)。它包含一些不影响解码过程但对增强视频使用有帮助的信息,如时间戳、用户数据等。 |
7 | 0x07 | 序列参数集 (SPS) | SPS (Sequence Parameter Set)。包含全局性的视频序列参数,如 Profile、Level、图像尺寸、参考帧数量等。它是解码器进行解码的必要配置。 |
8 | 0x08 | 图像参数集 (PPS) | PPS (Picture Parameter Set)。包含与一个或多个图像相关的参数,如熵编码模式、量化参数等。它引用 SPS,并与图像数据结合使用。 |
9 | 0x09 | 访问单元分隔符 | 标志一个新访问单元 (Access Unit) 的开始,一个访问单元通常对应一个完整的图像。 |
10 | 0x0A | 序列结束符 | 标志视频序列的结束。 |
11 | 0x0B | 流结束符 | 标志整个视频流的结束。 |
12 | 0x0C | 填充数据 | 用于填充以对齐数据,通常不包含实际的视频信息。 |
13 | 0x0D | 序列参数集扩展 | 包含了 SPS 的扩展信息,主要用于 SVC (Scalable Video Coding) 和 MVC (Multiview Video Coding)。 |
14 | 0x0E | 前缀 NALU | 用于 SVC 和 MVC。 |
15 | 0x0F | SVC/MVC 分片 | 用于 SVC/MVC 视频编码。 |
16-18 | 0x10-0x12 | SVC/MVC 分片 | 用于 SVC/MVC 视频编码。 |
19 | 0x13 | 非 IDR 图像分片 (包含参考图片列表) | 与 NALU Type 1 类似,但包含更灵活的参考图片列表重构信息。 |
20 | 0x14 | SVC/MVC 分片 | 用于 SVC/MVC 视频编码。 |
21-23 | 0x15-0x17 | 未定义 | 保留。 |
24-31 | 0x18-0x1F | 聚合或填充 NALU | Aggregate/Padding NALU。用于封装多个 NALU 或作为填充,主要用于 RTP (Real-time Transport Protocol) 传输。 |
什么是 SPS?
SPS 是一种特殊的 NALU,其 NALU Header 中的 nal_unit_type
字段值为 7。它包含了与整个视频序列相关联的参数,例如:
- Profile 和 Level 信息:决定了视频流所遵循的编码规格和复杂性等级,是解码器兼容性检查的关键。
- 图像尺寸:定义了视频的宽度和高度。
- 帧率:尽管不是直接存储在 SPS 中,但 SPS 提供了计算帧率所需的参数。
- 参考帧管理:定义了用于运动补偿的参考帧数量。
- 量化参数:提供了量化表的选择和相关参数。
- VUI (Video Usability Information):可选的,但包含了许多重要的信息,如宽高比、颜色空间、时间信息等。
SPS 可以存在于视频流的多个位置,通常在 IDR (Instantaneous Decoding Refresh) 帧之前出现,以确保解码器在任何时候进入视频流时都能获取到最新的配置信息。一个视频流中可以有多个 SPS,每个 SPS 都有一个唯一的 ID,通过 ID 来区分和引用。
SPS结构体
标准语法(ISO/IEC 14496-10 / ITU-T H.264, Table 7-1):
seq_parameter_set_rbsp( ) {
profile_idc
constraint_set_flags
level_idc
seq_parameter_set_id
log2_max_frame_num_minus4
pic_order_cnt_type
if( pic_order_cnt_type == 0 )
log2_max_pic_order_cnt_lsb_minus4
else if( pic_order_cnt_type == 1 ) {
delta_pic_order_always_zero_flag
offset_for_non_ref_pic
offset_for_top_to_bottom_field
num_ref_frames_in_pic_order_cnt_cycle
offset_for_ref_frame[ i ]
}
max_num_ref_frames
gaps_in_frame_num_value_allowed_flag
pic_width_in_mbs_minus1
pic_height_in_map_units_minus1
frame_mbs_only_flag
if( !frame_mbs_only_flag )
mb_adaptive_frame_field_flag
direct_8x8_inference_flag
frame_cropping_flag
if( frame_cropping_flag ) {
frame_crop_left_offset
frame_crop_right_offset
frame_crop_top_offset
frame_crop_bottom_offset
}
vui_parameters_present_flag
if( vui_parameters_present_flag )
vui_parameters( )
}
结构示意图:
Sequence Parameter Set (SPS)
│
├── profile_idc (8 bits)
├── constraint_set_flags (6 bits) + reserved_zero_2bits
├── level_idc (8 bits)
├── seq_parameter_set_id (UE)
│
├── log2_max_frame_num_minus4 (UE)
│
├── pic_order_cnt_type (UE)
│ ├── if = 0 → log2_max_pic_order_cnt_lsb_minus4 (UE)
│ └── if = 1
│ ├── delta_pic_order_always_zero_flag (1 bit)
│ ├── offset_for_non_ref_pic (SE)
│ ├── offset_for_top_to_bottom_field (SE)
│ ├── num_ref_frames_in_pic_order_cnt_cycle (UE)
│ └── offset_for_ref_frame[i] (SE) × N
│
├── max_num_ref_frames (UE)
├── gaps_in_frame_num_value_allowed_flag (1 bit)
│
├── pic_width_in_mbs_minus1 (UE)
├── pic_height_in_map_units_minus1 (UE)
├── frame_mbs_only_flag (1 bit)
│ └── if = 0 → mb_adaptive_frame_field_flag (1 bit)
├── direct_8x8_inference_flag (1 bit)
│
├── frame_cropping_flag (1 bit)
│ └── if = 1
│ ├── frame_crop_left_offset (UE)
│ ├── frame_crop_right_offset (UE)
│ ├── frame_crop_top_offset (UE)
│ └── frame_crop_bottom_offset (UE)
│
└── vui_parameters_present_flag (1 bit)
└── if = 1 → vui_parameters( )
C 语言结构体示例:
typedef struct {
int profile_idc; // Profile 标识 (例如: Baseline=66, Main=77, High=100)
int constraint_set_flags; // 约束集合标志 (constraint_set0_flag ~ constraint_set5_flag, 用于兼容性说明)
int level_idc; // Level 标识 (例如: 3.1=31, 4.0=40)
int seq_parameter_set_id; // SPS 的 ID (用于区分多个 SPS)
int log2_max_frame_num_minus4; // 最大帧号长度的 log2 值减4 → 最大 frame_num = 2^(log2_max_frame_num_minus4 + 4)
int pic_order_cnt_type; // POC (显示顺序) 类型 (0/1/2)
int log2_max_pic_order_cnt_lsb_minus4; // (pic_order_cnt_type==0) 时的 POC LSB 最大值的 log2 减4
int delta_pic_order_always_zero_flag; // (pic_order_cnt_type==1) 时的标志,若为1则相关偏移量恒为0
int offset_for_non_ref_pic; // (type=1) 非参考帧的 POC 偏移
int offset_for_top_to_bottom_field; // (type=1) 顶场到底场的 POC 偏移
int num_ref_frames_in_pic_order_cnt_cycle; // (type=1) POC 循环中的参考帧个数
int offset_for_ref_frame[256]; // (type=1) 每个参考帧的 POC 偏移 (最多255个)
int max_num_ref_frames; // 最大参考帧数 (解码器参考帧缓冲区大小)
int gaps_in_frame_num_value_allowed_flag; // 是否允许 frame_num 不连续 (1=允许丢帧)
int pic_width_in_mbs_minus1; // 图像宽度 (以宏块16像素为单位, 实际宽度= (pic_width_in_mbs_minus1+1)*16)
int pic_height_in_map_units_minus1; // 图像高度 (以宏块为单位, 实际高度= (pic_height_in_map_units_minus1+1)*16*(2 - frame_mbs_only_flag))
int frame_mbs_only_flag; // 是否仅支持帧编码 (1=帧编码, 0=支持场编码)
int mb_adaptive_frame_field_flag; // (当frame_mbs_only_flag=0时) 是否允许 MBAFF (宏块自适应帧/场编码)
int direct_8x8_inference_flag; // 是否启用 8x8 直接模式推断 (B帧运动补偿相关)
int frame_cropping_flag; // 是否存在裁剪参数
int frame_crop_left_offset; // 左边裁剪宏块数
int frame_crop_right_offset; // 右边裁剪宏块数
int frame_crop_top_offset; // 上边裁剪宏块数
int frame_crop_bottom_offset; // 下边裁剪宏块数
int vui_parameters_present_flag; // 是否存在 VUI 参数 (视频可用性信息: 时基, SAR, 色彩空间, HRD 等)
// struct VUI vui; // VUI 扩展信息 (可选)
} SPS_t;
SPS 参数详解
profile_idc
和 level_idc
这是 SPS 中最重要的两个参数,它们一起定义了视频流的编码约束和复杂性。
profile_idc
(Profile Indication): 指示了编码器使用的编码工具集。不同的 Profile 支持不同的编码特性,例如 B 帧、CABAC 等。常见的 Profile 包括:- Baseline Profile (66): 最基础的配置,不支持 B 帧和 CABAC,主要用于实时通信和移动设备,如视频会议。
- Main Profile (77): 支持 B 帧,用于标清电视广播。
- High Profile (100): 支持更多高级特性,如 8x8 块变换、无损编码等,广泛用于高清电视广播和蓝光光盘。
- 还有其他如 Progressive High Profile (110) 和 Multiview High Profile (118) 等,用于特殊应用场景。
level_idc
(Level Indication): 定义了视频流的约束级别,例如最大帧尺寸、最大比特率、最大宏块处理速率等。Level 越高,支持的视频分辨率、帧率和比特率就越高。这为解码器提供了性能参考,确保它有能力处理该视频流。例如,Level 4.1 支持 1920x1080p 分辨率,而 Level 5.1 则可以支持更高的分辨率。
seq_parameter_set_id
这是一个 0 到 31 的无符号整数,用于唯一标识当前的 SPS。当解码器解析 PPS (Picture Parameter Set) 时,它会通过 PPS 中的 seq_parameter_set_id
字段来引用相应的 SPS,从而建立起 PPS 和 SPS 之间的关联。这种设计使得多个 PPS 可以共享同一个 SPS,也方便了在视频流中切换不同的配置。
图像尺寸相关参数
SPS 中定义了视频图像的宽度和高度,但它们不是直接以像素为单位存储的,而是通过宏块 (Macroblock) 的数量来表示。
pic_width_in_mbs_minus1
: 图像宽度,以宏块为单位。实际宽度为16 * (pic_width_in_mbs_minus1 + 1)
像素。pic_height_in_map_units_minus1
: 图像高度,以宏块行数或宏块组为单位。实际高度为16 * (pic_height_in_map_units_minus1 + 1)
像素。
这种基于宏块的尺寸定义方式是 H.264 的基本特性,因为所有编码和解码操作都是以宏块为单位进行的。
参考帧相关参数
H.264 使用帧间预测来提高压缩效率,这就需要管理参考帧。SPS 中的参数定义了参考帧的管理方式。
num_ref_frames
: 指示了解码器用于帧间预测的参考帧的最大数量。这个参数对于解码器的内存管理至关重要,因为它需要为这些参考帧分配缓冲区。
chroma_format_idc
这个参数定义了视频的色度格式,也就是 YUV (或 YCbCr) 格式中的 U 和 V 分量采样方式。常见的有:
- 4:2:0 (1): 宽高都减半采样,最常用,适用于大多数应用场景。
- 4:2:2 (2): 垂直方向不减半,水平方向减半,主要用于广播和专业视频编辑。
- 4:4:4 (3): 不进行色度下采样,每个像素都有完整的 YUV 数据,用于高质量无损编码。
VUI (Video Usability Information)
VUI 是一个可选的子结构,但它包含了许多对解码和显示至关重要的信息。如果 SPS 中存在 vui_parameters_present_flag
为 1,则会包含 VUI。
aspect_ratio_idc
: 定义了像素宽高比 (Pixel Aspect Ratio),告诉播放器如何正确地拉伸图像以得到正确的显示宽高比 (Display Aspect Ratio)。timing_info_present_flag
: 如果为 1,则包含时间信息,如num_units_in_tick
和time_scale
。它们共同定义了视频的帧率。frame_rate = time_scale / (2 * num_units_in_tick)
video_signal_type_present_flag
: 定义了颜色空间、色度位置和视频信号类型。video_full_range_flag
: 指示亮度信号是全范围 (0-255) 还是有限范围 (16-235)。colour_primaries
,transfer_characteristics
,matrix_coefficients
: 这些参数共同定义了颜色空间,例如 BT.709 (高清) 或 BT.601 (标清),对于准确的色彩还原至关重要。
使用示例(c++)
// h264_sps_vui_parser.h
#pragma once
#include <cstdint>
#include <vector>
#include <stdexcept>
#include <string>
#include <limits>
struct H264VUI {
// aspect ratio
bool aspect_ratio_info_present_flag = false;
uint32_t aspect_ratio_idc = 0;
uint32_t sar_width = 0;
uint32_t sar_height = 0;
// overscan / video signal
bool overscan_info_present_flag = false;
bool overscan_appropriate_flag = false;
bool video_signal_type_present_flag = false;
uint32_t video_format = 0;
bool video_full_range_flag = false;
bool colour_description_present_flag = false;
uint32_t colour_primaries = 0;
uint32_t transfer_characteristics = 0;
uint32_t matrix_coefficients = 0;
// chroma loc
bool chroma_loc_info_present_flag = false;
uint32_t chroma_sample_loc_type_top_field = 0;
uint32_t chroma_sample_loc_type_bottom_field = 0;
// timing
bool timing_info_present_flag = false;
uint32_t num_units_in_tick = 0;
uint32_t time_scale = 0;
bool fixed_frame_rate_flag = false;
// other flags (parsed minimally)
bool nal_hrd_parameters_present_flag = false;
bool vcl_hrd_parameters_present_flag = false;
bool low_delay_hrd_flag = false;
bool pic_struct_present_flag = false;
// bitstream restriction
bool bitstream_restriction_flag = false;
uint32_t max_bytes_per_pic_denom = 0;
uint32_t max_bits_per_mb_denom = 0;
uint32_t log2_max_mv_length_horizontal = 0;
uint32_t log2_max_mv_length_vertical = 0;
uint32_t num_reorder_frames = 0;
uint32_t max_dec_frame_buffering = 0;
};
struct H264SPS {
// Basic
uint8_t profile_idc = 0;
uint8_t constraint_set_flags = 0; // bit0..5 valid
uint8_t level_idc = 0;
uint32_t seq_parameter_set_id = 0;
// Extended
uint32_t chroma_format_idc = 1; // default 1 (4:2:0)
bool separate_colour_plane_flag = false;
// Frame num & POC
uint32_t log2_max_frame_num_minus4 = 0;
uint32_t pic_order_cnt_type = 0;
// poc type 0
uint32_t log2_max_pic_order_cnt_lsb_minus4 = 0;
// poc type 1
uint8_t delta_pic_order_always_zero_flag = 0;
int32_t offset_for_non_ref_pic = 0;
int32_t offset_for_top_to_bottom_field = 0;
uint32_t num_ref_frames_in_pic_order_cnt_cycle = 0;
int32_t offset_for_ref_frame[256] = {0}; // standard allows up to 255
// Ref frames & geometry
uint32_t max_num_ref_frames = 0;
uint8_t gaps_in_frame_num_value_allowed_flag = 0;
uint32_t pic_width_in_mbs_minus1 = 0;
uint32_t pic_height_in_map_units_minus1 = 0;
uint8_t frame_mbs_only_flag = 1;
uint8_t mb_adaptive_frame_field_flag = 0;
uint8_t direct_8x8_inference_flag = 0;
// Cropping
uint8_t frame_cropping_flag = 0;
uint32_t frame_crop_left_offset = 0;
uint32_t frame_crop_right_offset = 0;
uint32_t frame_crop_top_offset = 0;
uint32_t frame_crop_bottom_offset = 0;
// VUI
uint8_t vui_parameters_present_flag = 0;
H264VUI vui;
// convenience: compute pixel size (considers chroma format & crop)
int width() const {
int pic_w = (int)((pic_width_in_mbs_minus1 + 1) * 16);
// height: each map unit is 16 for frame, 32 for field pairs when frame_mbs_only_flag==0
int pic_h = (int)((pic_height_in_map_units_minus1 + 1) * 16 * (2 - frame_mbs_only_flag));
if (frame_cropping_flag) {
int crop_unit_x = 1;
int crop_unit_y = 1;
// crop unit calculation depends on chroma_format_idc and separate_colour_plane_flag
if (chroma_format_idc == 0) { // 4:0:0
crop_unit_x = 1;
crop_unit_y = 2 - frame_mbs_only_flag;
} else if (chroma_format_idc == 1) { // 4:2:0
crop_unit_x = 2;
crop_unit_y = 2 * (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 2) { // 4:2:2
crop_unit_x = 2;
crop_unit_y = (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 3) { // 4:4:4
crop_unit_x = 1;
crop_unit_y = (2 - frame_mbs_only_flag);
}
pic_w -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
pic_h -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
}
return pic_w;
}
int height() const {
int pic_w = (int)((pic_width_in_mbs_minus1 + 1) * 16);
int pic_h = (int)((pic_height_in_map_units_minus1 + 1) * 16 * (2 - frame_mbs_only_flag));
if (frame_cropping_flag) {
int crop_unit_x = 1;
int crop_unit_y = 1;
if (chroma_format_idc == 0) {
crop_unit_x = 1;
crop_unit_y = 2 - frame_mbs_only_flag;
} else if (chroma_format_idc == 1) {
crop_unit_x = 2;
crop_unit_y = 2 * (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 2) {
crop_unit_x = 2;
crop_unit_y = (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 3) {
crop_unit_x = 1;
crop_unit_y = (2 - frame_mbs_only_flag);
}
pic_w -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
pic_h -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
}
(void)pic_w;
return pic_h;
}
// frame rate: if VUI timing info present, compute fps, else return 0.0
// common formula: fps = time_scale / (2 * num_units_in_tick)
double fps() const {
if (!vui.timing_info_present_flag || vui.num_units_in_tick == 0) return 0.0;
return (double)vui.time_scale / (2.0 * (double)vui.num_units_in_tick);
}
bool has_timing() const {
return vui.timing_info_present_flag;
}
};
// ---- 工具:移除 0x000003 仿射字节(得到 RBSP) ----
inline std::vector<uint8_t> avc_nal_to_rbsp(const uint8_t* data, size_t size) {
std::vector<uint8_t> out;
out.reserve(size);
int zero_count = 0;
for (size_t i = 0; i < size; ++i) {
uint8_t b = data[i];
if (zero_count == 2 && b == 0x03) {
// skip emulation prevention byte
zero_count = 0;
continue;
}
out.push_back(b);
if (b == 0x00) zero_count++;
else zero_count = 0;
}
return out;
}
// ---- 比特读取器(MSB-first)+ Exp-Golomb ----
class BitReader {
public:
BitReader(const uint8_t* d, size_t n) : data_(d), size_(n) {}
uint32_t readBits(int n) {
if (n <= 0 || n > 32) throw std::runtime_error("readBits n out of range");
uint32_t v = 0;
for (int i = 0; i < n; ++i) {
if (bitpos_ >= size_ * 8) throw std::runtime_error("bitstream overread");
v <<= 1;
v |= ((data_[bitpos_ >> 3] >> (7 - (bitpos_ & 7))) & 1);
++bitpos_;
}
return v;
}
uint8_t readBit() { return (uint8_t)readBits(1); }
// Unsigned Exp-Golomb
uint32_t readUE() {
int leadingZeroBits = -1;
uint8_t b = 0;
do {
b = readBit();
++leadingZeroBits;
if (bitpos_ > size_ * 8) throw std::runtime_error("UE parse overread");
} while (b == 0);
if (leadingZeroBits < 0) throw std::runtime_error("UE parse error");
uint32_t info = 0;
if (leadingZeroBits > 0) info = readBits(leadingZeroBits);
return ((1u << leadingZeroBits) - 1u) + info;
}
// Signed Exp-Golomb
int32_t readSE() {
uint32_t ue = readUE();
int32_t v = (int32_t)((ue + 1) / 2);
return (ue & 1) ? v : -v;
}
size_t bitsRemaining() const {
size_t total = size_ * 8;
return (bitpos_ <= total) ? (total - bitpos_) : 0;
}
private:
const uint8_t* data_;
size_t size_;
size_t bitpos_ = 0;
};
// ---- 辅助:解析 HRD parameters(用于推进比特流) ----
inline void parse_hrd_parameters(BitReader& br) {
uint32_t cpb_cnt_minus1 = br.readUE();
uint32_t bit_rate_scale = br.readBits(4);
uint32_t cpb_size_scale = br.readBits(4);
for (uint32_t i = 0; i <= cpb_cnt_minus1; ++i) {
(void)br.readUE(); // bit_rate_value_minus1
(void)br.readUE(); // cpb_size_value_minus1
(void)br.readBit(); // cbr_flag
}
(void)br.readBits(5); // initial_cpb_removal_delay_length_minus1
(void)br.readBits(5); // cpb_removal_delay_length_minus1
(void)br.readBits(5); // dpb_output_delay_length_minus1
(void)br.readBits(5); // time_offset_length
}
// ---- 解析 VUI parameters(实现常用字段与 timing info) ----
inline void parse_vui_parameters(BitReader& br, H264VUI& vui) {
vui.aspect_ratio_info_present_flag = br.readBit();
if (vui.aspect_ratio_info_present_flag) {
vui.aspect_ratio_idc = br.readBits(8);
if (vui.aspect_ratio_idc == 255) { // Extended_SAR
vui.sar_width = br.readBits(16);
vui.sar_height = br.readBits(16);
}
}
vui.overscan_info_present_flag = br.readBit();
if (vui.overscan_info_present_flag) {
vui.overscan_appropriate_flag = br.readBit();
}
vui.video_signal_type_present_flag = br.readBit();
if (vui.video_signal_type_present_flag) {
vui.video_format = br.readBits(3);
vui.video_full_range_flag = br.readBit();
vui.colour_description_present_flag = br.readBit();
if (vui.colour_description_present_flag) {
vui.colour_primaries = br.readBits(8);
vui.transfer_characteristics = br.readBits(8);
vui.matrix_coefficients = br.readBits(8);
}
}
vui.chroma_loc_info_present_flag = br.readBit();
if (vui.chroma_loc_info_present_flag) {
vui.chroma_sample_loc_type_top_field = br.readUE();
vui.chroma_sample_loc_type_bottom_field = br.readUE();
}
// timing info
vui.timing_info_present_flag = br.readBit();
if (vui.timing_info_present_flag) {
vui.num_units_in_tick = br.readBits(32);
vui.time_scale = br.readBits(32);
vui.fixed_frame_rate_flag = br.readBit();
}
// HRD params (nal & vcl)
vui.nal_hrd_parameters_present_flag = br.readBit();
if (vui.nal_hrd_parameters_present_flag) {
parse_hrd_parameters(br);
}
vui.vcl_hrd_parameters_present_flag = br.readBit();
if (vui.vcl_hrd_parameters_present_flag) {
parse_hrd_parameters(br);
}
if (vui.nal_hrd_parameters_present_flag || vui.vcl_hrd_parameters_present_flag) {
vui.low_delay_hrd_flag = br.readBit();
}
vui.pic_struct_present_flag = br.readBit();
// bitstream restriction
vui.bitstream_restriction_flag = br.readBit();
if (vui.bitstream_restriction_flag) {
vui.max_bytes_per_pic_denom = br.readUE();
vui.max_bits_per_mb_denom = br.readUE();
vui.log2_max_mv_length_horizontal = br.readUE();
vui.log2_max_mv_length_vertical = br.readUE();
vui.num_reorder_frames = br.readUE();
vui.max_dec_frame_buffering = br.readUE();
}
}
// ---- 核心:解析 SPS(输入为 NALU payload,不含起始码;可以含 NAL header) ----
inline H264SPS parse_h264_sps(const uint8_t* nalu, size_t nalu_size) {
if (!nalu || nalu_size < 4) throw std::runtime_error("SPS NAL too small");
size_t offset = 0;
uint8_t nal_unit_type = (nalu[0] & 0x1F);
if (nal_unit_type == 7) {
offset = 1; // skip nal header
}
auto rbsp = avc_nal_to_rbsp(nalu + offset, nalu_size - offset);
if (rbsp.empty()) throw std::runtime_error("empty RBSP after EP removal");
BitReader br(rbsp.data(), rbsp.size());
H264SPS sps{};
sps.profile_idc = (uint8_t)br.readBits(8);
sps.constraint_set_flags = (uint8_t)br.readBits(8);
sps.level_idc = (uint8_t)br.readBits(8);
sps.seq_parameter_set_id = br.readUE();
// profile-specific extensions (chroma format, bit depth, scaling lists)
bool high_profile = (sps.profile_idc == 100 || sps.profile_idc == 110 ||
sps.profile_idc == 122 || sps.profile_idc == 244 ||
sps.profile_idc == 44 || sps.profile_idc == 83 ||
sps.profile_idc == 86 || sps.profile_idc == 118 ||
sps.profile_idc == 128 || sps.profile_idc == 138 ||
sps.profile_idc == 139 || sps.profile_idc == 134 ||
sps.profile_idc == 135);
if (high_profile) {
sps.chroma_format_idc = br.readUE();
if (sps.chroma_format_idc == 3) {
sps.separate_colour_plane_flag = br.readBit();
}
(void)br.readUE(); // bit_depth_luma_minus8
(void)br.readUE(); // bit_depth_chroma_minus8
(void)br.readBit(); // qpprime_y_zero_transform_bypass_flag
uint8_t seq_scaling_matrix_present_flag = br.readBit();
if (seq_scaling_matrix_present_flag) {
int count = (sps.chroma_format_idc != 3) ? 8 : 12;
for (int i = 0; i < count; ++i) {
uint8_t seq_scaling_list_present_flag = br.readBit();
if (seq_scaling_list_present_flag) {
int sizeOfScalingList = (i < 6) ? 16 : 64;
int lastScale = 8;
int nextScale = 8;
for (int j = 0; j < sizeOfScalingList; ++j) {
if (nextScale != 0) {
int32_t delta_scale = br.readSE();
nextScale = (lastScale + delta_scale + 256) % 256;
}
lastScale = (nextScale == 0) ? lastScale : nextScale;
}
}
}
}
} else {
sps.chroma_format_idc = 1; // default 4:2:0
sps.separate_colour_plane_flag = false;
}
sps.log2_max_frame_num_minus4 = br.readUE();
sps.pic_order_cnt_type = br.readUE();
if (sps.pic_order_cnt_type == 0) {
sps.log2_max_pic_order_cnt_lsb_minus4 = br.readUE();
} else if (sps.pic_order_cnt_type == 1) {
sps.delta_pic_order_always_zero_flag = br.readBit();
sps.offset_for_non_ref_pic = br.readSE();
sps.offset_for_top_to_bottom_field = br.readSE();
sps.num_ref_frames_in_pic_order_cnt_cycle = br.readUE();
if (sps.num_ref_frames_in_pic_order_cnt_cycle > 255) throw std::runtime_error("num_ref_frames_in_pic_order_cnt_cycle too large");
for (uint32_t i = 0; i < sps.num_ref_frames_in_pic_order_cnt_cycle; ++i) {
sps.offset_for_ref_frame[i] = br.readSE();
}
}
sps.max_num_ref_frames = br.readUE();
sps.gaps_in_frame_num_value_allowed_flag = br.readBit();
sps.pic_width_in_mbs_minus1 = br.readUE();
sps.pic_height_in_map_units_minus1 = br.readUE();
sps.frame_mbs_only_flag = br.readBit();
if (!sps.frame_mbs_only_flag) {
sps.mb_adaptive_frame_field_flag = br.readBit();
}
sps.direct_8x8_inference_flag = br.readBit();
sps.frame_cropping_flag = br.readBit();
if (sps.frame_cropping_flag) {
sps.frame_crop_left_offset = br.readUE();
sps.frame_crop_right_offset = br.readUE();
sps.frame_crop_top_offset = br.readUE();
sps.frame_crop_bottom_offset = br.readUE();
}
sps.vui_parameters_present_flag = br.readBit();
if (sps.vui_parameters_present_flag) {
parse_vui_parameters(br, sps.vui);
}
return sps;
}
// ---- 简易辅助:从 Annex-B 字节流中抽取 SPS NAL 并解析 ----
inline bool find_and_parse_sps_from_annexb(const uint8_t* data, size_t size, H264SPS& out) {
// 搜索 00 00 01 / 00 00 00 01 start code
size_t i = 0;
auto is_start_code = [&](size_t p)->int {
if (p + 3 < size && data[p]==0 && data[p+1]==0 && data[p+2]==1) return 3;
if (p + 4 < size && data[p]==0 && data[p+1]==0 && data[p+2]==0 && data[p+3]==1) return 4;
return 0;
};
while (i + 4 < size) {
int sc = is_start_code(i);
if (sc == 0) { ++i; continue; }
size_t nal_begin = i + sc;
if (nal_begin >= size) break;
uint8_t nal_unit_type = data[nal_begin] & 0x1F;
// 找到下一个 start code 作为 NAL 结束
size_t j = nal_begin + 1;
while (j + 3 < size && is_start_code(j) == 0) ++j;
size_t nal_end = j;
if (nal_unit_type == 7) {
out = parse_h264_sps(data + nal_begin, nal_end - nal_begin);
return true;
}
i = j;
}
return false;
}
测试程序:
#include "h264_sps_vui_parser.h"
#include <iostream>
int main() {
const uint8_t sample_annexb[] = {
0x00,0x00,0x00,0x01, 0x67, 0x64,0x00,0x1f, 0xac,0xd9,0x40,0x78,0x02,0x27,0xe5,0xc0,0x44,0x00,0x00,0x03,0x00,0x04,0x00,0x00,0x03,0x00,0xc8,0x3c,0x60,0xc6,0x58
};
H264SPS sps;
if (find_and_parse_sps_from_annexb(sample_annexb, sizeof(sample_annexb), sps)) {
std::cout << "Profile: " << (int)sps.profile_idc
<< " Level: " << (int)sps.level_idc
<< " SPS id: " << sps.seq_parameter_set_id << "\n";
std::cout << "Chroma format: " << sps.chroma_format_idc
<< " separate_colour_plane_flag: " << sps.separate_colour_plane_flag << "\n";
std::cout << "Width x Height = " << sps.width()
<< " x " << sps.height() << "\n";
if (sps.has_timing()) {
std::cout << "Timing present: " << sps.vui.num_units_in_tick << " / " << sps.vui.time_scale
<< " fixed_frame_rate_flag=" << sps.vui.fixed_frame_rate_flag << "\n";
std::cout << "FPS ~= " << sps.fps() << "\n";
} else {
std::cout << "No timing info in VUI\n";
}
} else {
std::cout << "SPS not found\n";
}
return 0;
}
ffmpeg处理sps
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
#include <iostream>
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " input.h264" << std::endl;
return -1;
}
const char* filename = argv[1];
AVFormatContext* fmt_ctx = nullptr;
// 打开文件或流
if (avformat_open_input(&fmt_ctx, filename, nullptr, nullptr) < 0) {
std::cerr << "Could not open input file" << std::endl;
return -1;
}
// 读取流信息
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
std::cerr << "Could not find stream information" << std::endl;
avformat_close_input(&fmt_ctx);
return -1;
}
// 找到视频流
int video_stream_index = -1;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
break;
}
}
if (video_stream_index == -1) {
std::cerr << "Could not find video stream" << std::endl;
avformat_close_input(&fmt_ctx);
return -1;
}
AVCodecParameters* codecpar = fmt_ctx->streams[video_stream_index]->codecpar;
// 创建 codec context
const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id);
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_ctx, codecpar);
if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
std::cerr << "Could not open codec" << std::endl;
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
return -1;
}
// 输出 SPS 信息 (通过 codec context 已经解析好)
std::cout << "Codec: " << avcodec_get_name(codecpar->codec_id) << std::endl;
std::cout << "Profile: " << codec_ctx->profile << std::endl; // profile_idc
std::cout << "Level: " << codec_ctx->level << std::endl; // level_idc
std::cout << "Width: " << codec_ctx->width << std::endl; // 宽度
std::cout << "Height: " << codec_ctx->height << std::endl; // 高度
// 如果需要查看原始 extradata (包含 SPS/PPS NALU)
std::cout << "Extradata size: " << codecpar->extradata_size << std::endl;
if (codecpar->extradata && codecpar->extradata_size > 0) {
std::cout << "SPS/PPS data (hex):" << std::endl;
for (int i = 0; i < codecpar->extradata_size; i++) {
printf("%02X ", codecpar->extradata[i]);
}
std::cout << std::endl;
}
// 释放资源
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
return 0;
}
输出如下:
Codec: h264
Profile: 100
Level: 40
Width: 1920
Height: 1088
Extradata size: 39
SPS/PPS data (hex):
00 00 00 01 67 64 00 28 AC 2B 40 50 1E D0 0D 41 41 ...
作用
SPS 的存在为 H.264 视频流提供了一个清晰、可控的元数据层。其主要作用包括:
- 解码配置:它是解码器开始解码前必须获取的“说明书”。没有 SPS,解码器就无法知道视频的分辨率、Profile、Level 等基本信息,也就无法正确地分配内存、设置解码模式。
- 流式传输的健壮性:由于 SPS 可以多次出现,即使在传输过程中丢失了一部分数据,解码器也可以在下一个 SPS 出现时重新同步,继续解码。
- 兼容性检查:
profile_idc
和level_idc
允许解码器在开始解码前检查自身是否具备处理该视频流的能力,从而避免了因不支持的特性而导致的解码失败。 - 互操作性:SPS 提供了视频流的标准化描述,使得不同厂商的编码器和解码器可以相互兼容。