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

十二个函数按调用顺序排,能背下来一条流程就通了。 Python 侧只点名四个,其余八个被合并进属性与迭代器。

📝 本节和前面几节的关系

3.3 到 3.5 按层讲了结构体,这一节按调用顺序串联。 下一篇 3.7 把这些函数放到 Ubuntu 上真编译一遍。

一、十二个函数,一条调用顺序

教材的函数表是按层排的,按调用顺序重排更好记。 序 函数 什么时候调


1 avformat_open_input 一进来 2 avformat_find_stream_info 紧接着 3 av_find_best_stream 挑流 4 avcodec_find_decoder 挑流之后 5 avcodec_alloc_context3 建上下文 6 avcodec_open2 开解码器

序 函数 什么时候调


7 av_read_frame 循环里,每包一次 8 avcodec_send_packet 拿到包就送 9 avcodec_receive_frame 送到就收 10 sws_getContext 尺寸或格式变了才建 11 sws_scale 每帧一次 12 avformat_close_input 退出时

第 7、8、9 三个在循环里,前三后一在循环外。这个结构认清,代码骨架就定了。 第 8、9 是一对:送一个包不一定立刻出一帧(B 帧要缓冲),所以必须写成「送完就收, 收到空为止」。

二、返回值与异常的分界

C 侧每个函数都返回 int,负数才是错。 AVERROR_EOF 是特殊的负值,表示正常结束不是故障。 Python 侧分两种:能恢复的抛特定异常类,流结束则让迭代器自然停。 C 侧返回 Python 侧表现


大于等于 0 正常返回 AVERROR_EOF 迭代器停 AVERROR(EAGAIN) 内部消化,不抛 其他负值 抛 FFmpegError 及其子类

AVERROR(EAGAIN) 被吞掉是好事也是坏事。 好事是不用写重试循环, 坏事是「为什么没帧」变得不好查。 异常类的层级值得看一眼:DecoderNotFoundError、InvalidDataError、 ProtocolNotFoundError 都是 FFmpegError 的子类,分开捕获能给出更准的提示。

三、谁开的谁关,先开的后关

这是全课唯一一条必须背下来的规矩。 C 侧必须做 Python 侧


av_packet_free 不要,自动 av_frame_free 不要,自动 av_dict_free 不要,传 dict avcodec_close 不要,随容器关 avformat_close_input 要,with 或 close()

只有容器要手动关,因为它握着一个文件句柄或一个 socket。其余全是内存,交给引用计数。 C 侧的收尾要倒着写:先开的后关。这个顺序错了,会在「关容器时还有未释放的上下文」上报错。

四、Python 替你消掉的那一半

十二个函数里,有八个在 Python 侧根本不用点名。 被消掉的 换成了什么


open 与 find_stream av.open 一步 find_decoder 与 open2 首次 decode 时自动 send 与 receive packet.decode() 一步 getContext 与 scale to_ndarray 一步

这是封装的价值,也是封装的天花板。 想调中间某一步的参数(比如指定 sws 的插值算法), 在合并掉的接口上就没地方放。 遇到这种情况,PyAV 通常留了一个更底层的入口,比如 reformat 里能传算法参数。 找不到的时候,回 C 是最后一条路。

五、动手 Demo:十二个 API,逐个真调一次

按调用顺序跑一遍,顺便数一数每包几帧。

import av

c = av.open("_media32/tiny.mp4")
st = c.streams.video[0]
n_pkt = n_frame = 0
for p in c.demux(st):
    if not p.size:
        continue
    n_pkt += 1
    n_frame += len(p.decode())
    if n_pkt >= 10:
        break
print(n_pkt, n_frame)
c.close()

实跑输出:

--------------------------------------------------------------------
十二个 API,逐个真调一次
--------------------------------------------------------------------

**① 教材列的函数表,按调用顺序排**

  C 侧函数                      Python 侧对应            这次的结果
  -----------------------------------------------------------------------
  avformat_open_input           av.open                  开成功
  avformat_find_stream_info     av.open 内含             流已就绪
  av_find_best_stream           streams.video[0]         拿到视频流
  av_read_frame                 demux                    取到包
  avcodec_find_decoder          stream.codec_context     解码器已绑好
  avcodec_open2                 首次 decode 时自动开     已打开
  avcodec_send_packet           packet.decode            无返回
  avcodec_receive_frame         packet.decode 的返回     拿到帧
  sws_getContext                to_ndarray(format=…)     内部建好
  sws_scale                     同上                     顺便转了
  av_packet_unref               自动                     不用管
  avformat_close_input          close()                  已关

**十二个函数,Python 侧能点名的只有四个**,其余八个被并进了属性访问、迭代器和自动内存管理里。

**② 并进去的好处是「不会漏 unref」**

C 侧最经典的漏是:av_read_frame 之后忘了 av_packet_unref,跑十分钟内存涨到几个 G。
Python 侧包是引用计数对象,出作用域就回收,**这类泄漏在 Python 里结构性地不存在**。

**③ 但代价是「错了不知道错在哪」**

C 侧每个函数返回错误码,能一行一行判。
Python 侧抛异常,栈里只看到 av.open 或 decode 那一层,中间七步全糊在一起。

**排错要换个思路**:不问「哪一步错了」,问「哪一层的输入不对」。
打不开就看路径与格式,能打开但解不出就看包大小为不为 0。

**④ 真的跑通一次,顺便数一数**

取前 10 个包,解出 10 帧,平均每包 1.00 帧。

**每包一帧不是定律**,只是这个小文件刚好这么切。换 4K 素材,一个包可能半帧都不到。

**⑤ 还有一组「谁也别忘」的收尾动作**

  C 侧必须做的             Python 侧还要不要
  -------------------------------------------------------
  av_packet_free           不要,自动
  av_frame_free            不要,自动
  avcodec_close            不要,close() 一并做
  avformat_close_input     要,但写成 with 或 close()
  av_dict_free             不要,传的是 dict

**只有容器要手动关**,因为它握着一个文件句柄。其余全是内存,交给引用计数。

取前十个包,一包一帧。 这是这个小文件的切法,不是定律。 if not p.size 拦住末尾的哨兵,否则会多数一个包。

30 秒实验

  1. 把 if n_pkt >= 10 去掉,数整个文件的包与帧。
  2. 故意传一个不存在的文件名,看抛的异常类名是什么。
  3. import av; print(av.FFmpegError.__subclasses__()) 看异常层级。

常见误区

  1. 只送包不收帧。 B 帧要缓冲,不排空会丢掉最后几帧。
  2. 把 AVERROR_EOF 当错误处理。 那是正常结束,C 侧也是负值但不算故障。
  3. 收尾顺序写反。 先关的上下文被后关的容器持有,会报未释放。
自测三问(点开对答案) 1. 为什么 send 与 receive 要成对写? —— 送一个包不一定立刻出帧。B 帧要等后面的参考帧,必须「送完就收,收到空为止」。 2. AVERROR(EAGAIN) 在 Python 侧表现成什么? —— 被封装层吞掉,不抛异常。好处是不用写重试,坏处是没帧时不好查原因。 3. Python 侧唯一必须手动关的是什么? —— 容器。它握着文件句柄或 socket,其余全是内存,交给引用计数。

相关笔记