SDK 方式比命令行多的不是功能,是「控制权」:逐帧、可中断、能兜错。 但设备打不开时的三种报错,长得完全不一样——分不清就会修错方向。
上一节用命令行采到了帧,这一节换成代码,并把参数一个个对上。 这一节的重点是「报错分类」:设备问题、构建问题、参数问题,三种错的处理完全不同。 SDK 的完整用法在 32 课,那一本讲的是用 API 做推流与美颜;这一节只管「把帧拿到手」。
一、三行代码,三个对象
SDK 采一帧只要三行:打开、取流、解码。
| 行 | 做什么 | 失败时的典型错误 |
|---|---|---|
av.open(url, format=…, options=…) |
开设备 | OSError I/O error |
streams.video[0] |
取视频流 | IndexError 没有流 |
decode(stream) |
解出帧 | 一直是空 |
第一行的 format 参数不能省:给了 video=名字 但没给 dshow,FFmpeg 会当成一个文件名去找,
报「文件不存在」。
这个错最误导人——明明是设备问题,报的却是文件问题。
二、三种报错,三种病
D27 实测的三种结果,正好对应三类故障:
| 报错 | 真实原因 | 怎么修 |
|---|---|---|
| I/O error | 设备名错、被占用、没插 | 先列设备,再关占用程序 |
| no container format | 这个构建没编进该设备层 | 换 FFmpeg 构建 |
| 打不开但不报错 | 参数不被支持 | 去掉参数再试 |
第二行在 Linux 上特别常见:v4l2 是系统自带的东西,
但你拿到的那个 FFmpeg 二进制/库不一定编了它。
报「没有这个格式」的时候,先怀疑库,别怀疑设备。
第三行最难查:设备能开,但一帧也不出。通常是分辨率或像素格式不被支持,而设备层不吭声。 验法是退到最小参数:只给格式名,什么都不设,能出帧再一个个加。
三、时间戳比帧更可信
D27 里有个反直觉的实测:帧「到达的时刻」全是 0,但 pts 差得很有规律。
| 量的是什么 | 实测结果 | 能不能用 |
|---|---|---|
| 读到帧的时刻差 | 全 0.0 毫秒 | 不能,被缓冲骗了 |
| pts 的差 | 91 到 110 毫秒 | 能,这才是真相 |
为什么到达时刻不可信:采集层在后台一直往缓冲里塞帧,你的循环一开,攒好的几帧瞬间全吐出来。 所以「我什么时候读到」和「这一帧属于哪一刻」是两件事,后者才叫 pts。
这条在第五章做录像回放时是致命的:按到达时刻写文件,录出来的视频会「一阵快一阵慢」, 而 pts 是对的。
四、SDK 比命令行多出来的三件事
这三件事才是「必须用 SDK」的理由:
| 能力 | 命令行做不到的地方 |
|---|---|
| 逐帧判断 | 命令行只能整段转,中途不能按内容决定要不要 |
| 异常兜住 | 命令行进程退出,SDK 能 catch 了继续 |
| 把帧给别人 | 命令行只能写文件或推流,SDK 能直接送进渲染或算法 |
第三行是本课的主线:采到的帧要送进 SDL 显示(3.3.6)、送进算法做检测(第四章)、 送进编码器存盘(第五章)。 命令行在这些场景里都用不上,因为它的输出只能是文件或流。
五、动手 Demo:三种报错各试一次,再看 pts 稳不稳
先用三种格式各开一次设备,把报错收集齐;再采八帧,看设备自己打的 pts 抖成什么样。 这一步的目的不是「跑通」,是「认得三种错」。
for fmt, url in ((u"dshow", u"video=nonexistent"),
(u"v4l2", u"/dev/video0"),
(u"gdigrab", u"desktop")):
try:
cc = av.open(url, format=fmt,
options={"framerate": "5",
"video_size": "320x240"})
cc.close()
print(fmt, "打开成功")
except Exception as e:
print(fmt, type(e).__name__, str(e)[:60])
# 再看 pts:只能信它,不能信「读到它的时刻」
rows, t_first = [], None
for fr in c.decode(s):
t = float(fr.pts * s.time_base)
if t_first is None:
t_first = t
rows.append(t - t_first)
if len(rows) >= 8:
break
实跑输出:
--------------------------------------------------------------------
SDK 方式读摄像头,和命令行的三点差别
--------------------------------------------------------------------
**SDK 方式不是「命令行的包装」,它多给你三样东西:逐帧控制、原始时间戳、自己决定怎么处理。**
命令行 SDK
------------------------------------------------
粒度 整段处理 一帧一帧
时间戳 自己算 每帧带 pts
处理 只能交给滤镜 想怎么改就怎么改
异常 进程退出 自己能兜住
**先看设备不存在时会怎样**——这条错误信息必须认得:
dshow → OSError:[Errno 5] I/O error: 'video=nonexistent'
v4l2 → ValueError:no container format 'v4l2'
gdigrab → **打开成功**
**三条结果对应三种真实故障**:
现象 含义 怎么办
-----------------------------------------------------------------------------
I/O error 设备名错或设备被占用 换名字、关掉占用的程序
no container format 这个构建没编进该设备层 换 FFmpeg 构建
打开成功 能用 直接往下走
**第二条最容易误判**:Linux 上 `v4l2` 明明存在,但 PyAV 这个构建没编进去,报的是「没有这个格式」,
**不是「没设备」**。
**这两种错的处理完全不同**:一个去插设备,一个去换库。
**第二样差别:时间戳。** 采 8 帧,看设备自己打的 pts 稳不稳:
帧 pts(自首帧,秒) 与上一帧差
-------------------------------------------
1 0.000 —
2 0.094 93.7 毫秒
3 0.189 94.9 毫秒
4 0.292 103.7 毫秒
5 0.388 96.0 毫秒
6 0.496 107.5 毫秒
7 0.592 95.8 毫秒
8 0.699 107.6 毫秒
名义 10 fps 该是 100 毫秒一帧,实际 **94 到 108 毫秒**。
**这就是「摄像头不可靠」的第一课**:它说 10 fps,给的是**平均** 10 fps,单帧间隔会抖。
**所以播放端不能按「每帧固定 100 毫秒」显示**,必须按 pts。这一条在第五章做成系统时会反复出现。
**还有一件事这段量不出来,得说清**:我想量「帧真正到达的时刻」,但 gdigrab 的采集层有缓冲,
循环一开就把攒好的几帧瞬间全吐出来,实测相邻到达间隔全是 0.0 毫秒。
**那个数不是真相,是缓冲造成的假象**——**所以判断帧间隔只能信 pts,不能信「我什么时候读到它」。**
三条结果一条都不能少:I/O error、no container format、打开成功,
分别代表「设备不对」「库不对」「能用」。
中间那条最容易误判:Linux 上明明插着摄像头,报的却是「没有这个格式」,
因为这个构建没编 v4l2。
去插设备没用,要换库。
pts 那张表更值得看:名义 10 fps 该是 100 毫秒,实测 91 到 110 毫秒。抖动接近 ±10%。 这不是设备坏,是正常现象:采集卡的晶振、USB 调度、系统调度都会影响单帧间隔。 所以播放端必须按 pts 显示,不能按「每帧固定时长」——按固定时长的话,抖动会被累积成漂移。
30 秒实验
- 把
video_size去掉再开 gdigrab,看它默认给多大。 - 把
framerate设成1000,看 pts 间隔能不能真的到 1 毫秒——猜猜它会不会悄悄降回来。 - 在循环里数「读到但
fr.pts为 None」的帧,看有没有——有的话说明这一路流的时间戳不可靠。
常见误区
- 以为给了 URL 就不用给 format。 不给
dshow,FFmpeg 会当成文件名找, 报「文件不存在」——明明是设备问题,报的却是文件问题。 - 以为报「没有这个格式」就是设备没插。 那是构建没编进该设备层,要去换 FFmpeg 库, 不是去插摄像头。
- 以为帧的间隔可以按到达时刻算。 采集层有缓冲,循环一开就把攒的几帧瞬间吐出来, 到达间隔全是 0。只能信 pts。
自测三问(点开对答案)
1. 打开 dshow 设备时报「文件不存在」,最可能是什么原因? —— 漏了 `format='dshow'`。没有格式名,FFmpeg 把 `video=名字` 当成文件路径去找。 2. 「no container format」这个错该怎么处理? —— 换 FFmpeg 构建。这个错的意思是「这个库里没有这个设备层」,和设备插没插没关系。 3. 为什么不能用「读到帧的时刻」当帧的时间戳? —— 采集层有缓冲,`decode` 一开就把攒好的几帧瞬间返回,到达间隔接近 0。真实时刻只有 pts 里有。下一节进 SDL2:帧拿到了,接下来要把它画到屏幕上。
- ☐ (本节完,下一节 03.03.01 SDL2简介)