录屏就是把「桌面」当成一个摄像头来采,接口叫 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 秒实验
- 把
testsrc换成smptebars:彩条更规整,体积会更小。 - 把帧数从 150 改成 300:时长翻倍,体积也差不多翻倍。
- 把
-framerate从 30 改成 15:同样 5 秒,帧数减半,体积也降。
常见误区
- 因为沙箱抓不到就说「FFmpeg 录屏不能用」。是环境没有桌面会话。
- 用测试图的体积去估真实桌面。真实桌面更复杂,会明显更大。
- 录出全黑就以为参数错了。多半是硬件加速绕过了 GDI。
自测三问(点开对答案)
1. 录屏跟采集摄像头在 FFmpeg 里有什么区别? —— 几乎没区别,只是把接口换成 gdigrab、输入换成 desktop。 2. 为什么沙箱里 gdigrab 打不开? —— 没有可交互的桌面会话,参数被接受但没有内容可抓。 3. 录出来的画面全黑是什么原因? —— 目标程序用 DirectX 直接画在显卡上,GDI 那一层是空的。- ☐ 9.6.1 完,下一节 9.6.2 Linux 系统中 FFmpeg 录屏