🎬 第七章 AVFormat封装层理论及案例实战
📄 一句话概括

封装是按规则装箱,解封装是按规则拆箱,两头都用 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 秒实验

  1. 把解析器加一层递归,进 moov 里再切一遍,看 trak 里套了几层。
  2. 对 mkv 跑同一段代码,看它是不是也按「长度 + 类型」排的(提示:不是)。
  3. 把 size == 1 与 size == 0 两种情况处理掉,再跑一次。

常见误区

  1. 以为封装会重压数据。 它只搬字节,不碰像素。
  2. 以为 moov 一定在前面。 录出来的文件基本都在后面。
  3. 查字节偏移时忘了盒子头那 8 个字节。 内容从第 8 字节才开始。
自测三问(点开对答案) 1. 封装与解封装各做什么? —— 封装把多路压缩包排进一个容器;解封装把容器拆回多路压缩包。两者都不碰编码器。 2. moov 放在 mdat 后面有什么坏处? —— 播放器要先读到文件末尾拿到索引才能开始播,网络点播会明显卡。 3. 盒子长度写 0 是什么意思? —— 表示这个盒子一直到文件结尾,录制时不知道总长就这么写。

相关笔记