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

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 秒实验

  1. 把 video_size 去掉再开 gdigrab,看它默认给多大。
  2. 把 framerate 设成 1000,看 pts 间隔能不能真的到 1 毫秒——猜猜它会不会悄悄降回来。
  3. 在循环里数「读到但 fr.pts 为 None」的帧,看有没有——有的话说明这一路流的时间戳不可靠。

常见误区

  1. 以为给了 URL 就不用给 format。 不给 dshow,FFmpeg 会当成文件名找, 报「文件不存在」——明明是设备问题,报的却是文件问题。
  2. 以为报「没有这个格式」就是设备没插。 那是构建没编进该设备层,要去换 FFmpeg 库, 不是去插摄像头。
  3. 以为帧的间隔可以按到达时刻算。 采集层有缓冲,循环一开就把攒的几帧瞬间吐出来, 到达间隔全是 0。只能信 pts。
自测三问(点开对答案) 1. 打开 dshow 设备时报「文件不存在」,最可能是什么原因? —— 漏了 `format='dshow'`。没有格式名,FFmpeg 把 `video=名字` 当成文件路径去找。 2. 「no container format」这个错该怎么处理? —— 换 FFmpeg 构建。这个错的意思是「这个库里没有这个设备层」,和设备插没插没关系。 3. 为什么不能用「读到帧的时刻」当帧的时间戳? —— 采集层有缓冲,`decode` 一开就把攒好的几帧瞬间返回,到达间隔接近 0。真实时刻只有 pts 里有。

下一节进 SDL2:帧拿到了,接下来要把它画到屏幕上。

相关笔记