🎬 第九章 FFmpeg命令行实现音视频设备采集
📄 一句话概括

Linux 上一行 -f v4l2 -i /dev/video0 就开干,参数只有五个。 其中最值钱的是 -input_format mjpeg——它能把带宽砍到几分之一。

📝 本节和前面几节的关系

9.5.1 把摄像头接进了虚拟机,这一节真去采它。 顺便把三平台的写法摆在一起对照。

一、Linux 上的一条命令

采五分钟存成文件,就这么长:

ffmpeg -f v4l2 -i /dev/video0 -c:v libx264 -t 5 out.mp4

参数 作用


-f v4l2 告诉它这是 Linux 摄像头接口 -i /dev/video0 第一个视频节点 -video_size 1280x720 要多大,不写用设备默认 -framerate 30 要多少帧,不写用设备默认 -input_format mjpeg 让设备直接吐 MJPEG

-t 5 是「录五秒就停」,不写会一直录到按 Ctrl+C。 这个参数在采集里特别有用——设备是没有「结束」这个概念的。

二、为什么一定要 -input_format mjpeg

USB 2.0 的理论带宽只有 480 Mbps,实际能用的约 60 MB/s。 而 720p 的裸 YUYV 一秒就要这么多:

格式 一帧字节 一秒(30 帧)


YUYV 422 1843200 52.7 MB RGB 24 2764800 79.1 MB H.264 4 Mbps — 0.5 MB

52.7 MB 对 60 MB/s,一个摄像头就吃掉近九成带宽。 再加一个摄像头,直接不够——这就是「插两个就都卡」的原因。

-input_format mjpeg 是让设备内部先压一道。 MJPEG 就是「每一帧都是一张 JPEG」,压缩率大概十分之一, 带宽立刻降到 5 MB/s 量级,两个摄像头也够了。

代价是 CPU:解码 MJPEG 要花力气。 所以这是「用 CPU 换带宽」——跟 9.2 里「设备吐原始、自己压」正好相反。

三、三个平台的写法对照

换平台只改前两个参数,后面那一长串可以照抄。

平台 命令


Linux ffmpeg -f v4l2 -i /dev/video0 … Windows ffmpeg -f dshow -i video="名字" … macOS ffmpeg -f avfoundation -i "0:0" …

差别只在「接口名 + 设备名的写法」: Linux 用路径,Windows 用设备名,macOS 用「视频序号:音频序号」。

macOS 的 0:0 特别容易写错——第一个 0 是视频设备序号, 第二个 0 是音频设备序号,不想采音频要写 0:none。

四、实测:本机四种结果对应四种原因

同一套试开代码,在 Windows 上得到四种完全不同的结果。

接口 结果 原因


v4l2 ValueError 接口没编进去 dshow OSError 接口在,但没设备 vfwcap 能解码 接口在,也有设备 gdigrab OSError 接口在,没有桌面会话

四种结果对应四种完全不同的排查方向: ValueError 只能换二进制,OSError 要考虑设备与权限, 「能解码」才是真正可用的。

采集中最费时间的判断就是这个—— 先分清「没有通路」「没有设备」「有设备但没权限」三件事, 再谈参数怎么调。

五、动手 Demo:四种结果,四种原因

下面这段对四个接口各试一次,把异常类型与宽高都打出来。 然后算一遍 720p 裸 YUYV 一秒要多少带宽。

import av

def probe(fmt, arg):
    c = None
    try:
        c = av.open(arg, format=fmt)
        st = c.streams[0]
        for _f in c.decode(st):
            return u"能解码 %dx%d" % (st.codec_context.width,
                                           st.codec_context.height)
        return u"解不出帧"
    except Exception as e:
        return type(e).__name__
    finally:
        if c is not None:
            c.close()

for fmt, arg in (("v4l2", "/dev/video0"),
                 ("dshow", "video=nonexist"),
                 ("vfwcap", "0"),
                 ("gdigrab", "desktop")):
    print(u"%-10s -> %s" % (fmt, probe(fmt, arg)))

W, H, FPS = 1280, 720, 30
for name, bpp in (("YUYV 422", 2), ("RGB 24", 3)):
    one = W * H * bpp
    print(u"%s 一帧 %d 字节  一秒 %.1f MB"
          % (name, one, one * FPS / 1048576.0))

实跑输出:

--------------------------------------------------------------------
9.5.2 FFmpeg 采集 USB 摄像头获取的数据
--------------------------------------------------------------------

**① Linux 上的一条命令**

`ffmpeg -f v4l2 -i /dev/video0 -c:v libx264 -t 5 out.mp4`

  参数                     作用
  --------------------------------------------------------------
  -f v4l2                  告诉 FFmpeg 这是 Linux 摄像头接口
  -i /dev/video0           第一个视频节点
  -video_size 1280x720     要多大,不给就用设备的默认值
  -framerate 30            要多少帧,不给就用设备的默认值
  -input_format mjpeg      让设备直接吐 MJPEG,省带宽

**`-input_format mjpeg` 是这一节最值钱的一个参数。**
**USB 2.0 的带宽只有 480 Mbps,一路 720p 的裸 YUYV 就要占掉大半;**
**换 MJPEG 是设备内部先压一道,带宽立刻降到几分之一。**

**② 实测:本机没有 v4l2,只有 Windows 那两个**

  接口        参数               结果           宽高
  ---------------------------------------------------------
  v4l2        /dev/video0        ValueError     —
  dshow       video=nonexist     OSError        —
  vfwcap      0                  能解码         640x480
  gdigrab     desktop            OSError        0x0

**`v4l2` 报 `ValueError`(没编),`dshow` 报 `OSError`(没设备),**
**`vfwcap` 能出画面,`gdigrab` 开着但解不出。**
**四种结果对应四种完全不同的原因——这就是采集中最费时间的判断。**

**③ 同一件事在 Windows 上怎么写**

  平台        命令
  -------------------------------------------------------
  Linux       `ffmpeg -f v4l2 -i /dev/video0 …`
  Windows     `ffmpeg -f dshow -i video="摄像头名" …`
  macOS       `ffmpeg -f avfoundation -i "0:0" …`

**三者的差别只在「接口名 + 设备名的写法」,后面编码、封装完全一样。**
**所以换平台时只需要改前两个参数,中间那一长串可以照抄。**

**④ 采到的原始帧有多大**

  格式             一帧字节     一秒(30 帧)
  -----------------------------------------------
  YUYV 422         1843200      52.7 MB
  RGB 24           2764800      79.1 MB
  H.264 4 Mbps     —            0.5 MB

**720p 的裸 YUYV 一秒要 53 MB,USB 2.0 的 60 MB/s 理论带宽被吃掉近九成。**
**这就是为什么一定要用 `-input_format mjpeg`——**
**先让设备压成几百 KB,再交给 FFmpeg 解码,带宽一下就够了。**

ValueError 与 OSError 是两种完全不同的事: 前者是「没这条通路」,后者是「通路有、设备没有」。

720p 的 YUYV 一秒 52.7 MB,对 USB 2.0 的 60 MB/s 来说太满了—— 这就是 -input_format mjpeg 存在的理由。

30 秒实验

  1. 在 -input_format mjpeg 下再算一次带宽:约十分之一,两个摄像头也够。
  2. 把 -video_size 改成 320x240:算一下带宽降到了多少。
  3. 把 -framerate 从 30 降到 15:帧率减半,带宽也减半。

常见误区

  1. 采 720p 裸 YUYV 还插两个摄像头。带宽直接不够,两个都卡。
  2. 把 ValueError 当成「设备没插」。它是接口没编进去。
  3. macOS 写 -i 0。少一个冒号,会被当成文件路径。
自测三问(点开对答案) 1. 720p 的 YUYV 一帧多少字节? —— 1843200 字节,等于 1280 乘 720 乘 2。 2. 为什么建议加 `-input_format mjpeg`? —— 让设备先压一道,带宽降到约十分之一,代价是多花 CPU 解码。 3. `ValueError` 与 `OSError` 分别说明什么? —— 前者是接口没编进去,后者是接口在但设备(或权限)没有。

相关笔记