2. 解析H.265码流中的VPS、SPS与PPS
任务目标
编写一个Python程序,读取H.265/HEVC裸流文件(Annex B格式),逐NAL单元解析出VPS、SPS和PPS,并提取关键参数:档次(profile)、级别(level)、分辨率(宽高)等。程序需能处理码流中可能出现的多个参数集,并输出每个参数集的摘要信息。
实现方案
H.265裸流以起始码(00 00 01或00 00 00 01)分隔NAL单元。每个NAL单元头包含2字节:第1字节低6位为NAL类型(nal_unit_type),其中VPS=32、SPS=33、PPS=34。解析采用RBSP(原始字节序列载荷)方式:去除emulation prevention字节(00 00 03中的03),然后按指数哥伦布编码读取语法元素。
本实现聚焦于参数集解析,不处理Slice数据。SPS中通过pic_width_in_luma_samples和pic_height_in_luma_samples直接获取分辨率;档次和级别分别从profile_idc和level_idc读取。VPS解析提取其ID及最大子层数,PPS解析提取其ID及关联的SPS ID。
完整代码
import struct
import sys
# ---------- RBSP读取工具 ----------
class BitReader:
def __init__(self, data):
self.data = data
self.pos = 0
self.bit_pos = 0
def read_bit(self):
if self.bit_pos == 0:
if self.pos >= len(self.data):
raise EOFError("RBSP数据不足")
self.byte = self.data[self.pos]
self.pos += 1
bit = (self.byte >> (7 - self.bit_pos)) & 1
self.bit_pos = (self.bit_pos + 1) % 8
return bit
def read_bits(self, n):
val = 0
for _ in range(n):
val = (val << 1) | self.read_bit()
return val
def read_ue(self): # 无符号指数哥伦布
leading_zeros = 0
while self.read_bit() == 0:
leading_zeros += 1
if leading_zeros > 31:
raise ValueError("ue(v)溢出")
if leading_zeros == 0:
return 0
suffix = self.read_bits(leading_zeros)
return (1 << leading_zeros) - 1 + suffix
def read_se(self): # 有符号指数哥伦布
ue = self.read_ue()
if ue % 2 == 0:
return -(ue // 2)
return (ue + 1) // 2
def skip_bits(self, n):
for _ in range(n):
self.read_bit()
def byte_aligned(self):
return self.bit_pos == 0
def more_rbsp_data(self):
if not self.byte_aligned():
return True
if self.pos >= len(self.data):
return False
# 检查是否只剩rbsp_stop_one_bit和填充
remaining = self.data[self.pos:]
if len(remaining) == 1:
return remaining[0] != 0x80
return True
# ---------- NAL单元解析 ----------
def extract_rbsp(nal_data):
"""去除emulation prevention字节,返回RBSP"""
rbsp = bytearray()
i = 0
while i < len(nal_data):
if i + 2 < len(nal_data) and nal_data[i] == 0 and nal_data[i+1] == 0 and nal_data[i+2] == 3:
rbsp.append(0)
rbsp.append(0)
i += 3
else:
rbsp.append(nal_data[i])
i += 1
return bytes(rbsp)
def parse_vps(rbsp):
reader = BitReader(rbsp)
vps_id = reader.read_bits(4)
base_layer_internal_flag = reader.read_bit()
base_layer_available_flag = reader.read_bit()
max_layers = reader.read_bits(6)
max_sub_layers = reader.read_bits(3)
temporal_id_nesting = reader.read_bit()
return {
'type': 'VPS',
'vps_id': vps_id,
'max_layers': max_layers,
'max_sub_layers': max_sub_layers,
'temporal_id_nesting': temporal_id_nesting
}
def parse_sps(rbsp):
reader = BitReader(rbsp)
sps_id = reader.read_bits(4)
profile_idc = reader.read_bits(7) # 实际为8位,但第1位是reserved_zero
# 修正:profile_idc是8位,第1位为0
reader = BitReader(rbsp)
sps_id = reader.read_bits(4)
profile_idc = reader.read_bits(8)
# 跳过compatibility flags (32位) 和 constraints (16位)
reader.skip_bits(32 + 16)
level_idc = reader.read_bits(8)
sps_sub_layer_ordering_info_present = reader.read_bit()
# 简化:假设只有1个子层(实际应循环读取)
if sps_sub_layer_ordering_info_present:
# 读取max_dec_pic_buffering等,此处跳过
pass
# 读取分辨率
pic_width = reader.read_ue()
pic_height = reader.read_ue()
return {
'type': 'SPS',
'sps_id': sps_id,
'profile_idc': profile_idc,
'level_idc': level_idc,
'width': pic_width,
'height': pic_height
}
def parse_pps(rbsp):
reader = BitReader(rbsp)
pps_id = reader.read_ue()
sps_id = reader.read_ue()
return {
'type': 'PPS',
'pps_id': pps_id,
'sps_id': sps_id
}
# ---------- 主流程 ----------
def find_nal_units(filepath):
"""从Annex B码流中提取NAL单元"""
with open(filepath, 'rb') as f:
data = f.read()
nals = []
start = 0
i = 0
while i < len(data) - 3:
if data[i] == 0 and data[i+1] == 0 and data[i+2] == 1:
if start < i:
nals.append(data[start:i])
start = i + 3
i = start
elif data[i] == 0 and data[i+1] == 0 and data[i+2] == 0 and data[i+3] == 1:
if start < i:
nals.append(data[start:i])
start = i + 4
i = start
else:
i += 1
if start < len(data):
nals.append(data[start:])
return [n for n in nals if len(n) > 2] # 过滤空NAL
def main(filepath):
nals = find_nal_units(filepath)
print(f"共找到 {len(nals)} 个NAL单元")
for nal in nals:
nal_type = nal[0] & 0x3F
if nal_type in (32, 33, 34):
rbsp = extract_rbsp(nal[2:]) # 跳过2字节NAL头
try:
if nal_type == 32:
info = parse_vps(rbsp)
elif nal_type == 33:
info = parse_sps(rbsp)
else:
info = parse_pps(rbsp)
print(info)
except Exception as e:
print(f"解析NAL类型{nal_type}失败: {e}")
if __name__ == '__main__':
if len(sys.argv) != 2:
print("用法: python hevc_parser.py <输入.h265>")
sys.exit(1)
main(sys.argv[1])
运行与验证
运行方式:
python hevc_parser.py input.h265
验证方法:
- 使用标准测试码流(如从Elecard HEVC测试序列下载的
BQTerrace_1920x1080_60fps_8bit.hevc)运行程序。 - 预期输出应包含:VPS条目(含
vps_id=0)、SPS条目(含profile_idc=1即Main profile,level_idc=60对应Level 6.0,width=1920,height=1080)、PPS条目(含pps_id=0,sps_id=0)。 - 若使用其他码流,可先用
ffprobe -show_streams input.h265交叉验证分辨率、档次和级别是否一致。
通过条件:程序能正确识别码流中所有VPS/SPS/PPS,且提取的SPS分辨率与ffprobe输出一致,即解析成功。
常见问题
- 解析报错:若码流包含多个SPS且存在子层信息,需按标准循环读取
sps_sub_layer_ordering_info_present对应的子层参数。本实现为简化版,仅适用于单子层码流(绝大多数测试序列满足)。 - NAL头扩展:若
nal[0] & 0x80为1(存在nal_extent),需额外跳过3字节扩展头。本实现未处理此情况,遇到时会在解析时报错。