1. 解析H.264码流中的SPS与PPS
任务目标
读取H.264裸流文件(Annex B格式),定位并解析其中的SPS(序列参数集)和PPS(图像参数集),提取分辨率、帧率等关键编码参数,并以可读形式输出。
实现方案
H.264裸流以起始码 00 00 00 01 或 00 00 01 分隔NALU。每个NALU的第一个字节低5位为类型:7表示SPS,8表示PPS。SPS中通过pic_width_in_mbs_minus1和pic_height_in_map_units_minus1计算分辨率;帧率需结合SPS中的vui_parameters(若存在)中的timing_info推导。本程序使用Python实现,不依赖第三方库,直接对二进制位流进行解析。
完整代码
import struct
import sys
def read_ue(bit_reader):
"""读取无符号指数哥伦布编码"""
leading_zeros = 0
while bit_reader.read_bit() == 0:
leading_zeros += 1
if leading_zeros > 31:
raise ValueError("无效的指数哥伦布编码")
if leading_zeros == 0:
return 0
suffix = 0
for _ in range(leading_zeros):
suffix = (suffix << 1) | bit_reader.read_bit()
return (1 << leading_zeros) - 1 + suffix
class BitReader:
def __init__(self, data):
self.data = data
self.byte_pos = 0
self.bit_pos = 0 # 当前字节内已消费的位数
def read_bit(self):
if self.byte_pos >= len(self.data):
raise EOFError("位流读取越界")
byte = self.data[self.byte_pos]
bit = (byte >> (7 - self.bit_pos)) & 1
self.bit_pos += 1
if self.bit_pos == 8:
self.bit_pos = 0
self.byte_pos += 1
return bit
def read_bits(self, n):
val = 0
for _ in range(n):
val = (val << 1) | self.read_bit()
return val
def find_start_codes(data):
"""返回[(起始码位置, 起始码长度)],起始码为00 00 01或00 00 00 01"""
positions = []
i = 0
while i < len(data) - 3:
if data[i] == 0 and data[i+1] == 0:
if data[i+2] == 1:
positions.append((i, 3))
i += 3
continue
elif i+3 < len(data) and data[i+2] == 0 and data[i+3] == 1:
positions.append((i, 4))
i += 4
continue
i += 1
return positions
def parse_sps(nal_payload):
"""解析SPS,返回参数字典"""
br = BitReader(nal_payload)
profile_idc = br.read_bits(8)
constraint_flags = br.read_bits(8)
level_idc = br.read_bits(8)
sps_id = read_ue(br)
if profile_idc in (100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134):
chroma_format_idc = read_ue(br)
if chroma_format_idc == 3:
separate_colour_plane_flag = br.read_bit()
bit_depth_luma_minus8 = read_ue(br)
bit_depth_chroma_minus8 = read_ue(br)
qpprime_y_zero_transform_bypass_flag = br.read_bit()
seq_scaling_matrix_present_flag = br.read_bit()
if seq_scaling_matrix_present_flag:
# 简化处理:跳过缩放矩阵
for _ in range(8 if chroma_format_idc != 3 else 12):
if br.read_bit():
size = 16 if _ < 6 else 64
for __ in range(size):
read_ue(br)
log2_max_frame_num_minus4 = read_ue(br)
pic_order_cnt_type = read_ue(br)
if pic_order_cnt_type == 0:
log2_max_pic_order_cnt_lsb_minus4 = read_ue(br)
elif pic_order_cnt_type == 1:
delta_pic_order_always_zero_flag = br.read_bit()
offset_for_non_ref_pic = read_ue(br)
offset_for_top_to_bottom_field = read_ue(br)
num_ref_frames_in_pic_order_cnt_cycle = read_ue(br)
for _ in range(num_ref_frames_in_pic_order_cnt_cycle):
read_ue(br)
max_num_ref_frames = read_ue(br)
gaps_in_frame_num_value_allowed_flag = br.read_bit()
pic_width_in_mbs_minus1 = read_ue(br)
pic_height_in_map_units_minus1 = read_ue(br)
frame_mbs_only_flag = br.read_bit()
if not frame_mbs_only_flag:
mb_adaptive_frame_field_flag = br.read_bit()
direct_8x8_inference_flag = br.read_bit()
frame_cropping_flag = br.read_bit()
crop_left = crop_right = crop_top = crop_bottom = 0
if frame_cropping_flag:
crop_left = read_ue(br)
crop_right = read_ue(br)
crop_top = read_ue(br)
crop_bottom = read_ue(br)
width = (pic_width_in_mbs_minus1 + 1) * 16
height = (2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16
# 裁剪校正
crop_unit_x = 1
crop_unit_y = 2 - frame_mbs_only_flag
width -= (crop_left + crop_right) * crop_unit_x
height -= (crop_top + crop_bottom) * crop_unit_y
vui_present = br.read_bit()
fps = None
if vui_present:
aspect_ratio_info_present_flag = br.read_bit()
if aspect_ratio_info_present_flag:
aspect_ratio_idc = br.read_bits(8)
if aspect_ratio_idc == 255: # Extended_SAR
br.read_bits(32)
overscan_info_present_flag = br.read_bit()
if overscan_info_present_flag:
br.read_bit()
video_signal_type_present_flag = br.read_bit()
if video_signal_type_present_flag:
br.read_bits(4)
colour_description_present_flag = br.read_bit()
if colour_description_present_flag:
br.read_bits(24)
chroma_loc_info_present_flag = br.read_bit()
if chroma_loc_info_present_flag:
read_ue(br)
read_ue(br)
timing_info_present_flag = br.read_bit()
if timing_info_present_flag:
num_units_in_tick = br.read_bits(32)
time_scale = br.read_bits(32)
fixed_frame_rate_flag = br.read_bit()
if num_units_in_tick > 0:
fps = time_scale / (2.0 * num_units_in_tick)
return {
'profile_idc': profile_idc,
'level_idc': level_idc,
'width': width,
'height': height,
'fps': fps,
'frame_mbs_only_flag': frame_mbs_only_flag
}
def parse_pps(nal_payload):
"""解析PPS,返回关键参数"""
br = BitReader(nal_payload)
pps_id = read_ue(br)
sps_id = read_ue(br)
entropy_coding_mode_flag = br.read_bit()
bottom_field_pic_order_in_frame_present_flag = br.read_bit()
num_slice_groups_minus1 = read_ue(br)
if num_slice_groups_minus1 > 0:
slice_group_map_type = read_ue(br)
# 简化:仅记录类型,不展开映射
num_ref_idx_l0_default_active_minus1 = read_ue(br)
num_ref_idx_l1_default_active_minus1 = read_ue(br)
weighted_pred_flag = br.read_bit()
weighted_bipred_idc = br.read_bits(2)
pic_init_qp_minus26 = read_ue(br)
pic_init_qs_minus26 = read_ue(br)
chroma_qp_index_offset = read_ue(br)
deblocking_filter_control_present_flag = br.read_bit()
constrained_intra_pred_flag = br.read_bit()
redundant_pic_cnt_present_flag = br.read_bit()
return {
'pps_id': pps_id,
'sps_id': sps_id,
'entropy_coding_mode_flag': entropy_coding_mode_flag,
'weighted_bipred_idc': weighted_bipred_idc
}
def extract_nal_units(data, start_positions):
"""从起始码位置提取NALU列表 [(nal_type, payload)]"""
nalus = []
for idx, (pos, sc_len) in enumerate(start_positions):
nal_start = pos + sc_len
if idx + 1 < len(start_positions):
nal_end = start_positions[idx+1][0]
else:
nal_end = len(data)
# 去除防竞争字节 0x03
raw = bytearray(data[nal_start:nal_end])
cleaned = bytearray()
zero_count = 0
for b in raw:
if zero_count >= 2 and b == 3:
zero_count = 0
continue
cleaned.append(b)
if b == 0:
zero_count += 1
else:
zero_count = 0
if len(cleaned) == 0:
continue
nal_type = cleaned[0] & 0x1F
nalus.append((nal_type, bytes(cleaned[1:])))
return nalus
def main(filepath):
with open(filepath, 'rb') as f:
data = f.read()
starts = find_start_codes(data)
if not starts:
print("未找到起始码,文件可能不是H.264裸流")
return
nalus = extract_nal_units(data, starts)
sps_list = []
pps_list = []
for nal_type, payload in nalus:
if nal_type == 7:
sps_list.append(parse_sps(payload))
elif nal_type == 8:
pps_list.append(parse_pps(payload))
if not sps_list:
print("未找到SPS")
return
if not pps_list:
print("未找到PPS")
return
sps = sps_list[0]
pps = pps_list[0]
print(f"=== SPS 参数 ===")
print(f"Profile: {sps['profile_idc']}, Level: {sps['level_idc']}")
print(f"分辨率: {sps['width']} x {sps['height']}")
if sps['fps']:
print(f"帧率: {sps['fps']:.2f} fps")
else:
print("帧率: 未在SPS中声明(可能为可变帧率)")
print(f"帧编码方式: {'逐行' if sps['frame_mbs_only_flag'] else '支持隔行'}")
print(f"\n=== PPS 参数 ===")
print(f"PPS ID: {pps['pps_id']}, 关联SPS ID: {pps['sps_id']}")
print(f"熵编码模式: {'CABAC' if pps['entropy_coding_mode_flag'] else 'CAVLC'}")
if __name__ == '__main__':
if len(sys.argv) != 2:
print("用法: python h264_parser.py <文件路径>")
sys.exit(1)
main(sys.argv[1])
运行方式
python h264_parser.py test.h264
验证方法
使用已知参数的H.264裸流文件(如用ffmpeg生成:ffmpeg -f lavfi -i testsrc=size=1280x720:rate=30 -c:v libx264 -profile:v high test.h264),运行程序后应输出:
- 分辨率显示为
1280 x 720(与编码设置一致); - 帧率显示为
30.00 fps(若SPS中声明了timing_info); - PPS关联的SPS ID为0(默认值)。
若文件不含VUI信息,帧率显示“未在SPS中声明”,属正常情况。可通过修改ffmpeg命令添加 -x264-params vui_timing_info=1 强制写入帧率信息进行验证。