当前位置: 首页 > ops >正文

音视频学习(五十九):H264中的SPS

在 H.264 (也称为 AVC, Advanced Video Coding) 视频编码标准中,SPS (Sequence Parameter Set) 是一个至关重要的 NALU (Network Abstraction Layer Unit) 类型,它承载着整个视频序列共有的全局性配置信息。你可以把它理解为视频文件的“基因”,它定义了视频流的基础结构和解码规则。在解码器开始解析视频数据之前,它必须先获取并理解 SPS 中的内容,否则后续的图像数据将无法正确还原。

H264中的NALU Type

NALU Type (十进制)NALU Type (十六进制)名称描述
00x00未定义保留。
10x01非 IDR 图像中的非分片包含一个非 IDR (Instantaneous Decoding Refresh) 图像的编码分片。
20x02非 IDR 图像中的 A 型分片包含一个非 IDR 图像的编码分片 A。
30x03非 IDR 图像中的 B 型分片包含一个非 IDR 图像的编码分片 B。
40x04非 IDR 图像中的 C 型分片包含一个非 IDR 图像的编码分片 C。
50x05IDR 图像中的分片包含一个 IDR (Instantaneous Decoding Refresh) 图像的编码分片。IDR 帧标志着一个序列的开始,解码器可以在此点清空参考帧缓冲区,用于流的随机访问。
60x06补充增强信息 (SEI)SEI (Supplemental Enhancement Information)。它包含一些不影响解码过程但对增强视频使用有帮助的信息,如时间戳、用户数据等。
70x07序列参数集 (SPS)SPS (Sequence Parameter Set)。包含全局性的视频序列参数,如 Profile、Level、图像尺寸、参考帧数量等。它是解码器进行解码的必要配置。
80x08图像参数集 (PPS)PPS (Picture Parameter Set)。包含与一个或多个图像相关的参数,如熵编码模式、量化参数等。它引用 SPS,并与图像数据结合使用。
90x09访问单元分隔符标志一个新访问单元 (Access Unit) 的开始,一个访问单元通常对应一个完整的图像。
100x0A序列结束符标志视频序列的结束。
110x0B流结束符标志整个视频流的结束。
120x0C填充数据用于填充以对齐数据,通常不包含实际的视频信息。
130x0D序列参数集扩展包含了 SPS 的扩展信息,主要用于 SVC (Scalable Video Coding) 和 MVC (Multiview Video Coding)。
140x0E前缀 NALU用于 SVC 和 MVC。
150x0FSVC/MVC 分片用于 SVC/MVC 视频编码。
16-180x10-0x12SVC/MVC 分片用于 SVC/MVC 视频编码。
190x13非 IDR 图像分片 (包含参考图片列表)与 NALU Type 1 类似,但包含更灵活的参考图片列表重构信息。
200x14SVC/MVC 分片用于 SVC/MVC 视频编码。
21-230x15-0x17未定义保留。
24-310x18-0x1F聚合或填充 NALUAggregate/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 减4int delta_pic_order_always_zero_flag;  // (pic_order_cnt_type==1) 时的标志,若为1则相关偏移量恒为0int 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_idclevel_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_ticktime_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 ratiobool aspect_ratio_info_present_flag = false;uint32_t aspect_ratio_idc = 0;uint32_t sar_width = 0;uint32_t sar_height = 0;// overscan / video signalbool 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 locbool chroma_loc_info_present_flag = false;uint32_t chroma_sample_loc_type_top_field = 0;uint32_t chroma_sample_loc_type_bottom_field = 0;// timingbool 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 restrictionbool 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 {// Basicuint8_t profile_idc = 0;uint8_t constraint_set_flags = 0; // bit0..5 validuint8_t level_idc = 0;uint32_t seq_parameter_set_id = 0;// Extendeduint32_t chroma_format_idc = 1; // default 1 (4:2:0)bool separate_colour_plane_flag = false;// Frame num & POCuint32_t log2_max_frame_num_minus4 = 0;uint32_t pic_order_cnt_type = 0;// poc type 0uint32_t log2_max_pic_order_cnt_lsb_minus4 = 0;// poc type 1uint8_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 & geometryuint32_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;// Croppinguint8_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;// VUIuint8_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==0int 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_flagif (chroma_format_idc == 0) { // 4:0:0crop_unit_x = 1;crop_unit_y = 2 - frame_mbs_only_flag;} else if (chroma_format_idc == 1) { // 4:2:0crop_unit_x = 2;crop_unit_y = 2 * (2 - frame_mbs_only_flag);} else if (chroma_format_idc == 2) { // 4:2:2crop_unit_x = 2;crop_unit_y = (2 - frame_mbs_only_flag);} else if (chroma_format_idc == 3) { // 4:4:4crop_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 bytezero_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-Golombuint32_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-Golombint32_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_SARvui.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 infovui.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 restrictionvui.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_flaguint8_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:0sps.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 codesize_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 contextconst 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_idcstd::cout << "Level: "   << codec_ctx->level << std::endl;     // level_idcstd::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 视频流提供了一个清晰、可控的元数据层。其主要作用包括:

  1. 解码配置:它是解码器开始解码前必须获取的“说明书”。没有 SPS,解码器就无法知道视频的分辨率、Profile、Level 等基本信息,也就无法正确地分配内存、设置解码模式。
  2. 流式传输的健壮性:由于 SPS 可以多次出现,即使在传输过程中丢失了一部分数据,解码器也可以在下一个 SPS 出现时重新同步,继续解码。
  3. 兼容性检查profile_idclevel_idc 允许解码器在开始解码前检查自身是否具备处理该视频流的能力,从而避免了因不支持的特性而导致的解码失败。
  4. 互操作性:SPS 提供了视频流的标准化描述,使得不同厂商的编码器和解码器可以相互兼容。
http://www.xdnf.cn/news/19046.html

相关文章:

  • 使用STM32CubeMX使用CAN驱动无刷电机DJI3508
  • VisualStudio 将xlsx文件嵌入到资源中访问时变String?
  • HTML 和 JavaScript 关联的基础教程
  • LeetCode 刷题【56. 合并区间】
  • Linux - 中文显示乱码问题解决方法(编码查看及转换)- 学习/实践
  • 【Spring Cloud微服务】6.通信的利刃:深入浅出 Spring Cloud Feign 实战与原理
  • 智能体开发:学习与实验 ReAct
  • web端播放flv视频流demo(flv.js的使用)
  • API 月度更新汇总:ONLYOFFICE 协作空间文档
  • 【RAG Agent实战】告别“单线程”RAG:用查询理解与LangGraph构建能处理复杂意图的高级代理
  • WPF+IOC学习记录
  • 学习Java30天(tcp的多开客户端和bs架构以及java高级)
  • 群核科技--SpatialGen
  • 毕马威 —— 公众对人工智能的信任、态度及使用情况调查
  • OpenHarmony设备使用统计深度实战:从数据埋点到信息采集的全链路方案
  • matlab利用模糊算法控制PID参数实现模糊控制
  • echo、seq、{}、date、bc命令
  • Shell 秘典(卷二)——号令延展秘术 与 流程掌控心法・if 天机判语篇精解
  • SpringMvc下
  • log4jshell CVE-2021-44228 复现
  • 智能标签分类:新一代任务管理工具的进化方向
  • LangChain如何使用通义千问的向量模型
  • 【C语言入门级教学】sizeof和strlen的对⽐
  • Java使用apache.commons.math3的DBSCAN实现自动聚类
  • HTML 核心标签全解析:从文本排版到媒体嵌入
  • vue3中安装tailwindcss
  • C++函数继承
  • docker 搭建zookper集群,快照虚拟机多机模拟
  • 园区智慧水电管理系统:让能源管理从“成本黑洞”变“利润引擎”
  • 【实时Linux实战系列】实时数据可视化技术实现