在 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 減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_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 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 視頻流提供了一個清晰、可控的元數據層。其主要作用包括:
- 解碼配置:它是解碼器開始解碼前必須獲取的“說明書”。沒有 SPS,解碼器就無法知道視頻的分辨率、Profile、Level 等基本信息,也就無法正確地分配內存、設置解碼模式。
- 流式傳輸的健壯性:由于 SPS 可以多次出現,即使在傳輸過程中丟失了一部分數據,解碼器也可以在下一個 SPS 出現時重新同步,繼續解碼。
- 兼容性檢查:
profile_idc
和level_idc
允許解碼器在開始解碼前檢查自身是否具備處理該視頻流的能力,從而避免了因不支持的特性而導致的解碼失敗。 - 互操作性:SPS 提供了視頻流的標準化描述,使得不同廠商的編碼器和解碼器可以相互兼容。