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

发布于:2025-08-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

在 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_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 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 视频流提供了一个清晰、可控的元数据层。其主要作用包括:

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

网站公告

今日签到

点亮在社区的每一天
去签到