前言
在上一期我们使用Electron
实现了一个自己的录屏软件,评论区有小伙伴说如何支持区域录制。诚然区域录制也是一个十分常见的需求,所以今天我们就来实现一下录屏软件的区域录制。
前置优化
在开始区域录制之前,先优化一下之前的一些实现方式,以及重新定义一些概念。方便我们更好地开始实现后面的逻辑。
重新定义清晰度
在上一期中我们定义了几种清晰度标准:
- 超清:
3840x2160
分辨率 - 高清:
1280x720
分辨率 - 标清:
720x480
分辨率
然后通过ffmpeg
指定分辨率去录制不同质量的视频,但其实这样的实现是有一些问题的。试想一下你的录制硬件设备最高的分辨率都没达到超清的分辨率,那么设置超清的分辨率也是没有意义的。
所以录制的时候我会不指定分辨率,这样ffmpeg
就会采用最高分辨率来录制,录制完成之后再把视频转成指定的分辨率。这里的分辨率我也重新定义了一下,现在是以百分比来设置。
const DEFINITION_LIST = [ { label: '100%', value: '1' }, { label: '75%', value: '0.75' }, { label: '50%', value: '0.5' }, { label: '25%', value: '0.25' } ]
录制命令就改成了下面的样子
const ffmpegCommand = `${ffmpegPath} -f avfoundation -r ${frameRate} -i "1" -c:v libx264 -preset ultrafast ${fileName}`
视频格式转换
录制完成之后,再调用ffmpeg
的视频格式转换功能,这个时候顺便把分辨率一起设置一下。录制完成后对应的子进程就会退出,此时监听子进程的退出事件,调用转码方法
ffmpegProcess.on('exit', (code, signal) => { console.log(`Recording process exited with code ${code} and signal ${signal}`) afterRecord() })
下面是afterRecord
的部分实现,解释一下实现流程:
fileName
是录制的默认mp4
文件,output
是我们要输出的视频文件- 根据选择导出的视频格式,将
mp4
文件转成对应的格式 - 启动一个子进程,执行
ffmpeg
命令处理视频 - 视频处理完成后删除默认
mp4
文件,并打开保存视频的文件夹
const output = `${FILE_PATH}/record-${moment().format('YYYYMMDDHHmmss')}.${ext}` if (ext === 'mp4') { command = `${ffmpegPath} -i ${fileName} -vf "scale=${scale}" -c:a copy ${output}` } else if (ext === 'webm') { command = `${ffmpegPath} -i ${fileName} -vf "scale=${scale}" -c:v libvpx -c:a libvorbis ${output}` } else if (ext === 'gif') { command = `${ffmpegPath} -i ${fileName} -vf "fps=15,scale=${scale}:flags=lanczos" -c:v gif ${output}` } let progress = spawn(command, { shell: true }) progress.stderr.on('data', (data) => { console.log(`FFmpeg Convert Log: ${data}`) }) progress.on('exit', (code, signal) => { console.log(`Recording process exited with code ${code} and signal ${signal}`) fs.unlinkSync(fileName) if (code == 0) { shell.openPath(FILE_PATH) progress = null ffmpegProcess = null } })
区域窗口
下面来实现区域的录制,首先我们要实现一个能框住某个区域的窗口。这样我们在录制的时候,视频的录制范围就是区域的范围。
我在托盘菜单这里加了一个选取区域的菜单,点击这个菜单的时候会打开一个新窗口,这个新窗口是可以移动并且调整大小的。后续的屏幕录制就会以这个窗口的大小位置为基准录制。 链接
实现逻辑并不复杂,具体如下:
- 创建一个新窗口,
frame
为false
表示无边框,transparent
为true
表示透明窗口 loadURL
加载一个空的html
页面dragWindow
方法实现整体窗口可拖动,具体在第一期实现过,这里就不再赘述executeJavaScript
给新窗口注入一些样式
let areaWindow export const closeArea = () => { if (areaWindow) { areaWindow.close() areaWindow = null } } export const openRecordArea = () => { if (areaWindow) { closeArea() return } const newWindow = new BrowserWindow({ width: 600, height: 600, frame: false, transparent: true, webPreferences: { preload: PRELOAD_URL, sandbox: false } }) areaWindow = newWindow newWindow.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent('<html><body></body></html>')}` ) newWindow.on('ready-to-show', () => { newWindow.show() }) newWindow.on('close', () => { areaWindow = null }) newWindow.webContents.on('did-finish-load', () => { dragWindow(newWindow) newWindow.webContents.executeJavaScript(` const customStyles = \` html, body { padding: 0; margin: 0; background: transparent; border-radius:4px; } body { border: 2px dashed #ccc; } #root { display: none } \`; const styleTag = document.createElement('style'); styleTag.textContent = customStyles; document.head.appendChild(styleTag); `) }) }
指定区域录制
开始录制之前要先介绍三个概念,物理像素、逻辑像素跟DPI。
物理像素是显示屏上的实际光点或发光元素,是硬件层面上的概念,它们直接映射到显示设备的硬件组件。
逻辑像素是在软件层面上的概念,是应用程序和操作系统中用于描述图像和界面的基本单位。逻辑像素通常不直接映射到硬件上的物理像素,而是由操作系统和图形引擎处理,以适应不同的显示设备和分辨率。
DPI
代表“每英寸点数”(Dots Per Inch)
,它是一种用来度量打印设备、扫描仪、显示器或数字图像设备分辨率的单位。DPI表示在一英寸的空间内有多少个点或像素。
对于我们的功能需要注意的就是,ffmpeg
操作的像素都是物理像素,而我们的录制区域获取到的值都是逻辑像素。比如说我们通过以下的代码获取录制区域的位置大小信息:
const size = areaWindow.getSize() const position = areaWindow.getPosition() const [width, height] = size const [left, top] = position
这里的高度、宽度、距离等等都是逻辑像素。
我们是使用ffmpeg
的裁剪来录制指定区域,比如说下面的命令
ffmpeg -f avfoundation -r 30 -i "1" -vf "crop=800:600:100:100" output.mp4
-vf "crop=800:600:100:100"
的意思是裁剪视频,使其宽度为800
,高度为600
,并且从左上角坐标 (100, 100)
开始裁剪。
所以我们在计算窗口大小,即视频的分辨率大小时,是需要将逻辑像素转换成物理像素的。物理像素=逻辑像素×DPI
。所以我们获取录制区域信息的时候,需要这样计算
let cropRect = {} const size = areaWindow.getSize() const position = areaWindow.getPosition() const [width, height] = size const [left, top] = position const mainScreen = screen.getPrimaryDisplay() const { scaleFactor } = mainScreen cropRect = { width: width * scaleFactor, height: height * scaleFactor, left: left * scaleFactor, top: top * scaleFactor }
接着就可以使用crop进行区域录制了
const { frameRate } = config fileName = `${FILE_PATH}/${moment().format('YYYYMMDDHHmmss')}.mp4` const cropString = !isEmpty(cropRect) ? `-vf "crop=${cropRect.width}:${cropRect.height}:${cropRect.left}:${cropRect.top}"` : '' /**统一先录制为mp4,避免受硬件影响 */ const ffmpegCommand = `${ffmpegPath} -f avfoundation -r ${frameRate} -i "1" ${cropString} -c:v libx264 -preset ultrafast ${fileName}`
这里再看一个问题,如果我的选区是600*600
,DPI
为2
,那么录制出来的视频分辨率是多少呢?
没错,就是1200*1200
,那么再回到我们上面的清晰度配置项,如果我的清晰度选择了50%
,那么其实最终的产物文件分辨率应该是600*600
。所以在录制默认文件完成之后,还需要根据清晰度再去做一遍处理。
rate
就是选择的清晰度physicalWidth
、physicalHeight
分别对应屏幕的物理宽度、物理高度- 原始的分辨率应该就是
physicalWidth * scaleFactor
,这个时候乘以选择的清晰度就是设置的分辨率(physicalWidth * scaleFactor * rate)
- 算一下裁剪的区域占原始区域多少,再乘一下这个比例,就是最终指定区域的视频分辨率。(注意
cropRect.width
我们已经乘过DPI
了)
let command let rate = Number(definition) const mainScreen = screen.getPrimaryDisplay() const { scaleFactor } = mainScreen const { width: physicalWidth, height: physicalHeight } = mainScreen.size let scale = [physicalWidth * scaleFactor * rate, physicalHeight * scaleFactor * rate] if (!isEmpty(cropRect)) { const cropXRate = cropRect.width / (physicalWidth * scaleFactor) const cropYRate = cropRect.height / (physicalHeight * scaleFactor) scale = [scale[0] * cropXRate, scale[1] * cropYRate] } scale = `${Math.round(scale[0])}:${Math.round(scale[1])}` command = `${ffmpegPath} -i ${fileName} -vf "scale=${scale}" -c:a copy ${output}`
最后
以上就是本文介绍的指定区域录屏功能,如果你有一些不同的想法,欢迎评论区交流。如果你觉得有所收获的话,点点关注点点赞吧~