This is a small post about how to use electron.desktopCapturer
to capture a screenshot
in the new realities of main/renderer process separation.
What's the trick
desktopCapturer
is intended for building custom capture source picker interfaces,
which includes support for fetching thumbnails of screens and/or windows.
And those thumbnails are just screenshots that are scaled to desired size if necessary!
How it is
These are snippets for Electron Fiddle as that's by far the easiest way to do small tests on Electron.
Main process JS
3/4 of this code is Electron boilerplate so I'll highlight the important part:
const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron') const path = require('node:path') function createWindow () { const mainWindow = new BrowserWindow({ width: 1280, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadFile('index.html') mainWindow.webContents.openDevTools() } app.whenReady().then(() => { // ↓ ↓ ↓ ipcMain.handle('captureScreenshot', async (e, width, height) => { const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width, height } }) return sources[0].thumbnail.toDataURL() }) // ↑ ↑ ↑ createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() })
Now that use of desktopCapturer
in the renderer process has been outlawed,
you have to put it in the main process instead and expose it through IPC.
See "Notes" on picking through displays.
Preload JS
Here you might expose your IPC function to the renderer without exposing IPC to the renderer.
const { contextBridge, ipcRenderer } = require('electron/renderer') contextBridge.exposeInMainWorld('nativeBits', { captureScreenshot: (width, height) => ipcRenderer.invoke('captureScreenshot', width, height) })
If you do expose IPC to the renderer process, you could skip this and invoke IPC in there directly.
Renderer JS
And with all that done, you can finally call the exposed function and use the returned Data URL for your image element.
document.getElementById('take-screenshot').addEventListener('click', async () => { const dataURL = await nativeBits.captureScreenshot(640, 360) document.getElementById('screenshot').src = dataURL })
HTML
Most of this is also boilerplate, however:
if you are defining a Content Security Policy,
you will need to add data:
to the img-src
policy.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; img-src 'self' data:"> <title>Screenshots, huh?</title> </head> <body> <input id="take-screenshot" type="button" value="Take screenshot" /> <br/><img id="screenshot" /> <script src="./renderer.js"></script> </body> </html>
How it was
Prior to 2022 / Electron 17[?] you could just
const { desktopCapturer } = require('electron') document.getElementById('take-screenshot').addEventListener('click', async () => { const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: 640, height: 360 } }) document.getElementById('screenshot').src = sources[0].thumbnail.toDataURL() })
This is about half the line count of the IPC version, but the IPC version is undeniably more secure as we are no longer exposing other native APIs to the renderer.
Unfortunately, if your application is offline-only, all of this is little help against the last remaining threat: the user themselves.
Notes and caveats
You can combine this with the screen
API to pick the display that you'd like
to capture and/or extract size information out of it.
For example, the following would capture the user's smallest display at full resolution:
ipcMain.handle('captureScreenshot', async (e) => { const displays = screen.getAllDisplays() const getArea = (display) => display.size.width * display.size.height displays.sort((a, b) => getArea(a) - getArea(b)) const display = displays[0] const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: display.size }) const source = sources.filter(s => s.display_id == "" + display.id)[0] return source.thumbnail.toDataURL() })
Here you may also note a certain caveat with this whole approach: even though we only need a screenshot of one display, we can only fetch thumbnails for all of them. The more displays the user has, the slower the function gets!
In other words, if you intend to do this often, you should consider using the actual Screen Capture API.