🖥️Electron实现录屏软件(二)——指定区域录制

前言

在上一期我们使用Electron实现了一个自己的录屏软件,评论区有小伙伴说如何支持区域录制。诚然区域录制也是一个十分常见的需求,所以今天我们就来实现一下录屏软件的区域录制。

上一期的文章链接在这,感兴趣的朋友们可以前往观看:🔥Electron打造你自己的录屏软件🔥

前置优化

在开始区域录制之前,先优化一下之前的一些实现方式,以及重新定义一些概念。方便我们更好地开始实现后面的逻辑。

重新定义清晰度

在上一期中我们定义了几种清晰度标准:

  • 超清:3840x2160分辨率
  • 高清:1280x720分辨率
  • 标清:720x480分辨率

然后通过ffmpeg指定分辨率去录制不同质量的视频,但其实这样的实现是有一些问题的。试想一下你的录制硬件设备最高的分辨率都没达到超清的分辨率,那么设置超清的分辨率也是没有意义的。

所以录制的时候我会不指定分辨率,这样ffmpeg就会采用最高分辨率来录制,录制完成之后再把视频转成指定的分辨率。这里的分辨率我也重新定义了一下,现在是以百分比来设置。

js 复制代码
const DEFINITION_LIST = [
  { label: '100%', value: '1' },
  { label: '75%', value: '0.75' },
  { label: '50%', value: '0.5' },
  { label: '25%', value: '0.25' }
]

录制命令就改成了下面的样子

js 复制代码
const ffmpegCommand = `${ffmpegPath} -f avfoundation -r ${frameRate} -i "1" -c:v libx264 -preset ultrafast ${fileName}`

视频格式转换

录制完成之后,再调用ffmpeg的视频格式转换功能,这个时候顺便把分辨率一起设置一下。录制完成后对应的子进程就会退出,此时监听子进程的退出事件,调用转码方法。

js 复制代码
  ffmpegProcess.on('exit', (code, signal) => {
    console.log(`Recording process exited with code ${code} and signal ${signal}`)
    afterRecord()
  })

下面是afterRecord的部分实现,解释一下实现流程:

  • fileName是录制的默认mp4文件,output是我们要输出的视频文件
  • 根据选择导出的视频格式,将mp4文件转成对应的格式
  • 启动一个子进程,执行ffmpeg命令处理视频
  • 视频处理完成后删除默认mp4文件,并打开保存视频的文件夹
js 复制代码
    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
      }
    })

区域窗口

下面来实现区域的录制,首先我们要实现一个能框住某个区域的窗口。这样我们在录制的时候,视频的录制范围就是区域的范围。

我在托盘菜单这里加了一个选取区域的菜单,点击这个菜单的时候会打开一个新窗口,这个新窗口是可以移动并且调整大小的。后续的屏幕录制就会以这个窗口的大小位置为基准录制。

实现逻辑并不复杂,具体如下:

  • 创建一个新窗口,framefalse表示无边框,transparenttrue表示透明窗口
  • loadURL加载一个空的html页面
  • dragWindow方法实现整体窗口可拖动,具体在第一期实现过,这里就不再赘述
  • executeJavaScript给新窗口注入一些样式
js 复制代码
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操作的像素都是物理像素,而我们的录制区域获取到的值都是逻辑像素。比如说我们通过以下的代码获取录制区域的位置大小信息:

js 复制代码
const size = areaWindow.getSize()
const position = areaWindow.getPosition()
const [width, height] = size
const [left, top] = position

这里的高度、宽度、距离等等都是逻辑像素。

我们是使用ffmpeg的裁剪来录制指定区域,比如说下面的命令

lua 复制代码
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。所以我们获取录制区域信息的时候,需要这样计算

js 复制代码
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进行区域录制了

js 复制代码
  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*600DPI2,那么录制出来的视频分辨率是多少呢?

没错,就是1200*1200,那么再回到我们上面的清晰度 配置项,如果我的清晰度选择了50%,那么其实最终的产物文件分辨率应该是600*600。所以在录制默认文件完成之后,还需要根据清晰度再去做一遍处理。

  • rate就是选择的清晰度
  • physicalWidthphysicalHeight分别对应屏幕的物理宽度、物理高度
  • 原始的分辨率应该就是physicalWidth * scaleFactor,这个时候乘以选择的清晰度就是设置的分辨率(physicalWidth * scaleFactor * rate)
  • 算一下裁剪的区域占原始区域多少,再乘一下这个比例,就是最终指定区域的视频分辨率。(注意cropRect.width我们已经乘过DPI了)
js 复制代码
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}`

最后

以上就是本文介绍的指定区域录屏功能,如果你有一些不同的想法,欢迎评论区交流。如果你觉得有所收获的话,点点关注点点赞吧~

文章推荐

相关推荐
OpenTiny社区4 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠33 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞37 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js