Compare commits

..

6 Commits

Author SHA1 Message Date
George Xu
44c8db0655 test: cover window and browser values of preferredDisplaySurface
The request-object test only asserted the 'monitor' case, leaving
'window' and 'browser' unexercised. Since PreferredDisplaySurfaceToString
has a default branch that coerces unknown enum values to 'none', a typo
swapping the 'window' or 'browser' arms would ship silently. Parameterize
the existing test over all three non-default values.

Notes: none
2026-05-01 12:44:28 -07:00
George Xu
8165fd0571 docs: fix setDisplayMediaRequestHandler migration examples
Address review feedback on the desktopCapturer deprecation docs:

- Fix broken anchor link in the deprecation banner (was pointing at
  #sessetdisplaymediarequesthandleropts, now correctly points at
  #sessetdisplaymediarequesthandlerhandler-opts).
- Rewrite the "After (recommended)" migration example to gate
  useSystemPicker on desktopCapturer.isDisplayMediaSystemPickerAvailable()
  so the snippet works on Windows, Linux, and macOS < 15 (the previous
  version called callback({}) unconditionally and rejected
  getDisplayMedia on every non-macOS-15 platform).
- Document desktopCapturer.isDisplayMediaSystemPickerAvailable(), which
  has been a runtime export since #43581 but was never added to the
  reference docs.
- Update the top-level quickstart comment and prose to match what the
  example actually does (tab self-capture, not "first screen found").
- Mirror the platform-aware pattern in session.md.
- Fill in the real PR URL (#51235) in the getSources deprecated YAML
  block, drop the unused navigator.mediaDevices.getUserMedia link
  definition, and normalize migration-guide list markers to asterisks
  so lint:docs passes after the anchor fix exposes the pre-existing
  lint failures.

Notes: none
2026-05-01 12:44:28 -07:00
George Xu
4f84efa1ea deprecate: mark desktopCapturer.getSources as deprecated
Deprecate desktopCapturer.getSources() in favor of
session.setDisplayMediaRequestHandler(), which aligns with the
web-standard navigator.mediaDevices.getDisplayMedia() API.

Changes:
- Add deprecation warning (warnOnce) to getSources()
- Mark getSources() as deprecated in docs with migration guide
- Update session.md examples to not use desktopCapturer.getSources
- Add breaking-changes.md entry for 43.0
- Add tests for preferredDisplaySurface in media handler

Notes: Deprecated `desktopCapturer.getSources()` in favor of `session.setDisplayMediaRequestHandler()`.
2026-05-01 12:44:28 -07:00
George Xu
23c8577da9 fix: clean up native system picker flow in setDisplayMediaRequestHandler
Replace the frozen-object-with-getter pattern for generating native
picker source IDs with a simpler createNativePickerSource() function.
Each call produces a unique DesktopMediaID so Chromium treats concurrent
streams as distinct. Add clear comments explaining the magic
kMacOsNativePickerId (-4) constant and why unique window_ids are needed.

Notes: none
2026-05-01 12:42:28 -07:00
George Xu
2c3a3db2e0 feat: pass preferredDisplaySurface through to display media handler
The setDisplayMediaRequestHandler now exposes the renderer's preferred
display surface type (monitor, window, browser, or none) from the
getDisplayMedia() constraints. This lets handler code make smarter
decisions about which source to offer without requiring the deprecated
desktopCapturer.getSources() call.

Notes: Added `preferredDisplaySurface` property to the request object in `session.setDisplayMediaRequestHandler`.
2026-05-01 12:42:28 -07:00
Charles Kerr
f8d041246c fix: do not pass a DesktopMediaList* to DesktopCapturer::OnListReady() (#51399)
refactor: do not pass a DesktopMediaList* to DesktopCapturer::OnListReady()

The list pointer was being used as a proxy for its type, so just pass
the type instead. This solves a lifecycle issue occurring in CI where
the callack can outlive the DesktopMediaList.

Sample error log:

[48471:0428/193441.269750:FATAL:base/allocator/partition_alloc_support.cc:798] Detected dangling raw_ptr in unretained with id=0x0000013c02e14378:
 Task trace:
 0   Electron Framework  0x000000012283a0ba electron::api::DesktopCapturer::ListObserver::MaybeNotifyReady() + 170
 1   Electron Framework  0x0000000133246dc5 NativeDesktopMediaList::Worker::OnRecurrentCaptureResult(webrtc::DesktopCapturer::Result, std::__Cr::unique_ptr<webrtc::DesktopFrame, std::__Cr::default_delete<webrtc::DesktopFrame>>, long) + 357
 2   Electron Framework  0x000000013328dbcf (anonymous namespace)::ScreenshotManagerCapturer::OnRecurrentCaptureTimer() + 1343
 Stack trace:
 0   Electron Framework  0x000000012ade42f2 base::debug::CollectStackTrace(base::span<void const*, 18446744073709551615ul, void const**>) + 18
 1   Electron Framework  0x000000012add00e1 base::debug::StackTrace::StackTrace(unsigned long) + 225
 2   Electron Framework  0x000000012ade978a base::allocator::UnretainedDanglingRawPtrDetectedCrash(unsigned long) + 90
 3   Electron Framework  0x000000012ae437f7 base::internal::RawPtrBackupRefImpl<true>::ReportIfDanglingInternal(unsigned long) + 391
2026-05-01 11:14:48 -05:00
10 changed files with 226 additions and 52 deletions

View File

@@ -39,8 +39,8 @@ body:
- type: input
attributes:
label: Operating System Version
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a and include whether you use Wayland or X11.
placeholder: "e.g. Windows 11 25H2, macOS Tahoe 26.4.1, or Ubuntu 26.04 (Wayland)"
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations:
required: true
- type: dropdown

View File

@@ -1,30 +1,31 @@
# desktopCapturer
> Access information about media sources that can be used to capture audio and
> video from the desktop using the [`navigator.mediaDevices.getUserMedia`][] API.
> video from the desktop using the [`navigator.mediaDevices.getDisplayMedia`][] API.
Process: [Main](../glossary.md#main-process)
The following example shows how to capture video from a desktop window whose
title is `Electron`:
> [!WARNING]
> The `desktopCapturer` module is deprecated. Use
> [`session.setDisplayMediaRequestHandler`](session.md#sessetdisplaymediarequesthandlerhandler-opts)
> instead, which aligns with the web-standard
> [`navigator.mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia)
> API. See the [migration guide](#migrating-from-getsources-to-setdisplaymediarequesthandler) below.
The following example shows how to respond to a `getDisplayMedia` request by
letting the requesting tab capture itself:
```js
// main.js
const { app, BrowserWindow, desktopCapturer, session } = require('electron')
const { app, BrowserWindow, 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' })
})
// 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 })
// Allow the requesting tab to capture itself.
callback({ video: request.frame })
})
mainWindow.loadFile('index.html')
})
@@ -78,7 +79,7 @@ See [`navigator.mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-
The `desktopCapturer` module has the following methods:
### `desktopCapturer.getSources(options)`
### `desktopCapturer.getSources(options)` _Deprecated_
<!--
```YAML history
@@ -88,6 +89,8 @@ changes:
- pr-url: https://github.com/electron/electron/pull/16427
description: "This method now returns a Promise instead of using a callback function."
breaking-changes-header: api-changed-callback-based-versions-of-promisified-apis
deprecated:
- pr-url: https://github.com/electron/electron/pull/51235
```
-->
@@ -104,12 +107,27 @@ changes:
Returns `Promise<DesktopCapturerSource[]>` - Resolves with an array of [`DesktopCapturerSource`](structures/desktop-capturer-source.md) objects, each `DesktopCapturerSource` represents a screen or an individual window that can be captured.
**Deprecated:** Use [`session.setDisplayMediaRequestHandler`](session.md#sessetdisplaymediarequesthandlerhandler-opts) instead.
> [!NOTE]
<!-- markdownlint-disable-next-line MD032 -->
> * Capturing audio requires `NSAudioCaptureUsageDescription` Info.plist key on macOS 14.2 Sonoma and higher - [read more](#macos-versions-142-or-higher).
> * Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher, which can detected by [`systemPreferences.getMediaAccessStatus`][].
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
### `desktopCapturer.isDisplayMediaSystemPickerAvailable()` _macOS_ _Experimental_
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/43581
```
-->
Returns `boolean` - Whether the native macOS [`SCContentSharingPicker`](https://developer.apple.com/documentation/screencapturekit/sccontentsharingpicker) is available. Returns `true` on macOS 15 and later, `false` on earlier macOS versions and all non-macOS platforms.
Use this to decide whether to pass `useSystemPicker: true` to [`session.setDisplayMediaRequestHandler`](session.md#sessetdisplaymediarequesthandlerhandler-opts) — see the [migration guide](#migrating-from-getsources-to-setdisplaymediarequesthandler) below for an example.
[`navigator.mediaDevices.getDisplayMedia`]: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-windows-macos
## Caveats
@@ -158,3 +176,66 @@ It is possible to circumvent this limitation by capturing system audio with anot
[BlackHole](https://existential.audio/blackhole/) or [Soundflower](https://rogueamoeba.com/freebies/soundflower/)
and passing it through a virtual audio input device. This virtual device can then be queried
with `navigator.mediaDevices.getUserMedia`.
## Migrating from `getSources` to `setDisplayMediaRequestHandler`
`desktopCapturer.getSources` is deprecated. Applications should migrate to
[`session.setDisplayMediaRequestHandler`](session.md#sessetdisplaymediarequesthandlerhandler-opts),
which aligns with the web-standard [`navigator.mediaDevices.getDisplayMedia`][] API.
### Why migrate?
* **Web standards alignment** — `getDisplayMedia()` is the web-standard API for screen
capture. Using `setDisplayMediaRequestHandler` lets you integrate with it directly.
* **Platform integration** — On macOS 15+, setting `useSystemPicker: true` invokes the
native OS screen picker (SCContentSharingPicker), providing a familiar UX and
correct permissions handling.
* **Simpler architecture** — The handler receives the request when the renderer calls
`getDisplayMedia()` and you respond with the chosen source. No need to pre-enumerate
sources.
### Before (deprecated)
```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) => {
callback({ video: sources[0] })
})
})
mainWindow.loadFile('index.html')
})
```
### After (recommended)
On macOS 15+, the native OS picker (SCContentSharingPicker) is available. On
other platforms you must provide your own picker UI. A cross-platform handler
typically branches on `desktopCapturer.isDisplayMediaSystemPickerAvailable()`:
```js
const { app, BrowserWindow, desktopCapturer, session } = require('electron')
app.whenReady().then(() => {
const mainWindow = new BrowserWindow()
session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
// On macOS 15+, useSystemPicker: true causes the OS to present its own
// picker and the handler is not invoked. This branch runs everywhere else
// (Windows, Linux, and macOS < 15) — show your own picker UI here and
// call callback with the selected source, e.g. via desktopCapturer.getSources().
callback({})
}, { useSystemPicker: desktopCapturer.isDisplayMediaSystemPickerAvailable() })
mainWindow.loadFile('index.html')
})
```
`request.preferredDisplaySurface` (`'monitor'`, `'window'`, `'browser'`, or
`'none'`) indicates what kind of surface the caller asked for, and can be used
to drive the filtering in your custom picker UI.

View File

@@ -1031,6 +1031,7 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
* `videoRequested` Boolean - true if the web content requested a video stream.
* `audioRequested` Boolean - true if the web content requested an audio stream.
* `userGesture` Boolean - Whether a user gesture was active when this request was triggered.
* `preferredDisplaySurface` string - The preferred display surface type requested by the web content. Can be 'monitor', 'window', 'browser', or 'none'. This corresponds to the [`displaySurface`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/displaySurface) constraint in `getDisplayMedia()`.
* `callback` Function
* `streams` Object
* `video` Object | [WebFrameMain](web-frame-main.md) (optional)
@@ -1057,23 +1058,19 @@ via the `navigator.mediaDevices.getDisplayMedia` API. Use the
[desktopCapturer](desktop-capturer.md) API to choose which stream(s) to grant
access to.
`useSystemPicker` allows an application to use the system picker instead of providing a specific video source from `getSources`.
`useSystemPicker` allows an application to use the native OS picker instead of providing a specific video source from the handler callback.
This option is experimental, and currently available for MacOS 15+ only. If the system picker is available and `useSystemPicker`
is set to `true`, the handler will not be invoked.
```js
const { session, desktopCapturer } = require('electron')
const { desktopCapturer, session } = require('electron')
session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
// Grant access to the first screen found.
callback({ video: sources[0] })
})
// 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 })
// Runs on Windows, Linux, and macOS < 15. On macOS 15+ (where
// useSystemPicker is effective) the OS presents its own picker and this
// handler is not invoked.
callback({ video: request.frame })
}, { useSystemPicker: desktopCapturer.isDisplayMediaSystemPickerAvailable() })
```
Passing a [WebFrameMain](web-frame-main.md) object as a video or audio stream

View File

@@ -25,6 +25,15 @@ fallback frames as well.
Apps or extensions that relied on Electron skipping those frames should narrow their
injection target, frame IDs, or match patterns.
### Deprecated: `desktopCapturer.getSources()`
The `desktopCapturer.getSources()` API has been deprecated. Applications should use
[`session.setDisplayMediaRequestHandler`](api/session.md#sessetdisplaymediarequesthandlerhandler-opts)
instead, which aligns with the web-standard `navigator.mediaDevices.getDisplayMedia()` API.
See the [migration guide](api/desktop-capturer.md#migrating-from-getsources-to-setdisplaymediarequesthandler)
for detailed instructions and examples.
### Behavior Changed: Dialog methods default to Downloads directory
The `defaultPath` option for the following methods now defaults to the user's Downloads folder (or their home directory if Downloads doesn't exist) when not explicitly provided:

View File

@@ -1,5 +1,7 @@
import { BrowserWindow } from 'electron/main';
import * as deprecate from '@electron/internal/common/deprecate';
const { createDesktopCapturer, isDisplayMediaSystemPickerAvailable } = process._linkedBinding(
'electron_browser_desktop_capturer'
);
@@ -19,7 +21,10 @@ function isValid(options: Electron.SourcesOptions) {
export { isDisplayMediaSystemPickerAvailable };
const getSourcesDeprecated = deprecate.warnOnce('desktopCapturer.getSources', 'session.setDisplayMediaRequestHandler');
export async function getSources(args: Electron.SourcesOptions) {
getSourcesDeprecated();
if (!isValid(args)) throw new Error('Invalid options');
const resizableValues = new Map();

View File

@@ -7,20 +7,21 @@ import { net } from 'electron/main';
const { fromPartition, fromPath, Session } = process._linkedBinding('electron_browser_session');
const { isDisplayMediaSystemPickerAvailable } = process._linkedBinding('electron_browser_desktop_capturer');
// Fake video window that activates the native system picker
// This is used to get around the need for a screen/window
// id in Chrome's desktopCapturer.
let fakeVideoWindowId = -1;
// See content/public/browser/desktop_media_id.h
// Generates a source that triggers the native macOS SCContentSharingPicker.
// The magic ID kMacOsNativePickerId (-4) is recognized by the Chromium
// content layer (see content/public/browser/desktop_media_id.h) and routes
// to ScreenCaptureKitDeviceMac which presents the native picker.
// Each request needs a unique window_id so Chromium treats them as distinct
// streams (it deduplicates by DesktopMediaID).
let nextNativePickerWindowId = -1;
const kMacOsNativePickerId = -4;
const systemPickerVideoSource = Object.create(null);
Object.defineProperty(systemPickerVideoSource, 'id', {
get() {
return `window:${kMacOsNativePickerId}:${fakeVideoWindowId--}`;
}
});
systemPickerVideoSource.name = '';
Object.freeze(systemPickerVideoSource);
function createNativePickerSource () {
return {
id: `window:${kMacOsNativePickerId}:${nextNativePickerWindowId--}`,
name: ''
};
}
Session.prototype._init = function () {
addIpcDispatchListeners(this);
@@ -50,7 +51,9 @@ Session.prototype.setDisplayMediaRequestHandler = function (handler, opts) {
this._setDisplayMediaRequestHandler(async (req, callback) => {
if (opts && opts.useSystemPicker && isDisplayMediaSystemPickerAvailable()) {
return callback({ video: systemPickerVideoSource });
// On macOS 15+, use the native SCContentSharingPicker. This bypasses
// the JS handler and lets the OS present its own picker UI.
return callback({ video: createNativePickerSource() });
}
return handler(req, callback);

View File

@@ -240,7 +240,10 @@ class DesktopCapturer::ListObserver : public DesktopMediaListObserver {
ListObserver(DesktopCapturer* capturer,
DesktopMediaList* list,
bool need_thumbnails)
: capturer_{capturer}, list_{list}, need_thumbnails_{need_thumbnails} {}
: capturer_{capturer},
list_{list},
list_type_{list->GetMediaListType()},
need_thumbnails_{need_thumbnails} {}
~ListObserver() override = default;
[[nodiscard]] bool IsReady() const {
@@ -267,7 +270,7 @@ class DesktopCapturer::ListObserver : public DesktopMediaListObserver {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&DesktopCapturer::OnListReady,
capturer_->weak_ptr_factory_.GetWeakPtr(), list_.get()));
capturer_->weak_ptr_factory_.GetWeakPtr(), list_type_));
}
// DesktopMediaListObserver:
@@ -294,6 +297,7 @@ class DesktopCapturer::ListObserver : public DesktopMediaListObserver {
raw_ptr<DesktopCapturer> capturer_;
raw_ptr<DesktopMediaList> list_;
DesktopMediaList::Type list_type_;
bool need_thumbnails_ = false;
bool has_sources_ = false;
bool notified_ = false;
@@ -410,16 +414,21 @@ void DesktopCapturer::StartHandling(bool capture_window,
weak_ptr_factory_.GetWeakPtr()));
}
void DesktopCapturer::OnListReady(DesktopMediaList* list) {
void DesktopCapturer::OnListReady(const DesktopMediaList::Type type) {
if (finished_)
return;
if (list == window_capturer_.get()) {
FinalizeList(window_observer_, window_capturer_);
} else if (list == screen_capturer_.get()) {
FinalizeList(screen_observer_, screen_capturer_);
} else {
NOTREACHED();
switch (type) {
case DesktopMediaList::Type::kWindow:
if (window_capturer_)
FinalizeList(window_observer_, window_capturer_);
break;
case DesktopMediaList::Type::kScreen:
if (screen_capturer_)
FinalizeList(screen_observer_, screen_capturer_);
break;
default:
NOTREACHED();
}
}

View File

@@ -63,7 +63,7 @@ class DesktopCapturer final
void FinalizeList(std::unique_ptr<ListObserver>& observer,
std::unique_ptr<DesktopMediaList>& list);
void OnListReady(DesktopMediaList* list);
void OnListReady(DesktopMediaList::Type type);
void OnReadyTimeout();
void CollectSourcesFrom(DesktopMediaList* list);
void HandleFailure();

View File

@@ -4,6 +4,8 @@
#include "shell/common/gin_converters/media_converter.h"
#include <string_view>
#include "content/public/browser/media_stream_request.h"
#include "content/public/browser/render_frame_host.h"
#include "gin/data_object_builder.h"
@@ -11,6 +13,25 @@
#include "shell/common/gin_converters/gurl_converter.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
namespace {
std::string_view PreferredDisplaySurfaceToString(
blink::mojom::PreferredDisplaySurface surface) {
switch (surface) {
case blink::mojom::PreferredDisplaySurface::MONITOR:
return "monitor";
case blink::mojom::PreferredDisplaySurface::WINDOW:
return "window";
case blink::mojom::PreferredDisplaySurface::BROWSER:
return "browser";
case blink::mojom::PreferredDisplaySurface::NO_PREFERENCE:
default:
return "none";
}
}
} // namespace
namespace gin {
v8::Local<v8::Value> Converter<content::MediaStreamRequest>::ToV8(
@@ -26,6 +47,9 @@ v8::Local<v8::Value> Converter<content::MediaStreamRequest>::ToV8(
request.video_type != blink::mojom::MediaStreamType::NO_SERVICE)
.Set("audioRequested",
request.audio_type != blink::mojom::MediaStreamType::NO_SERVICE)
.Set("preferredDisplaySurface",
PreferredDisplaySurfaceToString(
request.preferred_display_surface))
.Build();
}

View File

@@ -62,6 +62,52 @@ describe('setDisplayMediaRequestHandler', () => {
expect(ok).to.be.true(message);
});
for (const surface of ['monitor', 'window', 'browser'] as const) {
it(`includes preferredDisplaySurface='${surface}' in the request object`, async () => {
const ses = session.fromPartition('' + Math.random());
let mediaRequest: any = null;
ses.setDisplayMediaRequestHandler((request, callback) => {
mediaRequest = request;
callback({ video: request.frame });
});
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
await w.loadURL(serverUrl);
const { ok, message } = await w.webContents.executeJavaScript(
`
navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: '${surface}' },
audio: false,
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
`,
true
);
expect(ok).to.be.true(message);
expect(mediaRequest.preferredDisplaySurface).to.equal(surface);
});
}
it('defaults preferredDisplaySurface to none when not specified', async () => {
const ses = session.fromPartition('' + Math.random());
let mediaRequest: any = null;
ses.setDisplayMediaRequestHandler((request, callback) => {
mediaRequest = request;
callback({ video: request.frame });
});
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
await w.loadURL(serverUrl);
const { ok, message } = await w.webContents.executeJavaScript(
`
navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
}).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
`,
true
);
expect(ok).to.be.true(message);
expect(mediaRequest.preferredDisplaySurface).to.equal('none');
});
it('does not crash when using a bogus ID', async () => {
const ses = session.fromPartition('' + Math.random());
let requestHandlerCalled = false;