数据从哪来,可以完全由你说了算——这就是自定义 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 秒实验
- 把异或改成「每 100 字节插入 4 个字节垃圾」,在回调里剔除,验证还能解。
- 在回调里
print(url),确认 av.open 传进来的到底是哪个字符串。 - 把返回值换成一个只有 read、没有 seek 的对象,看解到哪一步出错。
常见误区
- 以为回调是每帧都调。 只在打开输入源时调一次。
- 在 io_open 里一次性读完整个文件。 那就没有「流」了。
- 为了显本事到处用自定义 AVIO。 能直接给路径就别绕。
自测三问(点开对答案)
1. 教材的 `avio_alloc_context` 在 PyAV 里对应什么? —— `av.open(..., io_open=回调)`,回调返回一个有 read 的对象。 2. 为什么回调返回 None 报的是 ValueError 而不是 FFmpeg 的错? —— PyAV 在 Python 层就去问「有没有 read」,一问就穿帮了。 3. io_open 回调会被调几次? —— 每打开一个输入源一次。之后读数据走返回对象的 read。- ☐ (本节完,下一节 6.5 内存映射文件技术)