Electron 截圖踩坑和優化集合
上一篇文章《ofollow,noindex">從零開始用 electron 手擼一個截圖工具 》釋出之後發現閱讀的朋友還不少,不過工具真正使用的時候就發現了問題,所以為了讓我們的截圖工具更好用,就又做了很多優化,當然了也遇到了很多坑。
專案修改後的完整程式碼依然是之前的地址:github.com/chrisbing/e… 歡迎大家關注
接下來就列舉一下解決的問題和具體做法
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秒以上,而且滑鼠還會出現等待的樣式,這個體驗是相當差了
所以就需要尋求替代方案了,參考github.com/electron/el…
和github.com/electron/el…
這兩個 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) 複製程式碼
經過上面的優化後,這個截圖工具已經可以達到產品級了。當然還有一些不足的地方,比如跨屏截圖,塗鴉,各種各樣的體驗細節吧,後面有時間優化完,再來和大家分享!!!