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

打开、选流、取包、解帧、换格式,五步走完就出画面。 包与帧是两种东西:一个压缩的、一个解开的,差几十倍。

📝 本节和前面几节的关系

3.1.1 认了名字,这一篇把名字串成流程。 下一篇 3.1.3 只做前两步,把流信息读透。

一、五个动作,一条链

教材把解封装与解码拆成两章讲,其实是一条链上的两段。 步 做什么 C 侧的调用


1 打开文件,认出格式 avformat_open_input 2 挑出要用的那一流 av_find_best_stream 3 读一个压缩包 av_read_frame 4 把包解成原始帧 avcodec_send/receive 5 换成能显示的像素格式 sws_scale

第 3 步与第 4 步是两种完全不同的事。 第 3 步只搬字节,不碰内容;第 4 步才真正算。 所以第 3 步永远很快,第 4 步可能很慢。 这个快慢差是播放器的设计前提:包可以提前读一大把,帧不能。

二、包与帧的寿命不一样

一个 786 字节的包,解出 160×120 的 rgb24 帧要 57600 字节。 差 73 倍。 包 帧


体积 压缩后,几百字节起 一张原图那么大 来源 demux 给出 包解出来 内存 引用计数,用完即扔 要显式保住 能存多少 成千上万个 几帧就占满

所以解码器内部只留几帧。 想缓存一段画面,正确做法是存压缩后的包,要看的时候再解,而不是把帧堆在内存里。 1080p 的 yuv420p 一帧约 3 MB,30 帧就是一秒一百兆。拿帧做缓存的程序,通常几十秒就崩。

三、一包一帧不是定律

教材的例子里总是一包一帧,容易让人当成规则。 实测这个小文件:视频包 50 个、视频帧 50 个,音频包 88 个、音频帧 88 个,两路都是整齐的一比一。 但这只是切得刚好。 两种例外都存在: 情况 结果 什么时候出现


一包多帧 帧数大于包数 帧率低、码率低时切片大 多包一帧 包数大于帧数 单帧超过解复用缓冲

凡是拿总包数当视频帧数算的,只要文件里有声音就一定错。 这个文件的 138 个包里,88 个是音频。

四、播放器还缺什么:时钟

五步走完只得到一串帧,还不是「播放」。 要按时间把帧吐出来,得有一个时钟:音频按采样数推,视频按 pts 对,两路还要一个主时钟来对齐。 这一整套叫音视频同步,教材放在后面的章节。 这一节先把「解出来」做好,同步的事不在这里解决。 但要知道:解出来只是原料, 播出画面是另一层工作。

五、动手 Demo:五个动作逐个看产物,再把整段数一遍

下面这段是真实跑出来的,每一步都打印了产物。

import av

c = av.open("_media32/tiny.mp4")
st = c.streams.video[0]
pkt = next(p for p in c.demux(st) if p.size)
frame = list(pkt.decode())[0]
arr = frame.to_ndarray(format="rgb24")
print(pkt.size, frame.width, frame.height, arr.shape)
c.close()

实跑输出:

--------------------------------------------------------------------
解码播放的五个动作,逐个看产物
--------------------------------------------------------------------

**① 一个文件从头到出画面,只有五步**

打开 → 选流 → 取包 → 解帧 → 换格式。教材把中间两步讲得很细,其实这五步在 Python 里各对应一行。

**第 1 步:打开文件**

av.open 返回 InputContainer,格式认成 mov,mp4,m4a,3gp,3g2,mj2,里面有 2 路流。

**第 2 步:选流**

视频流是 0 号,编码 h264,160×120,时间基 1/12800。

**第 3 步:取包**

第一个包的 pts 是 0,大小 786 字节,关键帧=True。

**第 4 步:解帧**

解出 VideoFrame 对象,160×120,像素格式 yuv420p。

**第 5 步:换格式**

转成 rgb24 之后形状是 120×160×3,类型 uint8。

**② 整段跑一遍,看看一比多少**

  阶段               数量
  -----------------------------
  文件里的流         2 路
  demux 迭代次数     140 次
  其中视频包         50 个
  其中音频包         88 个
  视频帧             50 帧
  音频帧             0 帧

**140 次迭代打出 138 个有用的包**,差的 2 次是流结束的哨兵——每路流各一个。

**这句是新手最容易漏的**:demux 停下来不是因为迭代器到头,而是最后会吐一个大小为 0 的包。
不看 size 就当成数据,会解出一堆空帧。

**③ 包数 138,帧数 50,两者差在另一路**

视频包 50 个、视频帧 50 帧,**这一路里恰好一包一帧**。
音频包 88 个、音频帧 0 帧,也是一包一帧。

**所以容器里的总包数 138 跟视频帧数 50 对不上,差的 88 个全是音频的。**

**但「一包一帧」不是定律。** H.264 允许一个包切多个帧(帧率极低、码率极小时常见),
也允许多个包拼一个帧(一帧超过解复用器缓冲上限时)。这个小文件刚好切得整齐。

**记住这个不等**:凡是拿「容器总包数」当「视频帧数」算的地方,只要文件里有声音就一定算错。

五步在代码里只写了五行,因为 PyAV 把中间几步合并了。 if p.size 那个判断不能省,末尾的哨兵包大小为 0。

30 秒实验

  1. 把 if p.size 去掉,看解出多少空帧。
  2. 把 format="rgb24" 换成 "gray",看数组形状怎么变。
  3. 数一数 c.demux() 不带参数时,视频包与音频包各占多少。

常见误区

  1. 不看 size 就当数据解。 流末尾会吐大小为 0 的哨兵包,解出空帧。
  2. 拿容器总包数当视频帧数。 有音频轨时包数里混着音频的份。
  3. 把帧堆在内存里做缓存。 一帧几兆,几十秒就吃光内存,应该缓存包。
自测三问(点开对答案) 1. 为什么第 3 步永远快、第 4 步可能慢? —— 第 3 步只搬字节不解内容,第 4 步要做真正的解码运算。 2. 786 字节的包为什么能解出 57600 字节的帧? —— 因为压缩。H.264 用帧内与帧间预测把冗余去掉了,解码是还原的过程。 3. demux 循环什么时候该停? —— 遇到包大小为 0 时停。那是这一路流结束的标记,不是数据。

相关笔记