封装是按规则装箱,解封装是按规则拆箱,两头都用 libavformat。 mp4 的规则就是「长度 + 类型」的盒子一层套一层。
7.1.1 讲容器是什么,这一节讲它是怎么排出来的。 下一篇 7.2 把操作这些盒子的 API 列出来。
一、一对反动作
两个词经常被混着说,其实方向相反。 动作 方向 输入输出
封装 压缩包 → 容器文件 多路包 → 一个文件 解封装 容器文件 → 压缩包 一个文件 → 多路包
两个动作都不碰编码器。 这也是转封装能保住画质的原因:它只做了拆箱再装箱。 所以一条链里,解封装与封装是对称的两头,中间的解码、处理、编码才是可选的那一段。
二、mp4 的规则:盒子套盒子
每个盒子都是同一个格式:先 4 字节长度,再 4 字节类型。 字段 长度 作用
长度 4 字节 这个盒子一共多长 类型 4 字节 ftyp、moov、mdat 这种四字母 内容 剩下的 要么是数据,要么又是几个盒子
这个规则简单到可以手写解析器。 demo 里三十行 Python 就把整个文件切开了。 而且长度有两种特殊情况:写 1 表示「真正的长度在下一个 8 字节里」, 写 0 表示「一直到文件尾」。后者常用在录制的文件上,因为录的时候不知道会录多长。
三、顶层四个盒子各干什么
一个 mp4 的骨架就四个。 盒子 装什么 能不能没有
ftyp 品牌与兼容性声明 不能 free 空白填充 能 mdat 音视频数据 不能 moov 索引与参数 不能
只有 free 可以没有,它就是个占位符,方便以后原地改点东西。
ftyp 那四个字母后面是品牌名,比如 isom、mp42。
播放器靠它判断「这文件我认不认识」,认不出就干脆不打开。
四、索引放在前面还是后面
这个问题在实际工作里天天遇到。 顺序 优点 缺点
moov 在前 边下边播,立刻能开始 要重写一遍文件 moov 在后 录的时候直接往后追加 要先拉到末尾才能播
录像、录屏、手机拍摄出来的文件基本都是「moov 在后」,因为录的时候只能往后写,没法回头补。
demo 里那个文件也是这个顺序:mdat 在 40,moov 在 43513。
这就是为什么有些网课视频点开要转圈好几秒。
修法是 -movflags +faststart,把 moov 挪到前面。
它不改一个字节的编码数据,只是重排加改偏移量。
五、从盒子看回整条链
认了这几个盒子,再回看那条五环链就清楚了。 环 对盒子做了什么
解封装 读 moov 拿参数,读 mdat 拿包 解码 不碰盒子,只处理包里的数据 编码 同上 封装 写新的 mdat,写新的 moov
注意中间两环完全不知道盒子的存在。 这是分层最实在的好处:改编码器不用动封装, 换容器不用动编码。 也正因如此,转封装才能快:它一次解码都不做,只把 mdat 里的字节搬进新容器的 mdat, 再重新编一份索引。
六、动手 Demo:把一个 mp4 拆开看,里面就是一个套一个的盒子
三十行代码手写一个解析器,按「长度 + 类型」一路切下去。
import struct
data = open("tiny.mp4", "rb").read()
off = 0
while off + 8 <= len(data):
size = struct.unpack(">I", data[off:off + 4])[0]
typ = data[off + 4:off + 8].decode("latin-1")
print("%-6s %8d @%d" % (typ, size, off))
if size <= 0:
break
off += size
实跑输出:
--------------------------------------------------------------------
把一个 mp4 拆开看,里面就是一个套一个的盒子
--------------------------------------------------------------------
**① 读文件头,按「长度 + 类型」一路切下去**
整个文件 **46137 字节**,切出 **21 个盒子**。前四个是顶层盒子:
盒子 大小 起点 装什么
-------------------------------------------------------------
ftyp 32 0 品牌:告诉播放器「我是 mp4」
free 8 32 空位,留着以后填
mdat 43473 40 音视频数据本身,占绝大部分
moov 2624 43513 索引:时长、轨道、每帧在哪
**mdat 一个盒子就占了 43473 字节,是文件的 94%。** 剩下那点空间才是索引。
**② 再看 moov 里套了什么**
层 盒子 管什么
--------------------------------------------------
1 mvhd 整片的时长、时间刻度
1 trak 一条轨道(视频一条、音频一条)
2 tkhd 这条轨道的编号与显示尺寸
2 edts 剪辑表,通常只有一段
2 mdia 这条轨道的媒体信息
3 mdhd 这条轨道自己的时间刻度
3 hdlr 这条轨道是视频还是音频
3 minf 真正描述编码参数的地方
1 trak 一条轨道(视频一条、音频一条)
2 tkhd 这条轨道的编号与显示尺寸
2 edts 剪辑表,通常只有一段
2 mdia 这条轨道的媒体信息
3 mdhd 这条轨道自己的时间刻度
3 hdlr 这条轨道是视频还是音频
3 minf 真正描述编码参数的地方
1 udta 附加信息,比如谁写的这个文件
**`trak` 那一组出现了 2 遍**,因为文件里有 2 条轨道:视频一条、音频一条。
**两条轨道的参数是分开存的**,各自有自己的时间刻度与编码参数,这也是「时间基不能混用」的结构原因。
**注意 `mvhd` 与 `mdhd` 是两个不同的时间刻度**:一个管整片,一个管单条流。
**这就是 7.4 要讲的时基问题的来源。**
**③ 这个文件的索引在数据后面**
mdat 在 **40**,moov 在 **43513**——**索引排在了数据后面**。
**这叫「没做 faststart」。** 播放器要先把整个文件拉到末尾读完索引,才能开始播;
网络点播时会明显卡一下。
**做法是 `-movflags +faststart`**:把 moov 挪到 mdat 前面。
**代价是整个文件要重写一遍**,所以只在发布前做一次。
四个顶层盒子分别是 ftyp、free、mdat、moov。 mdat 占了 94%,索引只有一点点——容器开销本来就不大。
30 秒实验
- 把解析器加一层递归,进 moov 里再切一遍,看 trak 里套了几层。
- 对 mkv 跑同一段代码,看它是不是也按「长度 + 类型」排的(提示:不是)。
- 把
size == 1与size == 0两种情况处理掉,再跑一次。
常见误区
- 以为封装会重压数据。 它只搬字节,不碰像素。
- 以为 moov 一定在前面。 录出来的文件基本都在后面。
- 查字节偏移时忘了盒子头那 8 个字节。 内容从第 8 字节才开始。
自测三问(点开对答案)
1. 封装与解封装各做什么? —— 封装把多路压缩包排进一个容器;解封装把容器拆回多路压缩包。两者都不碰编码器。 2. moov 放在 mdat 后面有什么坏处? —— 播放器要先读到文件末尾拿到索引才能开始播,网络点播会明显卡。 3. 盒子长度写 0 是什么意思? —— 表示这个盒子一直到文件结尾,录制时不知道总长就这么写。- ☐ (本节完,下一节 7.2 封装层的重要API函数简介)