MP4 里用长度前缀标帧边界,其它场合用起始码。 h264_mp4toannexb 干的就是把前者换成后者,一个字节都不重压。
4.2.1 把流拆了出来,这一节讲拆出来之后为什么常常「放不了」。 4.3.2 转 AVI 时还会撞上同一个问题。
一、两种写法,开头四个字节就看得出来
同一个编码,装在不同地方,写法可以不一样:
写法 开头是什么 用在哪
长度前缀(AVCC) 00 00 02 A0 这样的长度 MP4、MKV
起始码(Annex B) 00 00 00 01 裸流、TS、AVI
第一行那四字节是「这一帧有多少字节」,读起来就能直接跳到下一帧。 第二行那四字节只是一个记号,说「从这里开始是一帧」,长度得靠往后扫。
两种写法各有好处:AVCC 能随机访问,Annex B 能一路顺序扫。 所以 MP4 选了前者,广播与裸流选了后者——不是谁更先进。
二、转一遍就能看到变化
用位流滤镜过一遍,前四个字节立刻变了:
项 值
MP4 里首包前 8 字节 00 00 02 A0 06 05 FF FF 过一遍滤镜后 00 00 00 01 06 05 FF FF
后面那四个字节 06 05 FF FF 一模一样,只有开头被替换了。
这正好证明它是「改记号」而不是「重编码」——画面数据一个字节没动。
所以这个操作极快,几乎等于内存里搬一遍。
命令行里对应 -bsf:v h264_mp4toannexb,bsf 就是 bitstream filter。
三、不转会怎样
症状很有迷惑性:文件能生成、大小也正常,就是放不出来。
场合 要哪种 不转的后果
MP4 内部 AVCC — 存成 .h264 裸流 Annex B 播放器解不出第一帧 封进 MPEG-TS Annex B 打包器认不出帧边界 绕进 AVI Annex B 封装时直接报 InvalidData
第二行是最常见的坑:从 mp4 里抠出 h264 存成 .h264,双击放不了。
文件没坏,是写法不匹配——播放器按起始码找帧,一个都找不到。
第四行更干脆:AVI 的封装器直接拒绝,报 InvalidData。
报错比静默坏文件好得多,4.3.2 会拿这个当场演示一遍。
四、动手 Demo:首包前后各 8 个字节,对照两种写法
取 MP4 里第一帧的包,先看它原始的开头,再用位流滤镜换一遍写法。 只对比前 8 个字节,就能看出「长度前缀」怎么变成「起始码」。
import av
c = av.open(u"m4.mp4")
pk0 = None
for pk in c.demux(video=0):
if pk.size:
pk0 = pk
break
print(u"原始 %s" % u" ".join(u"%02X" % b for b in bytes(pk0)[:8]))
bsf = av.BitStreamFilterContext(u"h264_mp4toannexb", c.streams.video[0])
pkts = bsf.filter(pk0)
out = pkts[0]
print(u"转换 %s" % u" ".join(u"%02X" % b for b in bytes(out)[:8]))
c.close()
print(u"位流滤镜总数 %d" % len(av.bitstream_filters_available))
实跑输出:
--------------------------------------------------------------------
4.2.2 h264_mp4toannexb——两种码流的写法差在哪
--------------------------------------------------------------------
**① 同一个包里,开头四个字节说明了写法**
项 值
---------------------------------------------------
MP4 里首包前 8 字节 00 00 02 A0 06 05 FF FF
这 4 个字节的含义 后面这一帧有多长
**MP4 用的是「长度前缀」:先写 4 个字节说这一帧多长,再写帧本身。**
**这种写法叫 AVCC,好处是能直接定位到任意一帧,不用扫描。**
**② 换成 Annex B:把长度前缀换成起始码**
项 值
----------------------------------------------------
支持的位流滤镜 49 个(含 h264_mp4toannexb)
转换后前 8 字节 00 00 00 01 06 05 FF FF
**转换后的开头变成 `00 00 00 01`——这就是起始码,一个「从这里开始是一帧」的记号。**
**Annex B 常见于裸流文件(.h264)与 TS 流,因为它能一路顺序扫下去。**
**③ 什么时候必须转**
场合 要哪种写法 不转的后果
----------------------------------------------------------------
MP4 内部 长度前缀(AVCC) —
存成 .h264 裸流 起始码(Annex B) 播放器解不出第一帧
封进 MPEG-TS 起始码(Annex B) 打包器认不出帧边界
**这就是「把 MP4 里的 H.264 直接抠出来存成 .h264 会放不了」的原因。**
**它不是坏了,是写法不匹配——所以 ffmpeg 里那个 `-bsf:v h264_mp4toannexb` 才有存在的必要。**
bsf.filter() 返回的是一个列表而不是单个包:一个输入包有可能变成多个输出包。
所以取 pkts[0];真实代码里应该把整个列表都写出去。
构造滤镜时必须把输入流传进去:h264_mp4toannexb 需要读 SPS/PPS 才知道帧长怎么算。
只传名字不传流,它会初始化失败。
本机这份构建里有 49 个位流滤镜,最常用的三个是:
h264_mp4toannexb、hevc_mp4toannexb、aac_adtstoasc(反向,把 ADTS 转成 ASC)。
30 秒实验
- 抠裸流:
ffmpeg -i 片子.mp4 -c:v copy -bsf:v h264_mp4toannexb 出.h264,再双击试试。 - 抠音频 ADTS:
ffmpeg -i 片子.mp4 -c:a copy -f adts 出.aac,这是另一个方向的转换。 - 看有哪些滤镜:
ffmpeg -bsfs,输出就是那四十几个名字。
常见误区
- 以为
.h264放不了是文件坏了。是写法(Annex B)与播放器预期不符,过一遍滤镜就好。 - 把
bsf.filter()的返回值当单个包用。它返回列表,一个输入包可能变成多个输出包。 - 以为转一遍会掉画质。它只替换开头那 4 个字节,画面数据一个字节没动。
自测三问(点开对答案)
1. AVCC 与 Annex B 最直观的区别在哪? —— AVCC 用 4 字节长度前缀标帧长,Annex B 用 `00 00 00 01` 起始码标帧开始。 2. 为什么 MP4 选 AVCC? —— 因为长度写在前面,可以直接跳到任意一帧,适合随机访问与拖动进度条。 3. `h264_mp4toannexb` 会重编码吗? —— 不会。它只把长度前缀换成起始码,画面数据原样保留。- ☐ (本节完,下一节 4.2.3 根据音频流和视频流合成 MP4 文件)