☰
H264/H265码流类型详解:Annex-B与AVCC格式解析及转换实战
2026/10/15 23:35:51 网站建设 项目流程

1. 从播放器黑屏说起:为什么你必须搞懂码流类型

很多人第一次接触H264/H265,是在一个很具体的场景里:明明拿到了一个后缀是.h264或者.265的文件,用播放器打开却是一片黑屏,或者只有声音没有画面;又或者做视频传输的时候,编码端跑得好好的,解码端却报出一堆"nal unit type unknown"之类的错误。折腾半天,最后发现问题根本不在编解码器本身,而是码流类型没对上——你给解码器喂的是裸流,它却以为自己在读带起始码的Annex-B格式,或者反过来。

这就是我想聊H264/H265码流类型的原因。它不是什么高深算法,但它是所有视频处理链路里最容易被忽略、又最容易让人栽跟头的一环。无论你是做视频播放、流媒体传输、录像存储,还是做AI视频分析的前处理,只要涉及H264/H265,你就绕不开"这段码流到底是怎么组织的"这个问题。

所谓码流类型,说白了就是"这一串0和1,是按什么规矩排列的"。同样一段H264编码数据,可以有两种完全不同的封装方式:一种是Annex-B字节流格式,用起始码把一个个NALU串起来,适合文件存储和实时流传输;另一种是AVCC格式,用长度前缀来描述每个NALU的大小,常见于MP4这类容器里。H265也类似,对应的是Annex-B和HVCC(也叫hvcC)两种组织方式。搞不清这两者的区别,你连一个NALU的边界在哪都找不到,更别提解析SPS、PPS、切片数据了。

这篇文章我打算从实战角度把这件事讲透:码流类型到底分几种、每种长什么样、怎么用代码判断和转换、实际项目里踩过哪些坑。适合做音视频开发、流媒体、视频监控、AI视觉前处理的同学,也适合刚入门想搞清楚"裸流""封装流"到底啥区别的朋友。看完你至少能做到:拿到一段未知的H264/H265数据,能判断它是什么类型,知道该怎么解析,遇到黑屏花屏时知道往哪个方向排查。

2. Annex-B与AVCC:两种码流组织的本质差异

2.1 起始码与长度前缀,到底在解决什么问题

要理解码流类型,先得理解一个最基础的问题:解码器怎么知道一个NALU从哪里开始、到哪里结束?

NALU是Network Abstraction Layer Unit的缩写,你可以把它理解成H264/H265码流里的"最小传输单元"。一个NALU可能装的是SPS(序列参数集)、PPS(图像参数集)、一个切片(slice),也可能是一个SEI(补充增强信息)。解码器要工作,第一步就是把这些NALU一个个切出来。

问题来了:NALU本身是变长的,没有固定长度。那怎么切?业界给出了两种思路。

第一种思路是用特殊标记做分隔符。在H264/H265里,这个标记就是起始码(start code)。起始码有两种形式:三字节的0x000001和四字节的0x00000001。解码器扫描码流,遇到起始码就认为"上一个NALU结束了,新的NALU开始了"。这种用起始码分隔NALU的组织方式,就叫Annex-B格式。它的名字来源于H.264规范附录B,本来是给字节流传输场景定义的。

第二种思路是在NALU前面加一个长度字段。每个NALU前面放2字节或4字节,明确告诉解码器"接下来这个NALU有多长"。解码器先读长度,再按长度读数据,读完就自然到了下一个NALU。这种组织方式叫AVCC格式(H264)或HVCC格式(H265),名字来自AVCDecoderConfigurationRecord和HEVCDecoderConfigurationRecord这两个配置结构。MP4、MKV这类容器文件里,视频数据基本都是这么存的。

一句话总结:Annex-B靠"分隔符"找边界,AVCC靠"长度"找边界。前者适合流式传输,后者适合随机访问。

2.2 Annex-B格式的字节级结构

我们具体看看Annex-B长什么样。假设有一段H264数据,它可能是这样的:

00 00 00 01 67 42 C0 1E ... // SPS,起始码是4字节 00 00 00 01 68 CE 3C 80 ... // PPS,起始码是4字节 00 00 00 01 65 88 84 ... // IDR切片,起始码是4字节 00 00 01 41 9A 22 ... // 非IDR切片,起始码是3字节

可以看到,每个NALU前面都跟着起始码。起始码的长度不固定,可能是3字节也可能是4字节,解码器扫描的时候要同时兼容这两种。这里有个细节:起始码本身不属于NALU内容,解析的时候要把它剥掉,剩下的才是真正的NALU数据。

那怎么从NALU数据里区分它是什么类型?看NALU的第一个字节。H264的NALU头是1字节,结构是:

+---------------+ |0|1|2|3|4|5|6|7| +-+-+-+-+-+-+-+-+ |F|NRI| Type | +---------------+

低5位是Type,也就是NALU类型。比如0x67的二进制是0110 0111,低5位是7,对应SPS;0x68低5位是8,对应PPS;0x65低5位是5,对应IDR切片;0x41低5位是1,对应非IDR切片。这几个类型是实战中最常打交道的。

H265的NALU头是2字节,结构更复杂一些:

+---------------+---------------+ |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |F| Type | LayerId | TID | +---------------+---------------+

第一个字节的低6位是Type,取值范围0到63。常见的类型有:VPS是32,SPS是33,PPS是34,IDR_W_RADL是19,IDR_N_LP是20,TRAIL_R是1。注意H265多了个VPS(视频参数集),这是H264没有的,解析H265的时候必须把VPS、SPS、PPS都拿到才能初始化解码器。

2.3 AVCC格式的长度前缀机制

再看AVCC格式。同样一段数据,在AVCC里可能是这样:

00 00 00 1A 67 42 C0 1E ... // 前4字节是长度0x1A=26,后面26字节是SPS 00 00 00 08 68 CE 3C 80 ... // 前4字节是长度8,后面8字节是PPS 00 00 00 5C 65 88 84 ... // 前4字节是长度0x5C=92,后面92字节是IDR切片

这里没有起始码,每个NALU前面是长度字段。长度字段的字节数由配置决定,通常是4字节,也可能是2字节。这个信息记录在AVCDecoderConfigurationRecord里,也就是MP4文件avcCbox的内容。

AVCDecoderConfigurationRecord的结构大致是:

字段字节数说明
configurationVersion1版本,通常为1
AVCProfileIndication1SPS里的profile
profile_compatibility1SPS里的兼容性
AVCLevelIndication1SPS里的level
lengthSizeMinusOne1长度字段字节数减1,低2位有效
numOfSequenceParameterSets1SPS数量,低5位有效
......SPS、PPS数据

关键就是lengthSizeMinusOne这个字段。如果它的值是3,说明长度字段是4字节;如果是1,说明是2字节。解析AVCC码流之前,必须先从这个配置里读出长度字段的字节数,否则你根本不知道该怎么读长度。

H265对应的配置结构叫HEVCDecoderConfigurationRecord,存在MP4的hvcCbox里,结构更复杂,包含VPS、SPS、PPS以及一堆参数。但核心逻辑一样:先读配置,拿到长度字段大小,再按长度切NALU。

2.4 两种格式的适用场景对比

为什么会有两种格式?因为它们解决的是不同场景的问题。

Annex-B的优势在于流式友好。你从网络收到一段数据,不需要知道总长度,只要扫描起始码就能找到NALU边界,丢包了也能从下一个起始码重新同步。所以RTP打包、RTSP传输、TS流里,用的都是Annex-B。它的缺点是随机访问差,你想跳到第100个NALU,必须从头扫描,没法直接定位。

AVCC的优势在于随机访问友好。每个NALU有明确长度,你可以直接跳到任意位置读取,不需要扫描。所以MP4这种需要seek的文件格式用AVCC。它的缺点是流式传输不友好,如果长度字段丢了或者错了,后面全乱套,没法重新同步。

对比维度Annex-BAVCC/HVCC
NALU边界识别起始码0x000001/0x00000001长度前缀
随机访问差,需扫描好,可直接定位
流式传输好,可重新同步差,错误会传播
常见场景RTP、RTSP、TS、裸流文件MP4、MKV、FLV
参数集位置在码流中,随流传输在容器配置box中
长度字段无2或4字节,由配置决定

搞清这张表,你基本就能判断一段码流该用什么方式解析了。

3. 用代码判断一段码流到底是什么类型

3.1 肉眼判断:看开头几个字节

拿到一段未知的H264/H265数据,最快的判断方法是用十六进制工具看开头。如果开头是00 00 00 01或00 00 01,那基本可以确定是Annex-B格式。如果开头是00 00 00 XX(XX不是01)或者00 00 XX XX,而且后面跟着的数据看起来像NALU头,那可能是AVCC格式。

但这里有个坑:AVCC的长度字段也可能是00 00 00 01。比如某个NALU长度恰好是1,那长度字段就是00 00 00 01,看起来和起始码一模一样。怎么区分?看后面的数据。如果00 00 00 01后面跟着的字节低5位是合法的NALU类型(比如7、8、5、1),那可能是起始码;如果后面跟着的字节看起来不像NALU头,那可能是长度。这种歧义在实战中确实会遇到,所以不能只靠开头判断,要结合上下文。

3.2 写个扫描函数自动识别

更靠谱的方法是写个扫描函数,统计起始码出现的规律。下面这段Python代码可以帮你快速判断:

def detect_stream_type(data): """ 判断码流是Annex-B还是AVCC 返回 'annexb' / 'avcc' / 'unknown' """ if len(data) < 8: return 'unknown' # 统计起始码出现次数 start_code_3 = 0 start_code_4 = 0 for i in range(len(data) - 3): if data[i:i+3] == b'\x00\x00\x01': start_code_3 += 1 if i < len(data) - 4 and data[i:i+4] == b'\x00\x00\x00\x01': start_code_4 += 1 # 如果起始码出现次数较多,且分布有规律,倾向Annex-B if start_code_3 + start_code_4 > 3: # 进一步验证:起始码后面的NALU类型是否合法 valid_count = 0 for i in range(len(data) - 4): if data[i:i+4] == b'\x00\x00\x00\x01': nalu_type = data[i+4] & 0x1F if nalu_type in [1, 5, 6, 7, 8, 9]: valid_count += 1 if valid_count >= 2: return 'annexb' # 尝试按AVCC解析:读4字节长度,看是否能连续解析 pos = 0 nalu_count = 0 while pos + 4 <= len(data): length = int.from_bytes(data[pos:pos+4], 'big') if length == 0 or pos + 4 + length > len(data): break nalu_type = data[pos+4] & 0x1F if nalu_type not in [1, 5, 6, 7, 8, 9]: break pos += 4 + length nalu_count += 1 if nalu_count >= 2 and pos >= len(data) - 8: return 'avcc' return 'unknown'

这段代码的思路是:先统计起始码,如果起始码多且后面的NALU类型合法,就判为Annex-B;否则尝试按AVCC的长度前缀连续解析,如果能连续解析多个NALU且刚好走到数据末尾,就判为AVCC。实测下来,对大多数正常码流都能判断准确。

注意:这个函数对短数据或者损坏数据可能判断不准。实战中最好结合数据来源判断,比如从MP4里读出来的基本是AVCC,从RTP里收到的基本是Annex-B。

3.3 从容器里读配置确认长度字段

如果你是从MP4文件里读视频数据,那更可靠的方法是直接读avcC或hvcCbox,从里面拿lengthSizeMinusOne。下面是从MP4里解析avcC的简化逻辑:

def parse_avcc_config(data): """ 解析AVCDecoderConfigurationRecord data是avcC box的内容(不含box header) """ if len(data) < 7: return None config = {} config['configurationVersion'] = data[0] config['AVCProfileIndication'] = data[1] config['profile_compatibility'] = data[2] config['AVCLevelIndication'] = data[3] # 低2位有效,加1就是长度字段字节数 config['lengthSize'] = (data[4] & 0x03) + 1 config['numOfSPS'] = data[5] & 0x1F pos = 6 sps_list = [] for _ in range(config['numOfSPS']): sps_len = int.from_bytes(data[pos:pos+2], 'big') pos += 2 sps_list.append(data[pos:pos+sps_len]) pos += sps_len config['SPS'] = sps_list if pos < len(data): config['numOfPPS'] = data[pos] pos += 1 pps_list = [] for _ in range(config['numOfPPS']): pps_len = int.from_bytes(data[pos:pos+2], 'big') pos += 2 pps_list.append(data[pos:pos+pps_len]) pos += pps_len config['PPS'] = pps_list return config

拿到lengthSize之后,解析AVCC码流就简单了:每次读lengthSize个字节作为长度,再读对应长度的数据作为一个NALU。H265的hvcC解析类似,只是字段更多,还要处理VPS。

4. 格式转换:Annex-B与AVCC互转的实操细节

4.1 Annex-B转AVCC:剥起始码加长度

实际项目里最常见的需求是把Annex-B转成AVCC,比如你要把RTSP收到的流存成MP4。转换逻辑不复杂,但有几个细节容易出错。

第一步是切分NALU。扫描起始码,把每个NALU的数据部分提取出来。注意起始码可能是3字节也可能是4字节,要都处理。还要注意一种特殊情况:NALU数据内部可能出现00 00 01这样的字节序列,这叫防竞争字节(emulation prevention bytes),编码时会在00 00 00、00 00 01、00 00 02、00 00 03这些序列的第三个字节前插入0x03来避免和起始码冲突。解析的时候要把这些0x03去掉,这叫去防竞争。

def remove_emulation_prevention(data): """去掉防竞争字节0x03""" result = bytearray() i = 0 while i < len(data): if i + 2 < len(data) and data[i] == 0 and data[i+1] == 0 and data[i+2] == 3: result.append(0) result.append(0) i += 3 # 跳过0x03 else: result.append(data[i]) i += 1 return bytes(result) def annexb_to_avcc(data, length_size=4): """Annex-B转AVCC""" nalus = [] i = 0 # 先找到所有起始码位置 positions = [] while i < len(data) - 3: if data[i:i+4] == b'\x00\x00\x00\x01': positions.append((i, 4)) i += 4 elif data[i:i+3] == b'\x00\x00\x01': positions.append((i, 3)) i += 3 else: i += 1 # 按起始码切分NALU for idx, (pos, sc_len) in enumerate(positions): start = pos + sc_len if idx + 1 < len(positions): end = positions[idx+1][0] else: end = len(data) nalu = data[start:end] # 去掉末尾可能的填充0 nalu = nalu.rstrip(b'\x00') if nalu: nalus.append(nalu) # 组装AVCC result = bytearray() for nalu in nalus: result += len(nalu).to_bytes(length_size, 'big') result += nalu return bytes(result)

这里有个细节:去防竞争应该在切分NALU之后、组装之前做,而且只对NALU的载荷部分做,NALU头不做。不过实际存储时,很多场景下不去防竞争也能工作,因为解码器自己会处理。但如果你要做NALU内容分析,比如提取SPS里的分辨率信息,那就必须去防竞争。

4.2 AVCC转Annex-B:加起始码

反向转换更简单,读长度、取数据、加起始码:

def avcc_to_annexb(data, length_size=4): """AVCC转Annex-B""" result = bytearray() pos = 0 while pos + length_size <= len(data): length = int.from_bytes(data[pos:pos+length_size], 'big') pos += length_size if pos + length > len(data): break nalu = data[pos:pos+length] pos += length # 加4字节起始码 result += b'\x00\x00\x00\x01' result += nalu return bytes(result)

看起来简单,但实战中要注意:转换后的Annex-B码流,参数集(SPS/PPS/VPS)可能不在里面。因为AVCC格式的参数集存在容器配置里,不在码流中。如果你直接把AVCC转成Annex-B喂给解码器,解码器可能因为拿不到参数集而报错。解决办法是先把配置里的SPS/PPS/VPS取出来,转成Annex-B格式,插到码流最前面。

def build_annexb_with_params(config, avcc_data, length_size=4): """把配置里的参数集和AVCC数据组装成完整Annex-B""" result = bytearray() # 先加VPS(H265才有) if 'VPS' in config: for vps in config['VPS']: result += b'\x00\x00\x00\x01' + vps # 加SPS for sps in config.get('SPS', []): result += b'\x00\x00\x00\x01' + sps # 加PPS for pps in config.get('PPS', []): result += b'\x00\x00\x00\x01' + pps # 加视频数据 result += avcc_to_annexb(avcc_data, length_size) return bytes(result)

4.3 转换中最容易踩的三个坑

第一个坑:起始码长度不统一。有些编码器输出的Annex-B,SPS/PPS用4字节起始码,切片用3字节。转换的时候如果只按一种长度扫描,会漏掉NALU。我的做法是统一按"先匹配4字节,再匹配3字节"的顺序扫描,这样不会漏。

第二个坑:NALU末尾的填充字节。有些码流在NALU末尾会补0对齐,转换时如果不去掉,会导致长度字段偏大,解码器读到多余的0可能报错。我一般会在切分后做一次rstrip(b'\x00'),但要注意如果NALU本身以0结尾(概率极低),可能会误删。更稳妥的做法是结合NALU类型判断,不过实战中直接rstrip基本够用。

第三个坑:参数集重复插入。有些Annex-B码流里,每个IDR帧前面都会重复插入SPS/PPS。转AVCC的时候,如果把这些重复的参数集都当普通NALU存进去,会导致码流冗余。正确做法是把参数集提取到配置里,码流中只保留切片数据。但有些场景下又需要保留,比如做流式传输时,所以要看具体需求。

5. 实战排查:黑屏、花屏、解码失败的定位链路

5.1 黑屏但无报错:大概率是参数集没送到

最常见的现象是:解码器初始化成功,但解码出来全是黑屏,也没有报错。这种情况十有八九是参数集没送到解码器。

H264/H265解码器在解码第一个IDR帧之前,必须先拿到SPS和PPS(H265还要VPS)。如果码流是AVCC格式,参数集在容器配置里,你只把视频数据喂给解码器,它拿不到参数集,就只能输出黑屏。解决办法就是上面说的,先把配置里的参数集转成Annex-B插到码流前面。

排查方法:打印一下你喂给解码器的前几个NALU类型。如果全是1(非IDR切片)或者5(IDR切片),没有7(SPS)、8(PPS),那基本可以确定是这个问题。

5.2 花屏、马赛克:NALU边界切错了

花屏通常意味着NALU边界切分错误,导致解码器读到了错误的数据。常见原因有几个:

一是长度字段字节数搞错了。AVCC的长度字段可能是2字节也可能是4字节,如果你固定按4字节读,而实际是2字节,那读出来的长度就是错的,后面全乱。排查方法是打印前几个长度值,看看是否合理。如果长度值大得离谱(比如几MB),那基本是字节数搞错了。

二是防竞争字节没处理。如果你在做NALU内容分析时没去防竞争,可能会把00 00 03 01误判成起始码,导致NALU被切碎。排查方法是搜索码流里00 00 03的出现位置,看看是否在NALU内部。

三是起始码扫描不完整。前面说过,起始码有3字节和4字节两种,如果只扫一种,会漏掉NALU。排查方法是统计一下两种起始码的数量,看看是否合理。

5.3 解码器报"nal unit type unknown":类型字段读错了

这个报错很直接:解码器读到的NALU类型不在它认识的范围内。原因通常是NALU头的位置读错了。

在Annex-B里,NALU头紧跟在起始码后面。如果你把起始码长度算错了,比如把3字节起始码当成4字节,那NALU头就会偏移一个字节,读出来的类型自然是错的。排查方法是打印起始码后面的第一个字节,看看低5位(H264)或低6位(H265)是否在合法范围内。

在AVCC里,NALU头紧跟在长度字段后面。如果长度字段字节数搞错,NALU头位置也会错。排查方法同上。

还有一种可能是H264和H265搞混了。H264的NALU头是1字节,H265是2字节。如果你拿H264的解析逻辑去解析H265,类型字段肯定读不对。排查方法是看SPS的长度和内容,H265的SPS通常比H264长,而且H265有VPS。

5.4 一个完整的排查流程

我把上面这些整理成一个排查流程,遇到问题可以按这个顺序走:

  1. 确认码流类型:用十六进制工具看开头,或者用前面的detect_stream_type函数判断。
  2. 确认参数集是否到位:打印前几个NALU类型,看有没有SPS/PPS/VPS。
  3. 确认长度字段字节数:如果是AVCC,从配置里读lengthSizeMinusOne,确认是2还是4。
  4. 确认NALU边界:打印每个NALU的长度和类型,看看是否合理。
  5. 确认防竞争处理:搜索00 00 03,看看是否需要去防竞争。
  6. 确认H264/H265:看SPS内容或者NALU头字节数,确认编码格式。

这个流程我用了很多次,基本能覆盖90%以上的码流解析问题。

6. 几个容易被忽略的边界情况

6.1 多Slice码流:一个帧不止一个NALU

很多人以为一个NALU就是一个帧,其实不对。H264/H265支持多Slice编码,一个帧可以被切成多个Slice,每个Slice是一个独立的NALU。这种情况下,你不能按"一个NALU一帧"来处理,而要按NALU里的first_mb_in_slice字段判断帧边界。

多Slice在监控场景里很常见,因为可以并行编码。处理的时候要注意:解码器需要拿到一个帧的所有Slice才能解出完整图像。如果你按NALU逐个送,解码器可能会等齐了才输出。排查花屏问题时,如果发现只有部分区域花,很可能就是Slice没送全。

6.2 SEI NALU:别把它当视频数据

SEI(Supplemental Enhancement Information)是补充增强信息,类型是6(H264)或39/40(H265)。它不包含图像数据,但可能包含重要信息,比如时间戳、HDR元数据。解析的时候要把SEI单独处理,不要混到视频数据里。

有些场景下SEI还承载了自定义信息,比如某些厂商会在SEI里放编码参数。如果你做视频分析,SEI里的信息可能有用。但如果你只是做解码播放,SEI可以直接跳过。

6.3 参数集更新:中途换分辨率怎么办

直播场景里,有时候会中途切换分辨率,这时候编码器会重新发送SPS/PPS。如果你只在一开始解析了参数集,后面不更新,解码器就会用旧参数解新数据,导致花屏或者解码失败。

正确做法是:每次遇到SPS/PPS都重新解析并更新解码器配置。但要注意,更新参数集要在合适的时机,通常是在下一个IDR帧之前。如果在中途更新,可能会导致当前帧解码异常。

6.4 码流损坏后的重新同步

网络传输难免丢包,Annex-B格式的好处是可以在丢包后重新同步:从下一个起始码开始继续解析。但AVCC格式就没这个能力,长度字段错了后面全乱。

实战中,如果收到AVCC格式的流式数据,建议先转成Annex-B再处理,这样容错性更好。或者在做AVCC解析时加校验:如果读出的长度明显不合理(比如超过剩余数据长度),就丢弃当前数据,等待下一个关键帧。

7. 我在这块踩过的坑和总结的经验

说几个我实际踩过的坑,都是文档里不会写的。

第一个坑:以为.h264文件一定是Annex-B。有一次拿到一个.h264文件,按Annex-B解析死活不对,后来发现它其实是AVCC格式,只是后缀写成了.h264。所以不要相信文件后缀,要看实际内容。判断方法就是前面说的看开头字节。

第二个坑:忽略了lengthSizeMinusOne的低2位。这个字段只有低2位有效,高6位是保留的。我一开始直接读整个字节,结果读出来是0xFF,算出来长度字段是256字节,完全不对。正确做法是(byte & 0x03) + 1。

第三个坑:H265的NALU头是2字节,但类型字段在第一个字节的低6位。我一开始以为类型在第二个字节,读出来全是错的。后来查了规范才知道,第一个字节的低6位是类型,第二个字节的高1位是LayerId的一部分,低3位是TID。

第四个坑:防竞争字节去掉后,NALU长度变了,但长度字段没更新。如果你先去掉防竞争再算长度,那长度字段是对的;但如果先去防竞争再按原长度读,就会读错。所以去防竞争和长度计算要一起做,不能分开。

第五个坑:多Slice码流里,非首个Slice的NALU头第一个bit是0。这个bit叫forbidden_zero_bit,正常应该是0。但有些编码器会把它设成1,导致解析异常。遇到这种情况,可以忽略这个bit,只看类型字段。

最后分享一个实用技巧:写一个NALU分析工具,把码流里所有NALU的类型、长度、位置打印出来。这个工具我用了好几年,排查问题时特别有用。你可以用Python快速写一个,输入码流文件,输出每个NALU的详细信息。有了这个工具,前面说的那些问题基本都能快速定位。

def analyze_nalus(data, is_annexb=True, length_size=4): """分析码流里的所有NALU""" nalus = [] if is_annexb: positions = [] i = 0 while i < len(data) - 3: if data[i:i+4] == b'\x00\x00\x00\x01': positions.append((i, 4)) i += 4 elif data[i:i+3] == b'\x00\x00\x01': positions.append((i, 3)) i += 3 else: i += 1 for idx, (pos, sc_len) in enumerate(positions): start = pos + sc_len end = positions[idx+1][0] if idx+1 < len(positions) else len(data) nalu = data[start:end].rstrip(b'\x00') if nalu: nalus.append(nalu) else: pos = 0 while pos + length_size <= len(data): length = int.from_bytes(data[pos:pos+length_size], 'big') pos += length_size if pos + length > len(data): break nalus.append(data[pos:pos+length]) pos += length for idx, nalu in enumerate(nalus): if len(nalu) == 0: continue nalu_type = nalu[0] & 0x1F # H264 print(f"NALU {idx}: type={nalu_type}, len={len(nalu)}, " f"first_bytes={nalu[:4].hex()}") return nalus

这个工具输出的是H264的类型,如果是H265,把& 0x1F改成& 0x3F,并且注意NALU头是2字节。有了它,你拿到任何一段码流都能快速看清结构,排查问题效率能提高一大截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询