🎬 第六章 AVProtocol协议层理论及案例实战
📄 一句话概括

数据从哪来,可以完全由你说了算——这就是自定义 AVIO。 教材的 avio_alloc_context,在 PyAV 里就是一个 io_open 回调。

📝 本节和前面几节的关系

6.3 换的是协议,这一节换的是「谁来给字节」。 下一篇 6.5 讲另一种「不抄数据」的做法:内存映射。

一、教材的做法:自己造一个 AVIO

C 语言里的三步:分配缓冲、交出两个回调、挂到 AVFormatContext 上。 步 干什么 给什么


一 avio_alloc_context 缓冲、读回调、seek 回调 二 把 AVIOContext 挂上去 AVFormatContext.pb 三 正常 open_input 它就用你造的 AVIO

关键在第一行:这三个参数就是把「读」这件事整个交出去。 FFmpeg 从此不再碰任何协议实现,只调你给的回调。 回调里想干什么都行:解密、解压、从数据库拼、从另一个进程取——只要按顺序吐出正确的字节。 PyAV 里没有 avio_alloc_context 这个名字,但有等价物:io_open 参数。

二、Python 侧的对等物:io_open 回调

一个三参数函数,返回一个「能读的东西」。 参数 是什么 怎么用


url 要开的地址 可以是你自己编的 flags 读还是写 读是 1 options 调用方塞的选项 通常不用管

返回值的要求很松:只要有 read,最好还有 seek 与 tell。 文件、BytesIO、自定义类、网络响应流,都行。 url 那个参数几乎可以无视:你还是得给 av.open 传一个地址,但因为回调接管了一切, 这个地址不会被真去打开。 这就是最实用的一点:可以写 av.open("mem://x", io_open=cb),用一个假地址,数据全从回调来。

三、四组实验看它能干到什么程度

同一个文件,四种来路,结果都一样。 回调返回 结果 说明


真文件 能解 最普通的用法 BytesIO 能解 数据已在内存 异或过的字节 能解 解密放在回调里 None 报 ValueError 没有 read 就穿帮

第三行才是这一节的重点。 磁盘上那份文件是加密的,谁直接拿它去 av.open 都解不开; 但经过回调转一手,上层根本不知道下面加过密。 这条路的实用价值很高:素材加密存盘、边下载边解密播放、把一个大文件切块存在别处再拼回来, 都能用它做,且不用改一行解码代码。 第四行是个提醒:PyAV 会去问返回值「有没有 read」。 给 None 就在这一步穿帮,报 ValueError 而不是 FFmpeg 的错。

四、回调什么时候被调

很多人以为回调是在每个包、每一帧都调一次。不是。 时机 调不调 io_open


打开输入源 调,一次 之后每次读数据 不调,走返回对象的 read 换一个输入源 再调一次

这个分工必须想清楚:io_open 管「找谁开」,返回对象的 read 管「怎么读」。 搞混了会写出很怪的代码:比如在 io_open 里循环读完整段数据, 那就把「流式」变成了「一次性载入」。 要真做到边下边解,得让返回对象的 read 每次只给一块、取的时候才去网络要。 这是 6.3 那个 Range 思路的自定义版。

五、什么时候值得自己造 AVIO

三条判断,占一条就值得。 情况 值得吗 为什么


数据要解密/解压 值得 解码器不认识加密字节 数据不在文件里 值得 没有路径可给 要边下边解不落盘 值得 省一次完整下载 就是打开个普通文件 不值 直接给路径更省事

最后一行要说重一点:自定义 AVIO 是有成本的——回调里的异常会被包一层、性能比原生协议慢、 调试更难。 所以别为了「高级」而用它。 能用路径就用路径,路径不行再用文件对象,文件对象不行才上回调。 三级降级,够用就好。

六、动手 Demo:自定义 AVIO——把读数据这件事整个接过来

同一份字节,分别从真文件、内存、解密后的内存里喂进去。 再故意返回 None,看它在哪一步穿帮。

import av, io

data = open("tiny.mp4", "rb").read()
enc = bytes(b ^ 0x5A for b in data)   # 假装这是磁盘上的密文

def cb_file(url, flags, options):
    return open("tiny.mp4", "rb")

def cb_mem(url, flags, options):
    return io.BytesIO(data)

def cb_dec(url, flags, options):
    return io.BytesIO(bytes(b ^ 0x5A for b in enc))

def cb_none(url, flags, options):
    return None

for tag, cb, url in (("file", cb_file, "tiny.mp4"),
                     ("mem", cb_mem, "mem://a"),
                     ("dec", cb_dec, "mem://b"),
                     ("none", cb_none, "mem://c")):
    try:
        c = av.open(url, io_open=cb)
        print(tag, c.format.name)
        c.close()
    except Exception as e:
        print(tag, "失败", type(e).__name__)

实跑输出:

--------------------------------------------------------------------
自定义 AVIO——把读数据这件事整个接过来
--------------------------------------------------------------------

**教材讲的是 `avio_alloc_context`,自己提供 read/seek 两个回调。**

**PyAV 把它包成了 `io_open` 参数**:传一个函数进去,FFmpeg 每次要开输入源就调它,
返回值必须是个文件对象。

**① 回调长什么样**

三个参数是地址、读写标志、选项字典,返回值是一个能读的对象,
也就是 `def cb(url, flags, options)` 这么一个函数。

**这三个参数各管一段**:url 是要开的地址(可能是你自己编的),flags 说明是读还是写,
options 是调用方额外塞进来的选项。**有了这个口子,数据从哪来就完全由你说了算。**

**② 三个实验:同一份字节,三种来路**

  回调返回什么             结果
  ---------------------------------------------------------
  回调返回真文件           认出 mov,mp4,m4a,3gp,3g2,mj2
  回调返回 BytesIO         认出 mov,mp4,m4a,3gp,3g2,mj2
  回调返回解密后的字节     认出 mov,mp4,m4a,3gp,3g2,mj2
  回调返回 None            失败 ValueError

**第三个是关键实验。** 喂进去的字节是先异或过的(文件本身完全不是 mp4),回调里再异或回来,
**照样解出完整容器**。

**这就是自定义 AVIO 的全部威力**:数据可以是加密的、压缩的、从数据库里拼的、
从内存缓冲区来的——**只要回调能给出正确的字节序列,上层的解封装器完全不知道下面有什么。**

**③ 返回 None 会怎样**

报的是 `ValueError: File object has no read() method`。
**注意这是 Python 侧的检查,不是 FFmpeg 的错。** PyAV 拿到 None,转头去问「有没有 read」,
一问就穿了。

**④ 回调被调几次**

上面四个实验一共触发 **4 次回调**,一个输入源一次。

**它不是在每个包里都被调**——只在整个输入源要打开的时候调一次。
之后每次读数据,走的是它返回的那个对象的 read。
**这个分工要想清楚**:io_open 管「找谁开」,read 管「怎么读」。

**⑤ 三种自定义 IO 的对照**

  做法                要写什么           适合什么
  -------------------------------------------------------------
  传文件对象          什么也不用写       内存里已有整段数据
  传 io_open 回调     一个三参数函数     边算边给、要解密
  改 FFmpeg 源码      C 代码,重编       要写进产品里

**中间那行才是这一节的主角。** 它比第一种多一层控制,
比第三种少一堆工作——**在 Python 里做 SDK 二次开发,这一层就够了。**

第三个能解才是重点:磁盘上那份是密文,直接开必然失败。 回调里转一手,上层完全不知道。

30 秒实验

  1. 把异或改成「每 100 字节插入 4 个字节垃圾」,在回调里剔除,验证还能解。
  2. 在回调里 print(url),确认 av.open 传进来的到底是哪个字符串。
  3. 把返回值换成一个只有 read、没有 seek 的对象,看解到哪一步出错。

常见误区

  1. 以为回调是每帧都调。 只在打开输入源时调一次。
  2. 在 io_open 里一次性读完整个文件。 那就没有「流」了。
  3. 为了显本事到处用自定义 AVIO。 能直接给路径就别绕。
自测三问(点开对答案) 1. 教材的 `avio_alloc_context` 在 PyAV 里对应什么? —— `av.open(..., io_open=回调)`,回调返回一个有 read 的对象。 2. 为什么回调返回 None 报的是 ValueError 而不是 FFmpeg 的错? —— PyAV 在 Python 层就去问「有没有 read」,一问就穿帮了。 3. io_open 回调会被调几次? —— 每打开一个输入源一次。之后读数据走返回对象的 read。

相关笔记