打开本地文件有五种写法,认出来的容器一模一样。 「能打开」只要 read,「能定位」才要 seek——这是两条独立的线。
6.1 讲的是结构,这一节开始动手。 下一篇 6.3 把同一个文件挂到网络上,看差别在哪。
一、五种写法
同一份字节,五个入口,结果一样。 写法 给的是什么 适合场景
路径 一个字符串 最常用 file: 前缀 带协议的字符串 路径里有冒号 concat: 前缀 一串路径 多个文件当一条流 文件对象 open 出来的 f 要自己控生命周期 BytesIO 内存里的一段字节 数据本来就在内存
file: 这个前缀是个容易忽略的保险。 路径里带冒号(Windows 盘符就是),
FFmpeg 会先把冒号前面当协议名。
写成 file:C:/x.mp4 就没这个问题:协议名被明确指定了,后面整串都当路径。
concat: 更冷门但有用:把几个文件首尾相接当一条流读,适合一堆按序号切好的分片。
二、三个坑,各报一种错
前两个报的是同一个错,原因却完全不同。 怎么错的 报什么 真原因
不 seek(0) 就再开 InvalidDataError 接着旧位置读,头没了 只给前 2000 字节 InvalidDataError 数据不够,探测不到容器 文本模式打开 TypeError 给的是字符串
第一条最容易踩:av.open(f) 读完不还原指针。
用完一次指针停在中间,同一个 f 再开就从那儿读起。
这不是 PyAV 偷懒:文件对象是你的,它凭什么替你复位?要复用就自己 f.seek(0)。
第三条报的不是 FFmpeg 的错,是 PyAV 在进 FFmpeg 之前就挡住了:它要字节流,你给它文本流。
报错类型本身就是线索:TypeError 是 Python 层拦的,
InvalidDataError 是 FFmpeg 判断「这不是我认识的格式」。
三、read 与 seek 是两件事
这是本节最重要的一条,很多人混着说。 能力 没有会怎样 谁通常没有
read 根本开不了 没有 seek 能开能播,跳不了 管道、摄像头 tell 部分格式读不下去 极少数流
demo 里拿一个只有 read 与 tell 的对象去开 mp4,居然开成功了。 因为打开只需要顺序读一段就能认出容器。 但接着调 seek 就露馅了:它返回成功,下一次读却直接崩。这就是「看起来成了」的典型。 所以判一个输入源能不能用,要问两遍:给不给 read?给不给 seek?只问一句会漏。
四、实战里怎么选
给路径最省事,能不用文件对象就别用。 情况 选哪个
普通本地文件 直接给路径 路径带冒号或中文 file: 前缀 + 正斜杠 数据已经在内存里 BytesIO 要控制打开与关闭时机 文件对象 + 每次 seek(0) 几个分片当一条流 concat: 前缀
文件对象唯一的好处是「你能自己管它」:可以先设好权限、先加解密、先测量大小,再交给 FFmpeg。 代价就是那条 seek(0) 的规矩得自己守。 这是用一个自由度换一条纪律,不亏,但要记住。
五、顺带说清 Windows 路径
反斜杠与正斜杠在这件事上不一样。
C:\视频\a.mp4 在 Python 字符串里要写两次反斜杠,倒不如一律用正斜杠 C:/视频/a.mp4,
FFmpeg 认。
但纯正斜杠路径开头带盘符时,av.open("C:/a.mp4") 会被当成协议名叫 C。
所以要写 file:C:/a.mp4。
这条规矩只看冒号:地址里第一个冒号前面那截,FFmpeg 一律当协议名。
知道这一条,所有诡异报错都能自己解释。
六、动手 Demo:AVIO 打开本地文件的五种写法与三个坑
先用五种写法开同一个文件,再故意用三种错法开一次。
import io, av
M = "tiny.mp4"
# 五种写法
print(av.open(M).format.name)
print(av.open("file:" + M.replace(chr(92), "/")).format.name)
print(av.open("concat:" + M).format.name)
print(av.open(open(M, "rb")).format.name)
print(av.open(io.BytesIO(open(M, "rb").read())).format.name)
# 三个坑
f = open(M, "rb")
c = av.open(f)
c.close()
print("读完停在", f.tell())
try:
av.open(f).close()
except Exception as e:
print("再开 =", type(e).__name__)
# 只有 read 没有 seek 的对象
class NoSeek(object):
def __init__(self, d):
self.d, self.p = d, 0
def read(self, n=-1):
b = self.d[self.p:self.p + n]
self.p += len(b)
return b
def tell(self):
return self.p
c = av.open(NoSeek(open(M, "rb").read()))
st = c.streams.video[0]
c.seek(1, stream=st)
try:
next(iter(c.decode(st)))
except Exception as e:
print("定位之后再读 =", type(e).__name__)
实跑输出:
--------------------------------------------------------------------
AVIO 打开本地文件的五种写法与三个坑
--------------------------------------------------------------------
**① 五种写法,结果一样**
写法 认出来的容器
----------------------------------------------
直接给路径 mov,mp4,m4a,3gp,3g2,mj2
file: 前缀 mov,mp4,m4a,3gp,3g2,mj2
concat: 前缀 mov,mp4,m4a,3gp,3g2,mj2
二进制文件对象 mov,mp4,m4a,3gp,3g2,mj2
BytesIO mov,mp4,m4a,3gp,3g2,mj2
**五种写法走的是同一套 AVIO**,只是最底下那层换了入口。
认出来的容器名一模一样,说明探测器看到的是同一段字节。
**`file:` 这个前缀不是装饰。** 它明确指定「用 file 协议」,而不是让 FFmpeg 去猜。
地址里带冒号的时候必须写它,否则 `C:/x.mp4` 会被当成协议名叫 `C` 的地址。
**② 三个坑,各报一种错**
怎么错的 报什么 为什么
-----------------------------------------------------------------------------
文件对象不 seek(0) 再开 InvalidDataError 接着上次的位置读,头没了
只给前 2000 字节 InvalidDataError 探测不到容器头
用文本模式打开 TypeError 给的是字符串不是字节
**第一条最容易踩**:`av.open(f)` 不会替你 seek 回开头。
用完一次,文件指针停在 **32816** 字节处,再开就直接从那儿读。
**要复用同一个文件对象,先 `f.seek(0)`。**
**第三条报的是 TypeError 不是 FFmpeg 的错**,因为它在进 FFmpeg 之前就被 PyAV 挡住了:
它要的是字节流。
**③ 反过来试:只给 read 不给 seek,行不行?**
结果是 **能开,认出 mov,mp4,m4a,3gp,3g2,mj2**。
**这就把一条常被混着说的边界划开了**:「能打开」只要 read,「能定位」才要 seek。
打开时用不到 seek 的格式(顺序读完就够),照样能解。
**那定位呢?再拿同一个对象试一次。**
`seek(1, stream=st)` **没报错**,可紧接着再解一帧,报的是 **InvalidDataError**。
**这是本节最值得记的一个陷阱**:定位这一步看起来成功了,底下其实一次都没跳成,
直到下一次读才炸出来。
**所以别信 `seek()` 的返回值**,它只是把请求转下去。成没成,要看定位之后那一次读。
**判断一个输入源能不能用,得问两句**:给不给 read?给不给 seek?
管道、标准输入、摄像头,通常只有前者。
seek(1, stream=st) 那句没报错,但它根本没跳成。
错在下一行才出来——这就是「看起来成了」。
30 秒实验
- 把
file:前缀去掉,写av.open("C:/x.mp4"),看报什么错。 - 给同一个文件对象加一行
f.seek(0),验证第二次能开。 - 用
concat:把两个 mp4 连起来,看时长是不是相加。
常见误区
- 写完不 seek(0) 就复用文件对象。 指针停在中间,头就丢了。
- 把「能打开」当成「能定位」。 两件事,要分别验。
- 以为路径里的反斜杠没事。 一律写成正斜杠更稳。
自测三问(点开对答案)
1. 为什么读完一次的文件对象,第二次开就报 InvalidDataError? —— 指针停在上次读到的位置,前面的容器头读不到了。要先 seek(0)。 2. 报 TypeError 与报 InvalidDataError 有什么不同? —— TypeError 是 PyAV 在 Python 层拦的,InvalidDataError 是 FFmpeg 判断格式不对。 3. 路径里为什么有时必须加 `file:` 前缀? —— FFmpeg 会把第一个冒号前那截当协议名,Windows 盘符会被误认。- ☐ (本节完,下一节 6.3 AVIO案例实战之打开网络直播流)