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

解封装就是一次循环:读一个包,看一眼,再读下一个。 包里的 pos 是它在文件里的位置,这是解封装独有的信息。

📝 本节和前面几节的关系

7.2 列了 API,这一节把它们串成一次完整的解封装。 下一篇 7.4 专门讲包上那些数字的尺子问题。

一、先把家底列清楚

打开之后第一件事不是读包,是把容器与流的信息列出来。 项 从哪拿 单位


容器名 container.format.name — 时长 container.duration 微秒 总码率 container.bit_rate bps 元数据 container.metadata — 流列表 container.streams —

duration 的单位是微秒,不是秒。 这是最常见的单位坑:直接当秒用, 会得到「两百万秒」这种荒唐数。 元数据里有 encoder 一项,写着是谁写的这个文件。 拿到别人给的文件时,这一行能看出它过过谁的手、用什么工具做的。

二、每条流各有一份账

流级别的信息与容器级别的是两套。 项 注意什么


index 包靠它认自己是哪条流的 type video、audio、subtitle time_base 这条流的尺子,各流不同 bit_rate 可能读不到,是估算值 frames 有的容器根本不写

bit_rate 读不到是常事,尤其 mkv。 拿到 None 不要以为文件坏了,拿总码率减去别的流也是一种办法。 demo 里视频 44380 加音频 127993 等于 172373,与总码率 182429 差 10056,那点差就是容器开销。 这个「加起来对不对得上」本身是个体检手段:差得离谱,说明有一路流的码率没读到, 或者容器写的是估算值。

三、包里有什么

一个包就是一小段压缩数据加几个说明。 字段 含义 用途


pts 显示时刻 排播放顺序 dts 解码时刻 排解码顺序 duration 这一包管多长时间 算帧率 size 数据字节数 算码率 pos 在文件里的偏移 做索引、做定位 is_keyframe 是不是关键帧 能不能从这里起播

pos 这一项只有解封装拿得到。 解码出来的帧上没有它,因为帧已经与文件位置无关了。 要自己做索引、自己实现定位,就得靠 pos 记账。 这也是很多播放器自己维护一份帧位置表的原因。

四、pts 等于 dts 意味着什么

这两个数不相等,是因为 B 帧。 情况 pts 与 dts 播放器要做什么


没有 B 帧 完全相等 读到就能播 有 B 帧 dts 小于 pts 先存着,等齐了再播

B 帧要用后面的帧做参考,所以编码顺序与显示顺序不一样:先编的那一帧可能后播。 所以「pts 等于 dts」是个有用的诊断:相等说明这条流没有 B 帧,延迟最小; 不等说明有,得留缓冲。 直播里常把 B 帧关掉,就是为了减少这个缓冲延迟。代价是同码率下画质略差。

五、一次解封装的完整形状

把上面几块拼起来,就是那十几行代码。 步 做什么


一 av.open 打开,拿 streams 二 挑一条流(或全部) 三 for pk in demux():跳过 size 为 0 的 四 按 pk.stream 分流处理 五 close

第三步那个「跳过 size 为 0」不能省。 demux 在结尾会吐空包,表示「这条流读完了」,不是错误。 很多人第一次写解封装,就是被这个空包卡住的:把空包当成数据去解,报一堆莫名其妙的错。

六、动手 Demo:解封装一次,把每条流的账都算清

先列容器与流的信息,再逐包看前五个,最后核一遍码率。

import av

c = av.open("tiny.mp4")
print(c.format.name, c.duration, c.bit_rate)
for st in c.streams:
    print(st.index, st.type, st.codec_context.name, st.time_base, st.bit_rate)

vs = c.streams.video[0]
for i, pk in enumerate(c.demux(vs)):
    if pk.size == 0:
        continue
    print(pk.pts, pk.dts, pk.duration, pk.size, pk.pos, pk.is_keyframe)
    if i >= 4:
        break
c.close()

实跑输出:

--------------------------------------------------------------------
解封装一次,把每条流的账都算清
--------------------------------------------------------------------

**① 先把家底列出来**

  项             值
  ------------------------------------------
  容器           mov,mp4,m4a,3gp,3g2,mj2
  时长           2.023 秒
  总码率         182429 bps
  起始时间       0
  元数据条数     4

**元数据里有一条 `encoder`**,写着 `Lavf62.12.102`。
**这行字说明这个文件是本机 libavformat 写出来的**,拿到别人给的文件时,这一行能看出它过过谁的手。

**② 每条流各算一遍**

  流     类型      编码     time_base     码率
  --------------------------------------------------
  0      video     h264     1/12800       44380
  1      audio     aac      1/44100       127993

**视频 44380 加音频 127993 等于 172373,与总码率 182429 相差 10056**,约 5.5%。

**差的那点就是容器开销**:索引、包头、元数据。
**这一项本身是个体检手段**——差得离谱,说明有一路流没读到码率,或者容器里写的是估算值。

**③ 逐包看,前五个**

  包     pts      dts      时长     字节     位置     关键帧
  --------------------------------------------------------------
  0      0        0        512      786      48       是
  1      512      512      512      260      1593     否
  2      1024     1024     512      175      2533     否
  3      1536     1536     512      193      3424     否
  4      2048     2048     512      256      3981     否

**五个包的 pts 与 dts 完全相等**,因为这条流里没有 B 帧。
**有 B 帧的时候 dts 会小于 pts**,这是解码顺序与显示顺序分家。

**「位置」这一列是包在文件里的字节偏移**,第一个是 48——正好是 ftyp 与 free 两个盒子之后。
**这说明 mdat 里的第一个包就在那儿。**

**视频包一共 50 个,字节合计 11095,占文件的 24%。** 其余是索引与音频。

前五个包的 pts 与 dts 完全相等,说明没有 B 帧。 第一个包的 pos 是 48,正好在 ftyp 与 free 之后。

30 秒实验

  1. 把 demux 换成 c.demux()(不传流),看包的交错顺序。
  2. 统计所有包的 size 之和,除以时长,与流码率对一对。
  3. 找一个有 B 帧的视频,打印 pts 与 dts,看差多少。

常见误区

  1. 把 duration 当秒用。 它是微秒。
  2. 把 size 为 0 的包当数据。 那是「这条流读完了」。
  3. 以为 bit_rate 一定有。 不少容器不写,读出来是 None。
自测三问(点开对答案) 1. container.duration 的单位是什么? —— 微秒。两百万左右的数对应两秒多。 2. demux 吐出的空包代表什么? —— 这条流读完了,不是错误,要跳过。 3. pts 与 dts 相等说明什么? —— 这条流里没有 B 帧,编码顺序与显示顺序一致。

相关笔记