Electron 截图踩坑和优化集合

痞子三分冷 提交于 2019-12-02 19:27:28

本文转载于:猿2048网站➸https://www.mk2048.com/blog/blog.php?id=c1ch0cb0j

上一篇文章《从零开始用 electron 手撸一个截屏工具》发布之后发现阅读的朋友还不少,不过工具真正使用的时候就发现了问题,所以为了让我们的截图工具更好用,就又做了很多优化,当然了也遇到了很多坑。

截屏效果图:

项目修改后的完整代码依然是之前的地址: https://github.com/chrisbing/electorn-capture-screen 欢迎大家关注

接下来就列举一下解决的问题和具体做法


1. 截图一瞬间卡顿问题

先放上一版截图代码

console.time('capture') desktopCapturer.getSources({     types: ['screen'],     thumbnailSize: {         width: width * scaleFactor,         height: height * scaleFactor,     } }, (error, sources) => {     console.timeEnd('capture')     let imgSrc = sources[0].thumbnail.toDataURL()     let capture = new CaptureRenderer($canvas, $bg, imgSrc, scaleFactor) }) 

desktopCapturer.getSources 会导致整个程序挂起,挂起时间与屏幕分辨率、屏幕数量和电脑性能有关。 在自用的 Macbook Pro 外接2K 显示器的情况下截图可以卡住2秒以上,而且鼠标还会出现等待的样式,这个体验是相当差了

所以就需要寻求替代方案了,参考 https://github.com/electron/electron/issues/8246https://github.com/electron/electron/issues/2237 这两个 Issue,替代方案有两种,第一种用第三方原生的一些截屏程序,第二种是利用getUserMedia

我选了第二种方法,主要是觉得简单吧。第一种方法大家可以尝试一下,也欢迎反馈结果。

下面附上修改后的代码

    const handleStream = (stream) => {         document.body.style.cursor = oldCursor         document.body.style.opacity = '1'         // Create hidden video tag         let video = document.createElement('video')         video.style.cssText = 'position:absolute;top:-10000px;left:-10000px;'         // Event connected to stream          let loaded = false         video.onloadedmetadata = () => {             if (loaded) {                 return             }             loaded = true             // Set video ORIGINAL height (screenshot)             video.style.height = video.videoHeight + 'px' // videoHeight             video.style.width = video.videoWidth + 'px' // videoWidth              // Create canvas             let canvas = document.createElement('canvas')             canvas.width = video.videoWidth             canvas.height = video.videoHeight             let ctx = canvas.getContext('2d')             // Draw video on canvas             ctx.drawImage(video, 0, 0, canvas.width, canvas.height)              if (this.callback) {                 // Save screenshot to png - base64                 this.callback(canvas.toDataURL('image/png'))             } else {                 // console.log('Need callback!')             }              // Remove hidden video tag             video.remove()             try {                 stream.getTracks()[0].stop()             } catch (e) {                 // nothing             }         }         video.srcObject = stream         document.body.appendChild(video)     }               // mac 和 windows 获取 chromeMediaSourceId 的方式不同     if (require('os').platform() === 'win32') {         require('electron').desktopCapturer.getSources({             types: ['screen'],             thumbnailSize: { width: 1, height: 1 },         }, (e, sources) => {             let selectSource = sources.filter(source => source.display_id + '' === curScreen.id + '')[0]             navigator.getUserMedia({                 audio: false,                 video: {                     mandatory: {                         chromeMediaSource: 'desktop',                         chromeMediaSourceId: selectSource.id + '',                         minWidth: 1280,                         minHeight: 720,                         maxWidth: 8000,                         maxHeight: 8000,                     },                 },             }, handleStream, handleError)         })     } else {         navigator.getUserMedia({             audio: false,             video: {                 mandatory: {                     chromeMediaSource: 'desktop',                     chromeMediaSourceId: `screen:${curScreen.id}`,                     minWidth: 1280,                     minHeight: 720,                     maxWidth: 8000,                     maxHeight: 8000,                 },             },         }, handleStream, handleError)     } 

代码有点多,主要也是复制来的。他的原理是用 getUserMedia 来录屏,获取到视频资源,然后将视频绘制到 canvas 上,最后转换成 url。

修改后截屏不会出现整个程序挂起的情况,时间也缩小到600ms 左右,这个时间对于截图来说已经是可以接受的了。

2. 多屏幕支持

当电脑有多个显示器的情况,多屏截图就很重要了,之前只提到了一个屏幕的情况,那多屏应该怎么处理呢?

由于全屏情况,窗口只能占据一个屏幕,所以多屏截图只能用多个截屏窗口来处理了(windows 或许有办法让全屏窗口跨屏显示,待尝试)

首先创建窗口就需要先获取屏幕数量,循环创建

const captureScreen = (e, args) => {     if (captureWins.length) {         return     }     const { screen } = require('electron')      let displays = screen.getAllDisplays()          // 循环创建截屏窗口     captureWins = displays.map((display) => {         let captureWin = new BrowserWindow({             // window 使用 fullscreen,  mac 设置为 undefined, 不可为 false             fullscreen: os.platform() === 'win32' || undefined,             width: display.bounds.width,             height: display.bounds.height,             x: display.bounds.x,             y: display.bounds.y,             transparent: true,             frame: false,             movable: false,             resizable: false,             enableLargerThanScreen: true,             hasShadow: false,         })         captureWin.setAlwaysOnTop(true, 'screen-saver')         captureWin.setFullScreenable(false)          captureWin.loadFile(path.join(__dirname, 'capture.html'))          // 调试用         // captureWin.openDevTools()          // 一个窗口关闭则关闭所有窗口         captureWin.on('closed', () => {             let index = captureWins.indexOf(captureWin)             if (index !== -1) {                 captureWins.splice(index, 1)             }             captureWins.forEach(win => win.close())         })         return captureWin     })  } 

然后每个窗口截取当前屏幕的画面进行操作,获取当前屏幕可以下面的方法

// 因为窗口是全屏的, 所以可以直接用 x, y 来对比 const getCurrentScreen = () => {     let { x, y } = currentWindow.getBounds()     return screen.getAllDisplays().filter(d => d.bounds.x === x && d.bounds.y === y)[0] } 

然后根据问题1的截图代码就可以获取到当前屏幕的截图, 其中chromeMediaSourceId代表的就是屏幕的 ID

改到这里,大体上就差不多了,但是还有个小问题,因为是多个窗口,每个窗口都可以通过拖拽选区图片区域。参考 QQ 在 Mac 上的做法,当一个屏幕有选区了,另一个屏幕上禁止操作

多窗口互通的话,使用了 ipc 通讯。窗口选区后发给 main 进程,main 进程广播给其他窗口,其他窗口接收后禁止操作。

    // main 进程     ipcMain.on('capture-screen', (e, { type = 'start', screenId, url } = {}) => {         // ...         if (type === 'select') {             captureWins.forEach(win => win.webContents.send('capture-screen', { type: 'select', screenId }))         }     }) 
    // renderer 进程     ipcRenderer.on('capture-screen', (e, { type, screenId }) => {         if (type === 'select') {             if (screenId && screenId !== currentScreen.id) {                 capture.disable()             }         }     }) 

3. Mac 下截取全屏窗口

Mac 下让窗口显示在全屏窗口之上的话,需要一段神奇的代码,当然代码的写法是查搜出来的,但是具体原来还不是很清楚,貌似是一些 hack 的手段吧。

在我这我只能称之为"黑魔法"

下面一段代码放在创建截屏窗口的代码后面

         let captureWin = new BrowserWindow({             // window 使用 fullscreen,  mac 设置为 undefined, 不可为 false             fullscreen: os.platform() === 'win32' || undefined,             width: display.bounds.width,             height: display.bounds.height,             x: display.bounds.x,             y: display.bounds.y,             transparent: true,             frame: false,             movable: false,             resizable: false,             enableLargerThanScreen: true,             hasShadow: false,             show: false,         })          // 黑魔法...         app.dock.hide()         captureWin.setAlwaysOnTop(true, 'screen-saver')         captureWin.setVisibleOnAllWorkspaces(true)         captureWin.setFullScreenable(false)         captureWin.show()         app.dock.show()         captureWin.setVisibleOnAllWorkspaces(false)  

经过上面的优化后,这个截图工具已经可以达到产品级了。当然还有一些不足的地方,比如跨屏截图,涂鸦,各种各样的体验细节吧,后面有时间优化完,再来和大家分享!!!


更多专业前端知识,请上【猿2048】www.mk2048.com
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!