解封装就是一次循环:读一个包,看一眼,再读下一个。 包里的 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 秒实验
- 把 demux 换成
c.demux()(不传流),看包的交错顺序。 - 统计所有包的 size 之和,除以时长,与流码率对一对。
- 找一个有 B 帧的视频,打印 pts 与 dts,看差多少。
常见误区
- 把 duration 当秒用。 它是微秒。
- 把 size 为 0 的包当数据。 那是「这条流读完了」。
- 以为 bit_rate 一定有。 不少容器不写,读出来是 None。
自测三问(点开对答案)
1. container.duration 的单位是什么? —— 微秒。两百万左右的数对应两秒多。 2. demux 吐出的空包代表什么? —— 这条流读完了,不是错误,要跳过。 3. pts 与 dts 相等说明什么? —— 这条流里没有 B 帧,编码顺序与显示顺序一致。- ☐ (本节完,下一节 7.4 FFmpeg封装格式之时基与时间刻度)