摄像头在 FFmpeg 眼里不是「硬件」,是一个「输入格式」。
换上 -f dshow 或 -f v4l2,它就和读一个 MP4 没区别——这条认识一建立,后面全通。
前两章讲的是「别人发、我来收」,这一章转到「我自己去采」。 先命令行、再 SDK,这个顺序不能反:命令行能立刻告诉你设备支不支持某组参数。 命令行与 API 的完整对照在 3.2,这一节先把「能采到」这件事跑通。
一、设备名就是 URL
avformat_open_input 的第一个参数叫 URL,但在设备上它就是「设备名」。
| 平台 | 格式名 | URL 写法 |
|---|---|---|---|
| Windows | dshow | video=USB Camera |
| Linux | v4l2 | /dev/video0 |
| macOS | avfoundation | 0(设备序号) |
| 屏幕(Windows) | gdigrab | desktop |
这一行表能省掉很多试错:同一个摄像头,三个平台三种写法,而上层代码几乎一样。 第四行是本课的救命稻草——沙箱里没有摄像头,但屏幕采集走的是同一条路, 所以本课的采集链能真跑。
二、参数是「请求」,不保证满足
-video_size 1920x1080 是「我想要」,不是「我要到了」。
| 参数 | 写法 | 不满足时 |
|---|---|---|
| 分辨率 | -video_size WxH |
报错,或悄悄换成最接近的 |
| 帧率 | -framerate 30 |
报错,或按设备实际帧率来 |
| 像素格式 | -pix_fmt yuyv422 |
报错 |
| 输入格式 | -f dshow |
找不到设备层 |
第二行是最容易被骗的一行:有些设备接受 30,实际只给 15,而且不报错。 验证的办法只有一个:采几帧,看 pts 的间隔——D26 就是干这个的。
三、采到的 pts 可能不从 0 开始
D26 里 gdigrab 给的 pts 是 17 亿多,因为它是墙钟(Unix 秒)。而摄像头的 pts 一般从 0 起。
这条差异会直接搞崩计时逻辑:
| 代码写法 | 后果 |
|---|---|
if pts == 0: 起点 = now |
永远不成立,起点一直是 None |
delay = pts - 0 |
延迟算成 17 亿秒 |
起点 = 第一帧 pts,后面减它 |
正确 |
第三行是唯一正确的写法,D26 的表就是这么算的。记住这一条,能少调一天。
四、为什么还要用命令行
既然要写代码,为什么不直接写?因为命令行能「试」。
| 要确认的事 | 命令行怎么试 |
|---|---|
| 设备叫什么名字 | ffmpeg -list_devices true -f dshow -i dummy |
| 支持哪些分辨率帧率 | 看报错里列的 Supported size |
| 能不能出画面 | -t 3 out.mp4 然后放 |
| 码率大概多少 | -f null - 看输出统计 |
第一行那个 -list_devices 是最常用的:Windows 上设备名带空格、带中文,拼错一个字符就打不开。
而报错只说 I/O error,不说「名字不对」——所以第一步永远是先列设备。
五、动手 Demo:用屏幕采集代替摄像头,把参数试一遍
沙箱没有摄像头,但 gdigrab 走的是同一条「输入设备」路径,所以参数怎么生效、pts 怎么来, 都能真看到。 这一段的价值在验证「参数是请求」和「pts 不从 0 开始」这两条。
c = av.open("desktop", format="gdigrab",
options={"framerate": "5", "video_size": "640x360"})
s = c.streams.video[0]
got = []
for fr in c.decode(s):
got.append((fr.width, fr.height,
float(fr.pts * s.time_base)))
if len(got) >= 5:
break
c.close()
# pts 要从第一帧起算,不能假设从 0 开始
base = got[0][2]
for w, h, pts in got:
print(w, h, pts - base)
实跑输出:
--------------------------------------------------------------------
命令行采摄像头,参数和 API 是一一对应的
--------------------------------------------------------------------
**命令行 `-f dshow -i video="USB Camera"` 里的每一项,都能在 SDK 里找到对口参数。**
**先会用命令行,再换 API,中间不用重新学一遍。**
命令行写法 SDK 里对应什么 作用
---------------------------------------------------------------------------------
`-f dshow` `av_find_input_format("dshow")` 选设备层
`-i video=名字` `avformat_open_input(url)` 设备名就是 URL
`-video_size 1280x720` `options["video_size"]` 分辨率
`-framerate 30` `options["framerate"]` 帧率
`-pix_fmt yuyv422` `options["pixel_format"]` 像素格式
`-t 10` 自己数帧,到点停 采多久
**沙箱里没有摄像头,但有一样东西走的是同一条路:屏幕采集(gdigrab)。**
在 FFmpeg 眼里,它和摄像头**都是「输入设备」**——同样要开设备、同样给尺寸帧率、同样出帧。
真机上摄像头的命令行长这样(Windows 用 dshow,Linux 用 v4l2):
ffmpeg -f dshow -i video="USB Camera" -video_size 1280x720 -framerate 30 out.mp4
ffmpeg -f v4l2 -i /dev/video0 -video_size 1280x720 -framerate 30 out.mp4
换成等价 API,采 5 帧看参数生效没有:
开设备 1222.3 毫秒,采 5 帧用了 5 毫秒
帧 尺寸 pts(秒,减首帧) 相邻差
-----------------------------------------------------
1 640×360 +0.000 —
2 640×360 +0.190 0.190 秒
3 640×360 +0.390 0.200 秒
4 640×360 +0.599 0.208 秒
5 640×360 +0.790 0.192 秒
**参数确实生效了**:要 640×360 就拿到 640×360,
帧间隔除了首帧那次(要等第一次采集)之外都稳在 0.19 到 0.21 秒。
**这就是命令行的价值**:先确认设备支不支持这组参数,再写代码。**反过来做,会白调半天。**
**注意 pts 的原始值是 17 亿多**:gdigrab 拿墙钟(Unix 秒)当 pts,不从 0 开始。
摄像头一般从 0 开始,**所以代码里绝不能假设「第一帧 pts 是 0」**。
正确做法是记下第一帧的 pts,后面全部减它——上面那张表就是这么算的。
**命令行有一件事 API 替不了:试参数。** 设备支持的尺寸帧率是**枚举出来的**,不是随便填。
命令行报错会直接告诉你「Supported size: …」,照着抄就行。
代码里得自己处理——`dshow` 不支持时返回 `EIO`,什么也不告诉你。
要 640×360 就拿到 640×360,帧间隔稳在 0.19 到 0.21 秒(5 fps 对应 0.2 秒),参数确实生效了。 开设备花了 1.2 秒——这是 gdigrab 初始化的一次性成本。 换成 USB 摄像头会短很多,但也在一两百毫秒量级。 这个数解释了一个常见现象:程序刚启动时画面要等一两秒才出来, 不是程序卡了,是在等设备初始化。
pts 那个 17 亿是本节最该记住的数:它不是 bug,是设计。
gdigrab 拿系统时间当时间基准,好处是「同一次采集里,不同设备的 pts 可以互相对齐」。
所以代码里绝不能出现 pts == 0 这种判断,只能「记第一帧、后面减它」。
30 秒实验
- 把
video_size改成1921x1081这种奇怪尺寸,看会不会报错、实际拿到的是多少。 - 把
framerate改成60,看帧间隔是不是真的变成 16 毫秒——如果还是 0.2 秒, 说明设备不认这个帧率。 - 试着打开一个不存在的设备(
video=nonexistent),看报错是哪一种——再去对照 D27 那张表。
常见误区
- 以为设了分辨率就一定按这个给。 参数是请求,不满足时可能报错,也可能悄悄给你最接近的。 只能采几帧验。
- 以为 pts 从 0 开始。 gdigrab 给的是墙钟秒数(17 亿多),代码里必须记第一帧再减。
- 以为设备名可以猜。 带空格带中文的设备名拼错一个字符就打不开,报错还只说 I/O error。
先
-list_devices。
自测三问(点开对答案)
1. `avformat_open_input` 的 URL 参数,在设备上是什么? —— 设备名。Windows 是 `video=摄像头名`,Linux 是 `/dev/video0`,macOS 是设备序号。 2. 怎么确认「帧率参数真的生效了」? —— 采几帧,算相邻 pts 的差。要 30 fps 就该是 33 毫秒左右,还是 66 毫秒说明设备没认这个帧率。 3. 为什么 pts 不能假设从 0 开始? —— 某些设备层(如 gdigrab)用墙钟当时间基准,pts 是 Unix 秒。必须记第一帧的 pts,后面全部减它。下一节换成 SDK 方式:同样的设备,不用命令行,自己控制每一帧。
- ☐ (本节完,下一节 03.02 FFmpeg的SDK方式读取本地摄像头)