Files
electron/docs/api/desktop-capturer.md
T
Shelley Vohr 37d0d2b4ce feat: allow restoring a desktopCapturer source across launches on Wayland (#54550)
* 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.
2026-10-19 19:46:12 +02:00

12 KiB

desktopCapturer

Access information about media sources that can be used to capture audio and video from the desktop using the navigator.mediaDevices.getUserMedia API.

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.getDisplayMedia does not permit the use of deviceId for selection of a source - see specification.

Methods

The desktopCapturer module has the following methods:

desktopCapturer.getSources(options)

  • options Object
    • types string[] - An array of strings that lists the types of desktop sources to be captured, available types can be screen and window.
    • thumbnailSize Size (optional) - The size that the media source thumbnail should be scaled to. Default is 150 x 150. 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.
    • fetchWindowIcons boolean (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.
    • persistMode string (optional) Linux Experimental - Can be transient or persistent. 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. persistent lasts across launches until the user revokes it; store the returned source's restoreToken to use it on a later launch. See Restoring a source on Wayland.
    • restoreToken string (optional) Linux Experimental - A restoreToken from an earlier call; persistMode must 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. Pass persistMode: 'persistent' with a persistent token every time: restoring it as transient ends 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 NSAudioCaptureUsageDescription Info.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

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, desktopCapturer still produces an audio track, but it is created in the ended state 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.