1. 码流类型到底在聊什么
刚接触音视频编解码这块的朋友,看到“H264/H265码流类型”这个词,第一反应往往是懵的。码流不就是视频数据吗,怎么还有类型之分?我当初也是这么想的,直到有一次在项目里做流媒体转发,发现同样的H264数据,有的播放器能直接解,有的死活黑屏,排查了大半天才意识到——问题出在码流类型上。
简单来说,码流类型指的是编码后的视频数据在传输和存储时采用的封装组织方式。H264和H265作为两大主流视频编码标准,它们产出的裸码流并不是一坨无结构的数据,而是按照特定规则组织起来的。这个组织规则的不同,就产生了不同的码流类型。最常见的两种分类维度是:Annex-B格式和AVCC格式,以及裸流和封装流。
这篇文章适合谁看?如果你在做音视频开发、流媒体服务搭建、播放器适配,或者只是单纯想搞明白为什么同一个视频文件换个场景就播不了,那这篇内容应该能帮你省下不少排查时间。我会从实际项目出发,把H264和H265的码流类型掰开揉碎讲清楚,包括它们长什么样、怎么判断、怎么转换、踩过哪些坑。
2. 为什么码流类型这么重要
2.1 一个真实踩坑场景
之前做过一个跨平台播放项目,服务端用某开源工具把视频转成H264裸流推给客户端。测试的时候在PC浏览器上一切正常,结果到了移动端播放器上直接花屏。抓包一看,数据本身没问题,解码器也没报错,但就是渲染不出来。后来定位到原因:服务端输出的是Annex-B格式的码流,而移动端播放器的解码接口期望的是AVCC格式。两种格式的起始码和NALU长度描述方式完全不同,解码器拿到不认识的格式,自然就罢工了。
这个坑让我意识到,码流类型不是学术概念,而是直接影响能不能跑起来的关键细节。很多文档只告诉你H264怎么编码,却不告诉你编码后的数据该怎么组织、怎么传给下游,结果就是各种“玄学”问题。
2.2 不同场景对码流类型的偏好
为什么会有不同的码流类型?因为不同场景的需求不一样。
实时流传输场景,比如视频通话、直播推流,通常用Annex-B格式。这种格式用起始码来分隔每个NALU,解析起来简单直接,适合边收边解。你不需要知道整个流有多长,收到一个起始码就知道一个新的NALU开始了。
文件存储和MP4封装场景,通常用AVCC格式。这种格式在NALU前面加长度字段,方便随机访问和跳转。MP4文件里的视频轨道,几乎都是AVCC格式的。
还有一种是裸流和封装流的区别。裸流就是纯编码数据,没有容器包裹;封装流则是放在MP4、MKV、TS等容器里的数据。容器会决定码流以什么形式存储,比如MP4用AVCC,TS用Annex-B。
注意:判断码流类型不能只看编码标准,H264和H265都有Annex-B和AVCC两种形式,关键看数据是怎么组织的。
3. H264码流类型深度拆解
3.1 Annex-B格式:起始码分隔一切
Annex-B格式的核心特征是起始码。每个NALU前面会有一个起始码,通常是0x00000001或0x000001。解码器通过扫描起始码来切分NALU,不需要额外的长度信息。
一个典型的Annex-B码流长这样:
00 00 00 01 67 64 00 1F ... (SPS) 00 00 00 01 68 EB EC B2 ... (PPS) 00 00 00 01 65 88 84 ... (IDR帧数据) 00 00 00 01 41 9A 02 ... (非IDR帧数据)每个00 00 00 01后面跟着的就是一个NALU。第一个字节是NALU头,包含NALU类型等信息。对于H264,NALU类型在第一个字节的低5位;对于H265,NALU类型在第一个字节的1到6位,这个差异后面会细说。
Annex-B格式的优点很明显:流式解析友好。你不需要预先知道每个NALU的长度,收到数据就扫描起始码,扫到了就切分。这对于实时传输场景非常合适,因为数据是源源不断来的,你没法预知下一帧有多大。
但缺点也很明显:随机访问困难。如果你想跳到视频的第10分钟,用Annex-B格式就得从头扫描起始码,效率很低。而且起始码本身会占用额外字节,虽然不多,但在海量数据下也是开销。
3.2 AVCC格式:长度字段说了算
AVCC格式的核心特征是长度前缀。每个NALU前面用固定字节数(通常是4字节)表示这个NALU的长度,没有起始码。
一个典型的AVCC码流长这样:
00 00 00 1A 67 64 00 1F ... (长度0x1A,后面是SPS) 00 00 00 08 68 EB EC B2 ... (长度0x08,后面是PPS) 00 00 12 34 65 88 84 ... (长度0x1234,后面是IDR帧数据)每个NALU前面的4个字节是大端序的长度值,解码器先读长度,再读对应字节数的数据。这种方式随机访问非常方便,因为你可以直接跳到某个位置,读长度,然后跳过对应字节数找到下一个NALU。
AVCC格式通常还会配合extradata使用。extradata里存放SPS和PPS,不放在码流里。这样每个NALU就纯粹是帧数据,结构更紧凑。MP4文件里的avcC盒子就是干这个的,它记录了SPS、PPS以及NALU长度字段的字节数。
提示:AVCC格式的NALU长度字段字节数不固定,可能是1、2、4字节,具体看extradata里的lengthSizeMinusOne字段。大多数情况是4字节,但别假设一定是4。
3.3 两种格式的转换实操
实际项目中经常需要在Annex-B和AVCC之间转换。比如从RTSP流(Annex-B)拿到数据,要存成MP4文件(AVCC),就得做转换。
Annex-B转AVCC的思路:
- 扫描起始码,切分出所有NALU
- 提取SPS和PPS,放入extradata
- 对每个NALU,去掉起始码,在前面加上4字节长度
- 把处理后的NALU拼接起来
用Python写个简化版转换逻辑:
def annexb_to_avcc(data): nals = [] i = 0 while i < len(data): # 找起始码 if data[i:i+4] == b'\x00\x00\x00\x01': start = i + 4 i += 4 elif data[i:i+3] == b'\x00\x00\x01': start = i + 3 i += 3 else: i += 1 continue # 找下一个起始码 j = i while j < len(data): if data[j:j+4] == b'\x00\x00\x00\x01' or data[j:j+3] == b'\x00\x00\x01': break j += 1 nals.append(data[start:j]) i = j # 转成AVCC result = b'' for nal in nals: result += len(nal).to_bytes(4, 'big') + nal return resultAVCC转Annex-B就反过来:读4字节长度,取出NALU,在前面加起始码。
def avcc_to_annexb(data): result = b'' i = 0 while i < len(data): length = int.from_bytes(data[i:i+4], 'big') i += 4 nal = data[i:i+length] result += b'\x00\x00\x00\x01' + nal i += length return result这两个函数是最简版本,实际用的时候要处理边界情况,比如数据不完整、长度字段字节数不是4等。
3.4 H264 NALU类型速查
理解码流类型,绕不开NALU类型。H264的NALU头是1字节,低5位是类型:
| NALU类型值 | 名称 | 说明 |
|---|---|---|
| 1 | 非IDR片 | 普通帧数据 |
| 5 | IDR片 | 关键帧,可独立解码 |
| 6 | SEI | 补充增强信息 |
| 7 | SPS | 序列参数集 |
| 8 | PPS | 图像参数集 |
| 9 | AUD | 访问单元分隔符 |
SPS和PPS是解码的“说明书”,没有它们解码器不知道分辨率、帧率等信息。IDR帧是随机访问的入口,播放器跳转时通常找最近的IDR帧开始解码。
实操心得:有些场景下SPS/PPS会重复发送,比如每个IDR帧前都带一份。这不是冗余,而是为了容错——万一中间丢了,后面还能恢复。但存文件时通常只保留一份,放在extradata里。
4. H265码流类型与差异对比
4.1 H265的NALU头变化
H265的NALU头和H264不一样,变成了2字节。类型字段在第一个字节的1到6位,共6位,能表示更多类型。第二个字节包含层ID和时域ID等信息。
H265常见NALU类型:
| NALU类型值 | 名称 | 说明 |
|---|---|---|
| 0-9 | 非IRAP | 普通帧数据 |
| 16-21 | IRAP | 关键帧相关 |
| 32 | VPS | 视频参数集 |
| 33 | SPS | 序列参数集 |
| 34 | PPS | 图像参数集 |
| 39 | SEI | 补充增强信息 |
注意H265多了个VPS,视频参数集。它比SPS层级更高,描述整个视频序列的全局信息。解码H265时,VPS、SPS、PPS三者缺一不可。
4.2 H265的Annex-B和AVCC
H265同样有Annex-B和AVCC两种格式,起始码和长度前缀的逻辑跟H264一样。区别在于:
- NALU头是2字节,解析类型时要取第一个字节的1-6位
- extradata结构更复杂,包含VPS、SPS、PPS三组参数
- AVCC格式的extradata里,参数集数量可能不止三个
判断H265码流类型的方法跟H264类似:看有没有起始码。有起始码就是Annex-B,有长度前缀就是AVCC。
4.3 H264和H265码流类型对比表
| 对比项 | H264 | H265 |
|---|---|---|
| NALU头长度 | 1字节 | 2字节 |
| 类型字段位置 | 低5位 | 第一个字节的1-6位 |
| 参数集 | SPS、PPS | VPS、SPS、PPS |
| 关键帧类型 | IDR(5) | IRAP(16-21) |
| Annex-B起始码 | 0x000001或0x00000001 | 同左 |
| AVCC长度前缀 | 通常4字节 | 通常4字节 |
| extradata内容 | avcC盒子 | hvcC盒子 |
这个表建议存下来,排查问题时对照看,能快速定位是哪种码流、哪个环节出了问题。
注意:H265的IRAP类型范围是16到21,其中16是BLA,17-18是IDR,19-20是CRA,21是保留。不同关键帧类型的随机访问行为有差异,做seek的时候要注意。
5. 实战:如何判断和转换码流类型
5.1 快速判断码流类型的脚本
拿到一段码流,怎么知道它是Annex-B还是AVCC?写个脚本扫一下:
def detect_stream_type(data): # 检查开头是否有起始码 if data[:4] == b'\x00\x00\x00\x01' or data[:3] == b'\x00\x00\x01': return "Annex-B" # 检查是否是AVCC:读4字节长度,看是否合理 if len(data) >= 4: length = int.from_bytes(data[:4], 'big') if 0 < length < len(data) - 4: # 进一步验证:跳过length后,下一个长度是否也合理 next_pos = 4 + length if next_pos + 4 <= len(data): next_length = int.from_bytes(data[next_pos:next_pos+4], 'big') if 0 < next_length < len(data) - next_pos - 4: return "AVCC" return "Unknown"这个脚本的逻辑是:先看有没有起始码,有就是Annex-B;没有就假设是AVCC,读长度字段验证一下。如果连续两个长度都合理,基本可以确定是AVCC。
实际用的时候,建议多验证几个NALU,避免误判。有些数据开头可能是SEI或其他类型,长度字段看起来可能不“合理”,多试几个位置更稳。
5.2 从容器中提取码流类型信息
如果你拿到的是MP4文件,可以用工具看它的码流类型。比如用ffprobe:
ffprobe -show_streams -select_streams v input.mp4输出里会显示codec_tag_string,如果是avc1就是H264 AVCC,hvc1或hev1就是H265 AVCC。MP4里的视频轨道基本都是AVCC格式,因为容器需要随机访问。
TS流则通常是Annex-B格式,因为TS是流式传输,用起始码分隔更自然。你可以用:
ffprobe -show_streams -select_streams v input.ts看codec_tag_string,如果是avc1但实际数据是Annex-B,说明容器和码流格式不匹配,播放器可能会出问题。
5.3 转换时的注意事项
做码流类型转换时,有几个坑我踩过:
第一,别忘了参数集。Annex-B转AVCC时,SPS和PPS要从码流里提取出来放到extradata。如果忘了这一步,AVCC格式的码流就没有解码所需的参数集,播放器直接黑屏。
第二,长度字段字节数要对齐。AVCC的extradata里有个lengthSizeMinusOne字段,表示长度字段用几个字节。大多数是4字节,但有些实现用2字节甚至1字节。转换时要跟下游对齐,不然长度解析全错。
第三,起始码可能是3字节或4字节。Annex-B的起始码有0x000001和0x00000001两种。扫描的时候两种都要认,不然会漏掉NALU。有些编码器会在每个NALU前用4字节,有些用3字节,甚至混用。
第四,H265的VPS不能丢。H265的extradata里必须包含VPS、SPS、PPS三组。少一个都可能导致解码失败。有些工具只提取SPS和PPS,忘了VPS,结果H265播不了。
实操心得:转换完成后,用ffplay或VLC实际播一下,别只看数据长度对不对。播放器能正常渲染,才算真正转换成功。
6. 常见问题与排查技巧
6.1 播放器黑屏/花屏排查思路
遇到播放器黑屏或花屏,按这个顺序排查:
- 确认码流类型:用上面的脚本判断是Annex-B还是AVCC,跟播放器期望的是否一致
- 检查参数集:SPS/PPS(H265还要VPS)是否存在且完整
- 验证NALU完整性:有没有截断的NALU,长度字段是否越界
- 看解码器日志:大多数解码器会输出具体错误,比如“missing SPS”或“invalid NALU length”
我遇到过一种情况:码流类型是对的,参数集也有,但播放器就是花屏。最后发现是NALU顺序错了——SPS和PPS必须在IDR帧之前,如果顺序颠倒,解码器可能来不及配置就遇到IDR帧,导致解码异常。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 黑屏无画面 | 码流类型不匹配 | 检查Annex-B/AVCC是否与播放器期望一致 |
| 花屏/绿屏 | 参数集缺失或错误 | 确认SPS/PPS/VPS存在且内容正确 |
| 播放几秒后卡住 | NALU长度字段错误 | 验证AVCC长度字段字节数和值 |
| 只有第一帧能播 | 关键帧间隔问题 | 检查IDR/IRAP帧是否定期出现 |
| 音频正常视频异常 | 视频码流类型问题 | 单独提取视频轨道分析 |
| H265播不了 | VPS缺失 | 确认extradata包含VPS |
6.3 独家避坑技巧
技巧一:用十六进制编辑器直接看。拿到一段码流,用hex编辑器打开,看开头几个字节。如果是00 00 00 01或00 00 01,基本就是Annex-B。如果是其他值,大概率是AVCC的长度字段。这个方法最快,不需要写代码。
技巧二:抓包时同时保存原始数据。排查流媒体问题时,别只看日志,把原始码流dump下来。日志可能骗人,但原始数据不会。我习惯在关键节点加个dump逻辑,把收到的数据存成文件,事后用脚本分析。
技巧三:H265的NALU类型解析要小心。H265的NALU头是2字节,类型在第一个字节的1-6位。解析时用(byte >> 1) & 0x3F,别用H264的byte & 0x1F,不然类型全错。
技巧四:转换后做round-trip验证。Annex-B转AVCC后,再转回Annex-B,对比原始数据。如果完全一致,说明转换无损。如果有差异,检查是不是参数集处理有问题。
技巧五:注意字节序。AVCC的长度字段是大端序,但有些平台默认小端序。写转换代码时明确指定big,别依赖默认行为。
7. 工具选型与实操建议
7.1 常用工具对比
| 工具 | 用途 | 优点 | 缺点 |
|---|---|---|---|
| ffmpeg | 格式转换、分析 | 功能全,支持H264/H265 | 命令行参数多,学习曲线陡 |
| ffprobe | 流信息分析 | 输出详细,易解析 | 只能分析,不能转换 |
| hex编辑器 | 直接看数据 | 直观,无需代码 | 大文件不方便 |
| 自写脚本 | 定制化分析 | 灵活,可集成 | 需要开发时间 |
| VLC | 播放验证 | 支持格式多 | 错误信息不够详细 |
ffmpeg是最常用的,比如把Annex-B转AVCC:
ffmpeg -i input.h264 -c:v copy -bsf:v h264_mp4toannexb output.mp4反过来:
ffmpeg -i input.mp4 -c:v copy -bsf:v h264_annexb_to_mp4 output.h264注意-c:v copy表示不重新编码,只做格式转换。这样速度快,而且无损。
7.2 实操建议
建议一:先确定下游期望什么格式。做转换之前,先搞清楚播放器或封装库期望Annex-B还是AVCC。别转了半天发现方向反了。
建议二:保留原始数据。转换前备份原始码流,出问题了可以回退对比。
建议三:小数据量先验证。别一上来就处理几个G的文件,先拿几帧数据测试转换逻辑,确认没问题再批量处理。
建议四:关注extradata。AVCC格式的extradata是解码的关键,转换时确保正确生成。可以用ffprobe查看extradata内容:
ffprobe -show_streams -select_streams v input.mp4 | grep extradata建议五:H265转换用专门工具。H265的码流结构比H264复杂,建议用ffmpeg的hevc_mp4toannexb和hevc_annexb_to_mp4,别自己手写转换逻辑,容易出错。
提示:ffmpeg的bitstream filter名字要记准,H264是
h264_mp4toannexb,H265是hevc_mp4toannexb,别搞混了。
8. 个人经验总结
码流类型这个问题,说大不大,说小不小。平时写业务代码可能接触不到,但一旦涉及底层传输、格式转换、播放器适配,就绕不开。我自己的经验是:遇到播放问题,先怀疑码流类型,再怀疑参数集,最后才怀疑解码器。这个排查顺序帮我省了很多时间。
另外,H264和H265虽然都是视频编码标准,但码流层面的差异不少。H265多了VPS,NALU头变成2字节,类型字段位置也变了。做H265相关开发时,别直接套用H264的经验,该查文档就查文档。
最后分享一个小技巧:如果你不确定一段码流是什么类型,用ffmpeg跑一下ffprobe -show_frames,看它能不能正常解析出帧信息。能解析出来,说明码流类型和参数集基本没问题;解析报错,就往码流类型方向排查。这个方法简单粗暴,但很有效。