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

录屏就是把「桌面」当成一个摄像头来采,接口叫 gdigrab。 它的坑是「打开成功但抓不到」——以及某些画面录出来全黑。

📝 本节和前面几节的关系

9.5 讲的是摄像头,这一节换成「屏幕」这个特殊的采集源。 9.6.2 再看 Linux 上同一个需求怎么写。

一、一条命令录全屏

录屏命令跟采集命令长得几乎一样,只换了接口与输入。

ffmpeg -f gdigrab -framerate 30 -i desktop -c:v libx264 \ -preset veryfast -crf 23 -pix_fmt yuv420p out.mp4

参数 作用


-f gdigrab Windows 桌面抓取接口 -i desktop 整屏;也可写 title=窗口名 -framerate 30 抓多少帧 -offset_x/-offset_y 只抓一块区域的左上角 -video_size 1280x720 只抓一块区域的大小

title=窗口名 里的名字必须精确匹配,错一个字就抓不到。 不想记名字,可以用 -i title=记事本 试,失败会列出所有可用窗口标题。

只抓一块区域时,-offset_x、-offset_y、-video_size 三个要一起给。 只给 -video_size 不给偏移,等于从左上角开始裁。

二、实测:沙箱里四种写法全失败

这是本章唯一一节「演示跑不通」的内容,原因不是参数,是环境。

传的选项 结果


不传选项 OSError framerate=30 OSError video_size=640x480 OSError 两个都传 OSError

四种写法全在解码阶段失败,宽高要么 0 要么是给的那个值—— 说明参数是被接受的,只是没有内容可抓。

原因是沙箱里没有可交互的桌面会话。 真实的 Windows 机器上这四条都能跑,这一点要说清楚, 不能因为演示失败就说「这功能不能用」。

三、等价验证:用合成画面走一遍链路

抓不到桌面,但「编码 + 录制」这一段是可以单独验的。 用 lavfi 的 testsrc 造一个 640×480、30 fps 的画面顶替桌面。

参数 5 秒字节 折算码率


crf 18 1100456 1761 kbps crf 28 761863 1219 kbps 码率 800 kbps 528108 845 kbps

crf 从 18 到 28,体积只降三成——因为测试图是规整图案,特别好压。 真实桌面有文字、有渐变、有视频,同样 crf 会明显更大。

所以这张表只能用来「验证链路通」,不能拿来估真实录屏的体积。 要估体积,得按分辨率与帧率算,见下面那张表。

四、录屏体积怎么估

一行粗略算式:码率约等于「宽 × 高 × 帧率 × 0.1 bit」。

分辨率 帧率 估码率 1 分钟


1280x720 30 2500 kbps 18 MB 1920x1080 30 5000 kbps 37 MB 1920x1080 60 9000 kbps 66 MB 2560x1440 30 8000 kbps 59 MB

1080p30 按算式算出来是 6.2 Mbps,表里写 5 Mbps—— 差两成,够用来估硬盘,不够用来做精确的容量规划。

录一小时 1080p30 大约 2.2 GB。 这个数字记住,比记任何参数都有用——它决定你要不要清硬盘。

五、两个最常见的坑

这两条跟参数无关,但踩上就以为「FFmpeg 坏了」。

现象 原因


录出来全黑 显卡硬件加速绕过了 GDI 鼠标不见了 gdigrab 默认不抓光标

全黑那条最坑:gdigrab 抓的是 GDI 绘制的内容。 某些播放器、某些游戏用 DirectX 直接画在显卡上,GDI 这一层就是空的。

解法有两条:关掉那个程序的硬件加速,或者换用 ddagrab 这类新后端。 ddagrab 走的是桌面复制接口,能抓到显卡上画的画面, 但它对系统版本有要求,老机器上不一定有。

六、动手 Demo:确认抓不到桌面,再用合成画面验链路

下面这段对 gdigrab 试四种参数组合,记录失败发生在哪一步。 再用 lavfi 造 640×480、30 fps 的画面,按三档质量各录 5 秒。

import os, tempfile, time
from fractions import Fraction
import av

def grab(opt):
    c = None
    try:
        c = av.open("desktop", format="gdigrab", options=opt)
        for _f in c.decode(c.streams[0]):
            return u"能解码"
        return u"解不出帧"
    except Exception as e:
        return type(e).__name__
    finally:
        if c is not None:
            c.close()

for opt in ({}, {"framerate": "30"}, {"video_size": "640x480"}):
    print(u"gdigrab %-24s -> %s" % (str(opt), grab(opt)))

def record(path, crf=None, br=None, nframes=150):
    c = av.open("testsrc=size=640x480:rate=30", format="lavfi")
    o = av.open(path, "w")
    v = o.add_stream("libx264", rate=Fraction(30, 1))
    v.width, v.height, v.pix_fmt = 640, 480, "yuv420p"
    if crf:
        v.options = {"crf": str(crf)}
    else:
        v.bit_rate = br
    n = 0
    for f in c.decode(c.streams[0]):
        g = f.reformat(width=640, height=480, format="yuv420p")
        g.pts = n
        g.time_base = Fraction(1, 30)
        for p in v.encode(g):
            o.mux(p)
        n += 1
        if n >= nframes:
            break
    for p in v.encode(None):
        o.mux(p)
    o.close()
    c.close()
    return os.path.getsize(path)

d = tempfile.mkdtemp()
for tag, kw in (("crf18", {"crf": 18}),
                ("crf28", {"crf": 28}),
                ("br800k", {"br": 800000})):
    p = os.path.join(d, tag + ".mp4")
    sz = record(p, **kw)
    print(u"%s  5 秒 %d 字节  %.0f kbps" % (tag, sz, sz * 8 / 5.0 / 1000))

实跑输出:

--------------------------------------------------------------------
9.6.1 Windows 系统中 FFmpeg 录屏
--------------------------------------------------------------------

**① 一条命令录全屏**

`ffmpeg -f gdigrab -framerate 30 -i desktop -c:v libx264 \`
`  -preset veryfast -crf 23 -pix_fmt yuv420p out.mp4`

  参数                     作用
  ----------------------------------------------------------
  -f gdigrab               Windows 桌面抓取接口
  -i desktop               整屏;也可以写 `title=窗口名`
  -framerate 30            抓多少帧,不是编码多少帧
  -offset_x/-offset_y     只抓一块区域时的左上角
  -video_size 1280x720     只抓一块区域时的大小

**`-i desktop` 抓的是整个桌面,包括任务栏;只想抓一个窗口就写**
**`-i title=记事本`。标题必须精确匹配,错一个字就抓不到。**

**② 实测:沙箱里 gdigrab 打不开**

  传的选项                              结果        宽高
  --------------------------------------------------------------
  不传选项                              OSError     0x0
  framerate=30                          OSError     0x0
  video_size=640x480                    OSError     640x480
  framerate=30/video_size=1280x720     OSError     1280x720

**四种写法全在解码阶段失败,这是沙箱没有桌面会话导致的,**
**跟参数怎么写无关——真实机器上这四条都能跑。**
**所以这一节只能拿「等价的合成画面」把编码与录制链路验一遍。**

**③ 等价验证:同样 640×480、30 fps,录 5 秒**

  参数              字节        折算码率
  -------------------------------------------
  crf 18            1100456     1761 kbps
  crf 28            761863      1219 kbps
  码率 800 kbps     528108      845 kbps

**同样是 5 秒,crf 18 出来 1 MB 出头,crf 28 只有 760 KB——**
**差的不多,因为测试图本身是规整的图案,比真实桌面好压。**
**真实桌面有文字、有渐变、有视频,同样 crf 会明显更大。**

**④ 录屏体积怎么估**

  分辨率        帧率     估码率        1 分钟
  -----------------------------------------------
  1280x720      30       2500 kbps     18 MB
  1920x1080     30       5000 kbps     37 MB
  1920x1080     60       9000 kbps     66 MB
  2560x1440     30       8000 kbps     59 MB

**一行粗略的算式:码率约等于「宽 × 高 × 帧率 × 0.1 bit」。**
**1080p30 算出来 6.2 Mbps,跟实测的 5 Mbps 差不多,够用来估硬盘。**

**⑤ 录屏最常踩的两个坑**

  现象           原因
  -----------------------------------------
  录出来全黑     显卡硬件加速绕过了抓屏
  鼠标不见了     gdigrab 默认不抓光标

**全黑那条最坑:gdigrab 抓的是 GDI 绘制的内容,**
**某些播放器用 DirectX 直接画在显卡上,GDI 这一层就是空的。**
**解法是关掉播放器的硬件加速,或者换用 `ddagrab` 这类抓取后端。**

gdigrab 四组参数全在解码阶段报 OSError—— 说明参数被接受了,只是没有桌面可抓,跟写法无关。

后面三行是「等价链路」的验证:编码、封装、落盘都正常, 换成真桌面时只差「帧从哪来」这一步。

30 秒实验

  1. 把 testsrc 换成 smptebars:彩条更规整,体积会更小。
  2. 把帧数从 150 改成 300:时长翻倍,体积也差不多翻倍。
  3. 把 -framerate 从 30 改成 15:同样 5 秒,帧数减半,体积也降。

常见误区

  1. 因为沙箱抓不到就说「FFmpeg 录屏不能用」。是环境没有桌面会话。
  2. 用测试图的体积去估真实桌面。真实桌面更复杂,会明显更大。
  3. 录出全黑就以为参数错了。多半是硬件加速绕过了 GDI。
自测三问(点开对答案) 1. 录屏跟采集摄像头在 FFmpeg 里有什么区别? —— 几乎没区别,只是把接口换成 gdigrab、输入换成 desktop。 2. 为什么沙箱里 gdigrab 打不开? —— 没有可交互的桌面会话,参数被接受但没有内容可抓。 3. 录出来的画面全黑是什么原因? —— 目标程序用 DirectX 直接画在显卡上,GDI 那一层是空的。

相关笔记