🎬 第三章 夯实FFmpeg基础:重要数据结构及API
📄 一句话概括

包是压缩的字节,帧是解开的画;一个几十万,一个几十万分之一。 两个结构体的字段不多,但 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 秒实验

  1. 把 pkt.is_keyframe 打在循环里,看关键帧占多少比例。
  2. 数一数 pict_type 出现过的值有几种。
  3. 把 50 帧全转成 numpy 存进列表,看内存涨多少。

常见误区

  1. pts 与 dts 混为一谈。 有 B 帧时两者不同,用错会画面抖动。
  2. 在循环里存帧对象引用。 缓冲被复用,存下来的内容会变。
  3. 按固定倍数估内存。 压缩比随内容变化,关键帧与非关键帧差几倍。
自测三问(点开对答案) 1. 为什么会有 pts 与 dts 两个时间戳? —— 因为 B 帧要参考后面的帧,解码顺序与显示顺序不一致。 2. `pict_type` 打出 1 是什么意思? —— I 帧。2 是 P 帧,3 是 B 帧。它给的是枚举值不是字母。 3. 想保存一串帧待会再看,正确做法是什么? —— 把像素深拷出来(转 numpy 或复制缓冲),不能只存 frame 对象引用。

相关笔记