AVIO 是整条链最底下那一层,负责把「字节」弄进来。 它一次要一大块,在内存里慢慢发——缓冲把两边的粒度解耦了。
第三章讲过 AVIOContext 的字段长相,这一节讲它怎么动。 6.2 起是四个实战:本地文件、网络流、自定义 IO、内存映射。
一、AVIO 在这条链的什么位置
FFmpeg 分四层,协议层是最下面那层。 层 归谁 管什么
封装层 libavformat 认容器、切出音视频包 协议层 AVIO 把字节读进来、送出去 协议实现 file/http/rtp 真去碰磁盘或网卡
严格说 AVIO 不是「协议」,是协议的外壳。 真正的协议实现藏在 read_packet 与 seek 这两个回调里。 这样分层的好处只有一个:上层永远不知道自己在读什么。 读文件、读网络、读内存,对解封装器来说一模一样。 这也是为什么 Demo 47 里五种写法能认出同一个容器:它们走的是同一个外壳。
二、数据怎么走
上层要数据,走的是一条三级台阶。 级 发生什么 粒度
一 解封装器说要 N 个字节 可能只有几十 二 缓冲里还有就切给它 内存操作 三 缓冲空了才调回调再装满 一次 32768
第二级是这一层的全部价值。 没有它,上层读一个字节,底下就要碰一次磁盘或网卡——那速度没法看。 有了它,读盘次数被摊平到「每 32768 字节一次」,不管上层怎么零碎地要。 这个数不是随便定的:太小了不起作用,太大了浪费内存,32 KB 是几十年的经验值。
三、七个字段各管什么
AVIOContext 里真正要紧的就是这七个。 字段 角色 一句话
buffer 缓冲首址 装字节的地方 buf_ptr 读指针 下次从这里取 buf_end 尾指针 取到这里就没了 read_packet 读回调 真去取下一块 write_packet 写回调 输出方向用这个 seek 定位回调 跳着读时用 opaque 私有指针 回调怎么找到自己
前三个是「记账」,中间三个是「干活的」,最后一个是「传话的」。 opaque 这个字段最容易被忽略,却最要紧:回调是 C 函数,没有对象、没有 this, 它凭什么知道该读哪个文件?就靠这个指针。 PyAV 把这个指针换成了 Python 对象引用,所以你传进去的文件对象能被回调找回来。
四、两种创建方式
AVIO 既可以由 FFmpeg 自己造,也可以由你来造。 方式 谁造 什么时候用
自动 avformat_open_input 给个路径就行 自建 avio_alloc_context 数据来路特殊
绝大多数情况走第一条:av.open("x.mp4") 里面就在自动建 AVIO。
第二条才是 6.4 的主角。
数据加密了、在内存里、要从网络边下边解,就得自己造一个,把读回调换成自己的函数。
造的时候要给三样东西:一块缓冲、一个读回调、一个 seek 回调。
缓冲可以给 NULL,让 FFmpeg 自己分配,这是常见做法。
五、这一层为什么值得单讲一章
因为它解释了一堆「莫名其妙」的现象。 现象 原因就在这一层
网络流不能跳着播 seek 要重发请求,代价高 管道输入不能快进 没有 seek 回调可调 文件对象要 seek(0) AVIO 不替你复位指针 读小文件也走 32 KB 缓冲按块要,不按需
这四条都不是 bug,是设计。 想清楚这一层的职责,它们就都顺理成章了。 反过来,不学这一层,就会把它们当 bug 去「绕」——绕出来的代码往往更脆。
六、动手 Demo:AVIO 一条流背后到底碰了几次文件
拿一个记录 read/seek 的包装器套住文件,看底下真发生了什么。
import io, av
class Log(io.RawIOBase):
def __init__(self, f):
self.f, self.log = f, []
def readable(self):
return True
def readinto(self, b):
n = self.f.readinto(b)
self.log.append(("read", n))
return n
def seek(self, off, whence=0):
r = self.f.seek(off, whence)
self.log.append(("seek", off, whence, r))
return r
with open("tiny.mp4", "rb") as f:
w = Log(f)
c = av.open(w)
for i, fr in enumerate(c.decode(c.streams.video[0])):
if i >= 4:
break
c.close()
for x in w.log:
print(x)
实跑输出:
--------------------------------------------------------------------
AVIO 一条流背后到底碰了几次文件
--------------------------------------------------------------------
**① 把每一次 read 与 seek 都记下来**
解 5 帧,底下发生了 **3 次读**、**11 次定位**,读了 **78905 字节**。文件本身才 **46137 字节**。
**读的次数比帧数少得多**,因为 AVIO 是「一次要一大块、在内存里慢慢发」。
第一次要的就是 **32768 字节**——这个数正好是 AVIO 默认缓冲区的大小。
**② 完整轨迹**
次序 动作 参数 读出来的意思
-------------------------------------------------------
1 读 32768 字节 要一整块缓冲
2 定位 -1 / 2 末尾退一格,量大小
3 定位 32768 / 0 跳到第 32768 字节
4 读 13369 字节 要一整块缓冲
5 定位 -1 / 2 末尾退一格,量大小
6 定位 46137 / 0 跳到第 46137 字节
7 定位 48 / 0 跳到第 48 字节
8 读 32768 字节 要一整块缓冲
9 定位 -1 / 2 末尾退一格,量大小
10 定位 32816 / 0 跳到第 32816 字节
11 定位 -1 / 2 末尾退一格,量大小
12 定位 32816 / 0 跳到第 32816 字节
13 定位 -1 / 2 末尾退一格,量大小
14 定位 32816 / 0 跳到第 32816 字节
**第三列是「偏移 / whence」**,whence 为 0 是从头算、为 2 是从末尾算。
表里所有 `-1 / 2` 都是一个动作:**跳到末尾再退一格**,拿到返回值加一,就是文件大小。
**为什么这么绕?** 因为最古老的 seek 只保证「能跳」,不保证「能问大小」。
要量大小,只能先跳到末尾。
**③ AVIOContext 的七个关键字段**
字段 作用 本机能看到的证据
------------------------------------------------------------
buffer 缓冲区首地址 一次要 32768 字节
buf_ptr 已读到哪 上层每读一点就前移
buf_end 缓冲到哪结束 到头了才再要一块
read_packet 真正去读的回调 就是我们的 readinto
seek 真去定位的回调 就是我们的 seek
opaque 回调的私有指针 PyAV 里是那个文件对象
pos 当前逻辑位置 与 f.tell() 对得上
**这张表解释了整章的结构**:上面三个字段是内存里的缓冲,中间三个是回调,最后一个记账。
**教材讲 AVIO 就是讲这七个字段怎么配合。**
**④ 分成三层看**
层 谁在里面 一次要多少
--------------------------------------------------------
上层 解封装器,按帧按包要 几十字节都行
缓冲层 AVIOContext 一次 32768
协议层 file、http、rtp 各自实现 取决于协议
**换协议只换最下面那层**,中间那层与上面那层一个字都不用改。
这就是为什么同一条转码链既能读文件又能读网络。
**反过来说**:上层要是每次都只读一个字节,底下也只会每 32768 字节才真读一次盘。
**缓冲层把两边的粒度解耦了。**
解 5 帧只读了 3 次,每次都按 32768 的缓冲来。 11 次 seek 里有 5 次是「跳末尾量大小」,不是真跳。
30 秒实验
- 把缓冲改成一次要 1024 字节,看读的次数变几倍(PyAV 侧由 FFmpeg 决定,改不动,但可以在包装器里限制 readinto 一次只给 1024)。
- 在包装器里加一个计数器,看解完整段视频一共读了几块。
- 把
readinto改成故意少给一半字节,看解码还能不能跑完。
常见误区
- 以为 AVIO 是「一个协议」。 它是协议的外壳,真实现是两个回调。
- 以为层数多就是慢。 缓冲层正是为了不慢。
- 把
opaque当无关字段。 回调靠它才找得到自己那份数据。
自测三问(点开对答案)
1. AVIO 一次为什么按 32768 字节要,而不是要多少给多少? —— 为了把上层的零碎请求摊平,减少真碰磁盘/网卡的次数。 2. `buf_ptr` 与 `buf_end` 相等时会发生什么? —— 缓冲空了,触发 read_packet 再装一块。 3. `opaque` 是干什么的? —— 回调的私有指针。C 回调没有对象,靠它找回自己的上下文。- ☐ (本节完,下一节 6.2 AVIO案例实战之打开本地文件)