* chore: patch webrtc to keep the ScreenCast persist mode with the restore token Carries https://webrtc-review.googlesource.com/c/src/+/505860 until it rolls. RestoreTokenManager stores the portal persist mode next to each restore token and BaseCapturerPipeWire::Start() requests that mode when restoring, so a persistent token stays persistent. Existing callers default to transient and behave as before. * feat: allow restoring a desktopCapturer source across launches on Wayland On Wayland desktopCapturer.getSources() shows the xdg-desktop-portal picker on every call, so a screen-capture app has to ask the user again on every launch even though ScreenCast v4 can persist the grant. Add `persistMode` ('transient' | 'persistent') and `restoreToken` options to getSources(), a `restoreToken` property on the returned sources, and desktopCapturer.getRestoreToken(sourceId) to read the token again after it rotates when a stream starts. With a stored persistent token the picker is skipped; a revoked or stale token falls back to the picker. Electron does not store tokens; the docs show how an app can. Other platforms ignore the options and return an empty restoreToken. * docs: say when persistMode is needed with a token and add API history * chore: update the carried webrtc patch to the latest upstream revision The persist mode enum moved to modules/portal/screencast_persist_mode.h upstream, so use it from there instead of including screencast_portal.h. * docs: say how a restored window is matched and handle a cancelled picker GNOME and KDE save a window as its app id and title and restore the open window of that app with the closest title, so a token does not always bring back the window the user picked. The sample now denies the request when the user closes the picker. * docs: format the restore token sample * fix: forward restore tokens from xdg-desktop-portal 1.22.1 Only tokens that parse as a UUID were passed to the portal. Since 1.22.1 it issues 22 character base64 tokens, and accepts those and UUIDs, so every token from a new portal was dropped and the picker came back. Forward what the portal itself accepts. * chore: drop the webrtc patch that has rolled in https://webrtc-review.googlesource.com/c/src/+/505860 is part of the WebRTC that Chromium 157.0.8085.0 pins. * fix: only look up and seed restore tokens for picker sessions Also covers an empty source list and a second request in the docs sample, and adds a round-trip spec. * fix: pass restore tokens through unchecked and require persistMode with them The portal's token grammar changed once already (1.22.1) and copying it here silently dropped valid tokens, so the token now goes to the portal as given: a value it did not issue makes getSources() reject, which the docs and the sample handle. restoreToken without persistMode restored a persistent grant as transient and ended it, so that combination is now an invalid option.
12 KiB
desktopCapturer
Access information about media sources that can be used to capture audio and video from the desktop using the
navigator.mediaDevices.getUserMediaAPI.
Process: Main
The following example shows how to capture video from a desktop window whose
title is Electron:
// main.js
const { app, BrowserWindow, desktopCapturer, session } = require('electron')
app.whenReady().then(() => {
const mainWindow = new BrowserWindow()
session.defaultSession.setDisplayMediaRequestHandler(
(request, callback) => {
desktopCapturer.getSources({ types: ['screen'] }).then(
(sources) => {
// Grant access to the first screen found.
callback({ video: sources[0], audio: 'loopback' })
},
() => {
// Deny the request if no sources could be retrieved, for example
// when the user cancels the PipeWire picker on Linux.
callback(null)
}
)
// If true, use the system picker if available.
// Note: this is currently experimental. If the system picker
// is available, it will be used and the media request handler
// will not be invoked.
},
{ useSystemPicker: true }
)
mainWindow.loadFile('index.html')
})
// renderer.js
const startButton = document.getElementById('startButton')
const stopButton = document.getElementById('stopButton')
const video = document.querySelector('video')
startButton.addEventListener('click', () => {
navigator.mediaDevices
.getDisplayMedia({
audio: true,
video: {
width: 320,
height: 240,
frameRate: 30
}
})
.then((stream) => {
video.srcObject = stream
video.onloadedmetadata = (e) => video.play()
})
.catch((e) => console.log(e))
})
stopButton.addEventListener('click', () => {
video.pause()
})
<!-- index.html -->
<html>
<meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline'" />
<body>
<button id="startButton" class="button">Start</button>
<button id="stopButton" class="button">Stop</button>
<video width="320" height="240" autoplay></video>
<script src="renderer.js"></script>
</body>
</html>
See navigator.mediaDevices.getDisplayMedia for more information.
Note
navigator.mediaDevices.getDisplayMediadoes not permit the use ofdeviceIdfor selection of a source - see specification.
Methods
The desktopCapturer module has the following methods:
desktopCapturer.getSources(options)
optionsObjecttypesstring[] - An array of strings that lists the types of desktop sources to be captured, available types can bescreenandwindow.thumbnailSizeSize (optional) - The size that the media source thumbnail should be scaled to. Default is150x150. Set width or height to 0 when you do not need the thumbnails. This will save the processing time required for capturing the content of each window and screen.fetchWindowIconsboolean (optional) - Set to true to enable fetching window icons. The default value is false. When false the appIcon property of the sources return null. Same if a source has the type screen.persistModestring (optional) Linux Experimental - Can betransientorpersistent. How long the source the user picks in the system picker can be reopened without showing the picker again.transient, the default, lasts at most until the app exits.persistentlasts across launches until the user revokes it; store the returned source'srestoreTokento use it on a later launch. See Restoring a source on Wayland.restoreTokenstring (optional) Linux Experimental - ArestoreTokenfrom an earlier call;persistModemust be passed with it. If the system still honors the token, the promise resolves with that source and no picker is shown. A token that was revoked or names a source that no longer exists is ignored and the picker is shown as if no token was passed; a value that is not a token the system issued makes the promise reject. PasspersistMode: 'persistent'with a persistent token every time: restoring it astransientends the persistent grant.
Returns Promise<DesktopCapturerSource[]> - Resolves with an array of DesktopCapturerSource objects, each DesktopCapturerSource represents a screen or an individual window that can be captured.
Note
- Capturing audio requires
NSAudioCaptureUsageDescriptionInfo.plist key on macOS 14.2 Sonoma and higher - read more.- Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher, which can detected by
systemPreferences.getMediaAccessStatus.
desktopCapturer.getRestoreToken(sourceId) Linux Experimental
sourceIdstring - Theidof aDesktopCapturerSource.
Returns string - The current restore token for the source, or an empty string if it has none.
The system may replace the token each time the source is opened, which happens once in
desktopCapturer.getSources and again when a stream is started from the source, and then only
the newest token works on the next launch. Read it with this method once the stream is running,
or before the app quits, and store that value rather than the one getSources returned. See
Restoring a source on Wayland.
Caveats
Linux
desktopCapturer.getSources(options) only returns a single source on Linux when using Pipewire.
PipeWire supports a single capture for both screens and windows. If you request the window and screen type, the selected source will be returned as a window capture.
Restoring a source on Wayland
On Wayland the source is chosen in the system's own picker, which desktopCapturer.getSources
shows on every call. To let the user pick once and capture that choice again on later launches,
pass persistMode: 'persistent', store the source's restore token, and pass it back as
restoreToken next time. Electron does not store tokens itself. A token names a screen or
window the user already agreed to share, so keep it somewhere private to the app, for example
encrypted with safeStorage. If the user has revoked the grant or the screen or
window no longer exists, the token is ignored and the picker is shown again. If the stored value is
not a token the system issued, getSources rejects, so drop it and ask again without one.
How a token is matched to a source is up to the desktop environment. GNOME and KDE remember a window by its app and title, and restore the open window of that app whose title is closest, so a token can resolve to a different window of the same app. If no title is close enough the picker is shown.
With a valid token nothing asks the user before capture starts. Show the user what is being
shared, and let them choose something else by calling desktopCapturer.getSources again without
restoreToken.
This needs xdg-desktop-portal with version 4 or later of the ScreenCast interface. In an X11
session and on other platforms the options are ignored and restoreToken is always empty. In a
Wayland session they also apply when the app runs under XWayland.
const { app, desktopCapturer, safeStorage, session } = require('electron')
const fs = require('node:fs/promises')
const path = require('node:path')
const tokenFile = path.join(app.getPath('userData'), 'screencast-token')
async function readToken() {
try {
const { result } = await safeStorage.decryptStringAsync(await fs.readFile(tokenFile))
return result
} catch {
return undefined
}
}
async function writeToken(token) {
if (token) await fs.writeFile(tokenFile, await safeStorage.encryptStringAsync(token))
}
app.whenReady().then(() => {
let sourceId
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
// There is no source if the user closes the picker, and the call rejects if
// the stored value is not a token the system issued; forget it either way.
const [source] = await desktopCapturer
.getSources({
types: ['screen', 'window'],
persistMode: 'persistent',
restoreToken: (sourceId && desktopCapturer.getRestoreToken(sourceId)) || (await readToken())
})
.catch(() => fs.rm(tokenFile, { force: true }).then(() => []))
if (!source) {
callback(null)
return
}
sourceId = source.id
await writeToken(source.restoreToken)
callback({ video: source })
})
app.on('will-quit', (event) => {
if (!sourceId) return
event.preventDefault()
writeToken(desktopCapturer.getRestoreToken(sourceId)).finally(() => {
sourceId = undefined
app.quit()
})
})
})
macOS versions 14.2 or higher
NSAudioCaptureUsageDescription Info.plist key must be added in order for audio to be captured by
desktopCapturer. If instead you are running Electron from another program like a terminal or IDE
then that parent program must contain the Info.plist key.
This is in order to facilitate use of Apple's CoreAudio Tap API by Chromium, which is gated behind the "System Audio Recording" privacy permission. macOS attributes that permission to the responsible process, so when running unpackaged from a terminal or IDE it is the terminal or IDE that must be granted access.
Warning
If the permission is missing or has been denied,
desktopCapturerstill produces an audio track, but it is created in theendedstate and never delivers samples. No warning or error is surfaced to JavaScript.
Since Electron 39, Chromium uses the CoreAudio Tap API by default
for system audio capture on macOS 14.2 and later. There is no automatic fallback to the older
ScreenCaptureKit-based "Screen & System Audio Recording" path if tap creation fails, and as of
Electron 45 the MacCatapLoopbackAudioForScreenShare feature flag that previously allowed opting
back into it has been removed upstream
and no longer has any effect.
macOS versions 12.7.6 or lower
navigator.mediaDevices.getUserMedia does not work on macOS versions 12.7.6 and prior for audio
capture due to a fundamental limitation whereby apps that want to access the system's audio require
a signed kernel extension.
Chromium, and by extension Electron, does not provide this. Only in macOS 13 and onwards does Apple
provide APIs to capture desktop audio without the need for a signed kernel extension.
It is possible to circumvent this limitation by capturing system audio with another macOS app like
BlackHole or Soundflower
and passing it through a virtual audio input device. This virtual device can then be queried
with navigator.mediaDevices.getUserMedia.