分类
技术

使用 Node.js 驱动 FFmpeg 在 Linux + vncserver 下完成视频录制

自动化录制屏幕有很多用途,比如生成教学视频、生成产品文档,等等。对比人工,自动化有很多好处:

  1. 避免创作者的设备和环境问题(比如邻居装修、麦克风不好等)
  2. 避免创作者的语言、发音问题(比如普通话不标准、不会说某种语言)
  3. 录制环境出现变化,可以方便的重录(比如换个背景图,界面有升级)
  4. 就像写博客一样,任何时候,拿出电脑或者手机都能编辑一段

所以目前研究这方面应用的很多,我厂也是。我近期就投入大量时间在这项工作上面,现在终于有所成果,写篇博客分享一下。

0. 准备环境

首先推荐大家使用 Linux。Linux 开源,有很多开源免费的工具可以完成各种操作,不仅可以录屏,还可以很容易地模拟各种用户操作,给我们留下大量开发空间。

建议选择有图形界面的 Linux 发行版,我尝试过 fedora 33 和 Debian 10 树莓派版,都很容易配置。如果使用纯命令行版本,然后自己完成安装图形界面,比如 gnome,再完成剩下来的配置,会很麻烦。

然后记得把系统更新到最新版,以规避可能遇到的问题。

1. 配置 vncserver

如果只能在主屏录制,这个产品的实用性就会大打折扣。所以我们选择用 vncserver 创建虚拟屏幕,然后在虚拟屏幕上完成录制。如果需要的话,也可以随时用 vnc viewer 之类的软件连上 VNC 实时查看效果,非常方便。

有些系统自带 vncserver,比如 Debian 10 树莓派,那就不用安装。我们选用 fedora 33,需要手动安装,这里推荐 TigerVNC,安装使用都很方便:

sudo dnf install tiger-vnc

安装完成后,使用:

vncserver :5 -geometry 1280x720

就可以创建虚拟显示器了。其中,:5 是显示器 id,可以顺延,比如 :6:7、甚至 :99,至于上限在哪里我暂时不知道。-geometry 1280x720 是设定显示器分辨率为 1280×720。

另外,还可以使用下面的命令查看和关闭显示器:

 vncserver -list
 vncserver -kill :5 

1.1 测试

配置完成之后,可以用 Firefox 测试一下效果。

# 安装 firefox
sudo dnf install firefox

# 在指定虚拟显示器打开 firefox
DISPLAY=:5 firefox https://cn.bing.com

# 截图,可能需要安装 xwd
xwd -root -display :5 > screen.xwd

# 转换成 png,可能需要安装 ImageMagick
convert screen.xwd screen.png

然后把图片下载到本地,或者启动一个 http 服务器就能看到了。

1.2 关闭桌面

默认的 VNC server 会启动桌面,此时可能会要求我们登录什么的。我们在这套系统当中并不需要桌面,只要有显示器即可,所以可以修改 ~/.vnc/xstartup 禁用:

#!/bin/sh
 

unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
# 把下面这行注释掉 
# exec /etc/X11/xinit/xinitr

2. 使用 FFmpeg 捕获屏幕内容

使用 FFmpeg 录屏比较简单,将输入源设置为指定显示器即可,formatx11grab,命令大体如下:

ffmpeg -y -i :5.0+0,0 -f x11grab -video_size 1280x720 -framerate 30 -c:v h264 a.mp4

其中,

  • -y 意思是自动覆盖前面生成的视频
  • :5.0+0,0 是使用刚才创建的 :5 显示器,用它的 0 号桌面,启动位置是 0,0 即左上角
  • -f x11grab 是使用 X server 抓取格式,Linux 下的图形界面一版是基于这个系统

3. 使用 node.js 驱动

最后只要用 node.js 的 child_process.spawn() 功能调用上面的命令即可。这段代码属于公司,我就不贴了,主要分享几点经验教训:

  1. 要用 spawn,因为录制过程中我们需要用输出来判断录制状态,exec 这种只能在结束时提供输出的没法用
  2. FFmpeg 会把输出输出到 stderr,理由不明,不过记得要用 stderr 来检查
  3. 录像完成,如果在命令行,按 q 或者 ctrl+C 都可以停止录像,并开始封装视频文件。在 node.js 里,我们可以调用 cp.kill('SIGINT') 。注意,调用之后,FFmpeg 子进程并没有立刻结束,它要把前面的录像进行封装,这个过程也是需要时间的,所以如果你接下来还要对视频文件进行操作,应该等待子进程彻底结束
  4. 判断录像开始的依据,我目前用的是:输出里包含 Output #0, mp4, to 'a.mp4'
  5. 判断录像结束,视频已经生成的依据,我用的是 cp.on('exit', onExit),然后在 onExit 里处理。注意,其它情况导致 ffpmeg 子进程退出时也会触发这个函数,所以我们必须检查 code。此时,在我的机器上,code 是 255,表示它是用户手动中止的,可以当作判断依据。

总结

剩下来的内容基本就是怎么驱动图形界面程序运行了。一般来说用 puppeteer 比较好,可以很容易的跟 node.js 联动,我厂的 showman 也是基于这个方案来实现的,最后贴一段视频,大家看下效果:

一段用上述技术生成的视频

参考阅读