解封装只做一件事:把容器拆开,把字节分给各条流。 六个数读出来,文件的家底就清楚了。
3.1.2 的第 1、2 步,这一篇展开讲。 下一篇 3.2.1 开始逐个结构体看,这一篇是它们的第一次露面。
一、解封装到底做了什么
容器就像信封,码流是信纸。 解封装是拆信封,不是读信。 MP4 里有 moov、mdat 这些盒子,MKV 里有 EBML 元素, FLV 里有 tag——格式各不相同,但拆完得到的都是同一种东西:带时间戳的包。 层 输入端 输出端
解封装 文件、网络流 带时间戳的压缩包 解码 压缩包 原始帧 转换 原始帧 能显示的格式
这个分层解释了为什么换个容器不用改解码代码。 MP4 换成 MKV,变的只是拆信封那一段, 后面的解码一模一样。
二、容器这一层能读出的六个字段
这六个数是看一个陌生文件的第一眼。 字段 含义 单位
format 容器格式的短名 — duration 总时长 微秒 bit_rate 整文件平均码率 bit/秒 size 文件字节数 字节 start_time 起点时间戳 时间基 metadata 写文件的人留下的字 —
duration 的单位是微秒,这一条最容易错。 2023220 不是两千秒,是 2.02 秒。 要换成秒得除 1000000。 metadata 里的 encoder 是个线索。 它写着 Lavf62.12.102,说明这文件是本机 libavformat 写的。 拿别人给的文件时,这一行能看出工具链。
三、每一路流各自的账
容器是一本总账,每一路流是一本分账。 总码率是各路之和,总时长取最长的那一路。 字段 视频流 音频流
index 0 1 codec h264 aac time_base 1/12800 1/44100 duration 25600 89088 bit_rate 44380 138049 frames 50 0
音频的 frames 是 0,这不算错。 音频轨的头里一般不放总帧数,视频轨能放。 别把 0 读成「没有音频」。 视频的 duration 25600 乘 1/12800 是 2 秒,音频的 89088 乘 1/44100 是 2.02 秒。 同一段内容, 两条轨道的时长差一点点,是正常的。
四、时间基不一样,pts 不能直接比
这是本课后面反复要用的一个换算。 视频的时间基是 1/12800,音频是 1/44100。 同一个 pts 值 12800,在视频里是 1 秒,在音频里是 0.29 秒。 要做的事 正确的式子
pts 换秒 pts × time_base 秒换 pts 秒 ÷ time_base 比较两路 各自换秒之后再比 换到同一个时间基 pts × 原基 ÷ 新基
时间基是整数分子除以整数分母,不是浮点。 换算是整数运算, 要小心除不尽时的舍入——差一格就是一帧的几十毫秒。 下一节 3.2.1 会看到,时间基这个字段挂在三层上:容器、流、包,每一层的值可能都不一样。 以流上的为准最稳。
五、动手 Demo:把流信息一次读干净
真读一遍,看这个文件的家底。
import av
c = av.open("_media32/tiny.mp4")
print(c.format.name, c.duration, c.bit_rate, c.size)
for s in c.streams:
print(s.index, s.type, s.codec_context.name,
s.time_base, s.duration)
print(dict(c.metadata))
c.close()
实跑输出:
--------------------------------------------------------------------
把流信息一次读干净
--------------------------------------------------------------------
**① 容器这一层能读出什么**
字段 值 含义
---------------------------------------------------------------------
format mov,mp4,m4a,3gp,3g2,mj2 容器格式的短名
duration 2023220(÷1000000) 微秒,=2023 毫秒
bit_rate 182429 整文件平均码率,bit/秒
size 46137 文件字节数
start_time 0 起点时间戳
flags 0x200001 打开方式的位标志
**duration 的单位是微秒,不是秒**,也不是 ffmpeg 命令行打出来的那种 00:00:02.02。
要换算成秒得除 1000000。
**② metadata 里的四条是写文件的人留下的**
compatible_brands = isomiso2avc1mp41
encoder = Lavf62.12.102
major_brand = isom
minor_version = 512
**encoder 那一行写着 Lavf62.12.102**,正是本机 libavformat 的版本。
拿别人的文件时,这一行能告诉你它是被哪一版写出来的。
**③ 每一路流各自的账**
字段 视频流 音频流
----------------------------------------
index 0 1
codec h264 aac
time_base 1/12800 1/44100
duration 25600 89224
bit_rate 44380 127993
frames 50 88
average_rate 25 —
sample_rate — 44100
**两路流的时间基不一样**:视频 1/12800,音频 1/44100。
所以 pts 不能直接比大小,必须各自乘自己的时间基换成秒。
**④ 音频的 frames 是 0,视频是 50**
这是「头上写没写总帧数」的区别。视频轨道的头部能放帧数,音频轨道一般不放。
别把 0 当成「没有音频」。
**⑤ 一条命令代替这一整节**
ffprobe 打出来的就是这张表。本机没装 ffprobe(后面 3.7 会验证),所以只能用代码读。
**能读代码的地方,读代码比调命令行稳。**
c.duration 是 2023220,除一百万才得 2.02 秒。
metadata 里 encoder 那一行是写文件的工具留下的,不是内容信息。
30 秒实验
- 把
c.duration / 1000000打出来,跟 ffprobe 习惯的显示对上。 - 读一个 mp3 文件,看它的流数与时间基跟 mp4 差多少。
- 故意用一个不存在的文件名,看 av.open 抛的是哪种异常。
常见误区
- 把 duration 当秒用。 它的单位是微秒,直接用会差一百万倍。
- 拿视频的 pts 去跟音频 pts 比。 两路时间基不同,必须先换秒。
- 以为 frames 为 0 就是没有这一路。 音频轨一般不在头里写总帧数。
自测三问(点开对答案)
1. `c.duration` 是 2023220,这是几秒? —— 2.02322 秒。单位是微秒,除 1000000。 2. 为什么视频与音频的时长不完全相同? —— 两路独立编码,各自的最后一帧结束时刻不同,差几十毫秒很正常。 3. 解封装之后得到的是帧还是包? —— 包。带时间戳的压缩包,还没解码。- ☐ (本节完,下一节 3.2.1 使用 FFmpeg 进行解码的 10 个经典结构体)