📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

摄像头在 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 秒实验

  1. 把 video_size 改成 1921x1081 这种奇怪尺寸,看会不会报错、实际拿到的是多少。
  2. 把 framerate 改成 60,看帧间隔是不是真的变成 16 毫秒——如果还是 0.2 秒, 说明设备不认这个帧率。
  3. 试着打开一个不存在的设备(video=nonexistent),看报错是哪一种——再去对照 D27 那张表。

常见误区

  1. 以为设了分辨率就一定按这个给。 参数是请求,不满足时可能报错,也可能悄悄给你最接近的。 只能采几帧验。
  2. 以为 pts 从 0 开始。 gdigrab 给的是墙钟秒数(17 亿多),代码里必须记第一帧再减。
  3. 以为设备名可以猜。 带空格带中文的设备名拼错一个字符就打不开,报错还只说 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 方式:同样的设备,不用命令行,自己控制每一帧。

相关笔记