包是压缩的字节,帧是解开的画;一个几十万,一个几十万分之一。 两个结构体的字段不多,但 pts、dts、duration 三个时间戳必须分清。
3.2.1 把十个结构体过了一遍,这一篇把最重要的两个拆开。 下一篇 3.3 转去协议层。
一、包是压缩的,帧是解开的
教材把这两个放在一起讲,是因为它们是同一条流水线的前后两站。 AVPacket AVFrame
装什么 压缩后的码流片段 解开的像素或采样 体积 几百字节到几兆 一张原图那么大 谁产生 解封装 解码 能不能直接看 不能 能,但要先换格式
这个差距不是常数。 压缩率高的时候差得多,关键帧比非关键帧大好几倍。 所以不能按固定倍数估算内存。
二、包上的九个字段
pts 与 dts 的区别,是这一节最该拿走的一条。 字段 含义
pts 显示时间戳,什么时候该出现在屏幕上 dts 解码时间戳,什么时候该送去解码 duration 这个包占多少时间基 time_base 这个包用哪把尺子量时间 size 压缩后的字节数 pos 在文件里的偏移位置 is_keyframe 能不能不靠别的包单独解出来 stream_index 属于哪一路流
pts 与 dts 不一样,是因为存在 B 帧。 B 帧要参考后面的帧, 所以「解码顺序」与「显示顺序」相反,两个时间戳就分家了。 这个小文件没有 B 帧(max_b_frames 是 0),所以第一个包的 pts 与 dts 都是 0。 真实素材里两者经常差好几帧。
三、帧上的几个字段
帧的字段比包多,因为解开的画面信息量大。 字段 含义
width/height 解出来的实际尺寸 format 原始像素格式,如 yuv420p pts 这一帧的时间戳 pict_type 1 是 I 帧,2 是 P 帧,3 是 B 帧 key_frame 关键帧标记 is_corrupt 解码时有没有坏块 colorspace 色彩空间枚举
pict_type 打出来是数字不是字母,别以为是出错。枚举值到名字的映射得自己写一张表。 is_corrupt 是个便宜的健康检查。 网络流里丢包丢成什么样,看这个标记比看画面快得多。
四、引用计数与显式保住
C 侧这两个结构体都要手动分配释放,Python 侧自动。但有一件事必须自己管:在迭代里保存帧。 解码器可能复用内部的帧缓冲。 你在循环里存下 frame 对象引用,下一轮再解,之前那个对象的内容可能已经变了。 要留住就必须深拷一份,把像素数据复制出来,或者转成 numpy 数组。只存对象引用是留不住的。 这个问题在 Python 里表现得很隐蔽:不报错,就是画面变了。 凡是「保存一串帧之后发现全一样」的现象,多半是这个原因。
五、动手 Demo:包与帧——两种东西,两种寿命
同一个包,从压缩到解开,体积差多少。
import av
c = av.open("_media32/tiny.mp4")
pkt = next(p for p in c.demux(c.streams.video[0]) if p.size)
print(pkt.pts, pkt.dts, pkt.duration, pkt.size, pkt.is_keyframe)
frame = list(pkt.decode())[0]
print(frame.width, frame.height, frame.format.name)
print(frame.width * frame.height * 3 // pkt.size)
c.close()
实跑输出:
--------------------------------------------------------------------
包与帧——两种东西,两种寿命
--------------------------------------------------------------------
**① 先看一个包有哪些字段**
字段 值 说明
---------------------------------------------------------
pts 0 显示时间戳,乘时间基得秒
dts 0 解码时间戳
duration 512 占多少时间基
time_base 1/12800 这个包用哪把尺子
size 786 压缩后字节数
pos 48 在文件里的偏移
is_keyframe True 能不能单独解出来
stream_index 0 属于哪一路
**② 再看一帧的字段**
字段 值 说明
--------------------------------------------------
width/height 160×120 解出来的实际尺寸
format yuv420p 原始像素格式
pts 0 帧自己的时间戳
pict_type 1 1=I 帧
key_frame True 关键帧标记
is_corrupt False 有没有坏块
colorspace 2 色彩空间枚举
**③ 寿命不一样,这是最关键的一条**
包 帧
--------------------------------------------------------
占多大 几百字节到几 MB 一张原图那么大
怎么来 demux 给出 包解出来
怎么放 引用计数,用完就扔 要显式保住
能不能存 能存一大堆 存几帧就占满内存
**一个 786 字节的包,解出 160×120 的 rgb24 帧要 57600 字节**,差 73 倍。
**所以解码器的缓冲永远只留几帧**。想缓存一段画面,要么存压缩后的包,要么把帧转小再存。
存原帧是最贵的做法。
最后一行的 73 就是「压缩比」,是代码算出来的,不是写死的。 这一帧是 I 帧(is_keyframe 为 True),换成 P 帧倍数会大得多。
30 秒实验
- 把
pkt.is_keyframe打在循环里,看关键帧占多少比例。 - 数一数
pict_type出现过的值有几种。 - 把 50 帧全转成 numpy 存进列表,看内存涨多少。
常见误区
- pts 与 dts 混为一谈。 有 B 帧时两者不同,用错会画面抖动。
- 在循环里存帧对象引用。 缓冲被复用,存下来的内容会变。
- 按固定倍数估内存。 压缩比随内容变化,关键帧与非关键帧差几倍。
自测三问(点开对答案)
1. 为什么会有 pts 与 dts 两个时间戳? —— 因为 B 帧要参考后面的帧,解码顺序与显示顺序不一致。 2. `pict_type` 打出 1 是什么意思? —— I 帧。2 是 P 帧,3 是 B 帧。它给的是枚举值不是字母。 3. 想保存一串帧待会再看,正确做法是什么? —— 把像素深拷出来(转 numpy 或复制缓冲),不能只存 frame 对象引用。- ☐ (本节完,下一节 3.3 协议层的三大重要数据结构)