Electron 截图踩坑和优化集合

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

截屏效果图: html

截屏效果图

项目修改后的完整代码依然是以前的地址: github.com/chrisbing/e… 欢迎你们关注git

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


1. 截图一瞬间卡顿问题

先放上一版截图代码web

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秒以上,并且鼠标还会出现等待的样式,这个体验是至关差了chrome

因此就须要寻求替代方案了,参考 github.com/electron/el…github.com/electron/el… 这两个 Issue,替代方案有两种,第一种用第三方原生的一些截屏程序,第二种是利用getUserMediacanvas

我选了第二种方法,主要是以为简单吧。第一种方法你们能够尝试一下,也欢迎反馈结果。windows

下面附上修改后的代码app

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。electron

修改后截屏不会出现整个程序挂起的状况,时间也缩小到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)

复制代码

通过上面的优化后,这个截图工具已经能够达到产品级了。固然还有一些不足的地方,好比跨屏截图,涂鸦,各类各样的体验细节吧,后面有时间优化完,再来和你们分享!!!

相关文章
相关标签/搜索