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.
This commit is contained in:
Shelley Vohr authored and GitHub committed 2026-10-19 19:46:12 +02:00
1 parent 360d1da1ee
commit 37d0d2b4ce
8 files changed
+286 -55

No files matched your search

+111
View File
@@ -101,6 +101,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
- pr-url: https://github.com/electron/electron/pull/54550
description: "Added the `persistMode` and `restoreToken` options."
```
-->
@@ -114,6 +116,18 @@ changes:
* `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](#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`](structures/desktop-capturer-source.md) objects, each `DesktopCapturerSource` represents a screen or an individual window that can be captured.
@@ -122,6 +136,25 @@ Returns `Promise<DesktopCapturerSource[]>` - Resolves with an array of [`Desktop
> * 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`][].
### `desktopCapturer.getRestoreToken(sourceId)` _Linux_ _Experimental_
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/54550
```
-->
* `sourceId` string - The `id` of a [`DesktopCapturerSource`](structures/desktop-capturer-source.md).
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](#restoring-a-source-on-wayland).
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-windows-macos
@@ -133,6 +166,84 @@ Returns `Promise<DesktopCapturerSource[]>` - Resolves with an array of [`Desktop
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`](safe-storage.md). 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.
```js
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
@@ -24,3 +24,7 @@
application that owns the window or null if the source has a type screen.
The size of the icon is not known in advance and depends on what
the application provides.
* `restoreToken` string _Linux_ _Experimental_ - A token that lets a later
`desktopCapturer.getSources` call reopen this source without showing the
system picker, or an empty string if the system did not issue one. See
[Restoring a source on Wayland](../desktop-capturer.md#restoring-a-source-on-wayland).
+14 -6
View File
@@ -1,6 +1,6 @@
import { BaseWindow } from 'electron/main';
const { createDesktopCapturer, isDisplayMediaSystemPickerAvailable } = process._linkedBinding(
const { createDesktopCapturer, isDisplayMediaSystemPickerAvailable, getRestoreToken } = process._linkedBinding(
'electron_browser_desktop_capturer'
);
@@ -12,12 +12,16 @@ let currentlyRunning: {
getSources: Promise<ElectronInternal.GetSourcesResult[]>;
}[] = [];
// |options.types| can't be empty and must be an array
const persistModes = new Set(['transient', 'persistent']);
function isValid(options: Electron.SourcesOptions) {
return Array.isArray(options?.types);
if (!Array.isArray(options?.types)) return false;
if (options.persistMode != null && !persistModes.has(options.persistMode)) return false;
if (options.restoreToken != null && typeof options.restoreToken !== 'string') return false;
return !options.restoreToken || options.persistMode != null;
}
export { isDisplayMediaSystemPickerAvailable };
export { isDisplayMediaSystemPickerAvailable, getRestoreToken };
export async function getSources(args: Electron.SourcesOptions) {
if (!isValid(args)) throw new Error('Invalid options');
@@ -37,12 +41,16 @@ export async function getSources(args: Electron.SourcesOptions) {
const { thumbnailSize = { width: 150, height: 150 } } = args;
const { fetchWindowIcons = false } = args;
const persistent = args.persistMode === 'persistent';
const restoreToken = args.restoreToken ?? '';
const options = {
captureWindow,
captureScreen,
thumbnailSize,
fetchWindowIcons
fetchWindowIcons,
persistent,
restoreToken
};
for (const running of currentlyRunning) {
@@ -96,7 +104,7 @@ export async function getSources(args: Electron.SourcesOptions) {
resolveGetSources(sources);
};
capturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons);
capturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons, persistent, restoreToken);
return getSources;
}
@@ -20,6 +20,7 @@
#include "chrome/browser/media/webrtc/thumbnail_capturer_mac.h"
#include "chrome/browser/media/webrtc/window_icon_util.h"
#include "content/public/browser/desktop_capture.h"
#include "content/public/browser/desktop_media_id.h"
#include "gin/object_template_builder.h"
#include "gin/persistent.h"
#include "shell/browser/javascript_environment.h"
@@ -54,6 +55,11 @@
#include "ui/base/cocoa/permissions_utils.h"
#endif
#if defined(WEBRTC_USE_PIPEWIRE)
#include "third_party/webrtc/modules/desktop_capture/linux/wayland/restore_token_manager.h"
#include "third_party/webrtc/modules/portal/screencast_persist_mode.h"
#endif
namespace {
#if BUILDFLAG(IS_LINUX)
// Private function in ui/base/x/x11_display_util.cc
@@ -151,7 +157,33 @@ base::flat_map<int32_t, uint32_t> MonitorAtomIdToDisplayId() {
}
#endif
std::unique_ptr<ThumbnailCapturer> MakeWindowCapturer() {
// A PipeWire capturer's portal session asks for `persistent` and tries
// `restore_token`; the issued token is kept under the capturer's source id.
std::unique_ptr<ThumbnailCapturer> WrapCapturer(
std::unique_ptr<webrtc::DesktopCapturer> capturer,
bool persistent,
const std::string& restore_token) {
if (!capturer)
return nullptr;
#if defined(WEBRTC_USE_PIPEWIRE)
webrtc::DesktopCapturer::SourceList sources;
if (capturer->GetDelegatedSourceListController() &&
capturer->GetSourceList(&sources) && sources.size() == 1) {
if (persistent || !restore_token.empty()) {
webrtc::RestoreTokenManager::GetInstance().AddToken(
sources[0].id, restore_token,
persistent ? webrtc::xdg_portal::ScreenCastPersistMode::kPersistent
: webrtc::xdg_portal::ScreenCastPersistMode::kTransient);
}
capturer->SelectSource(sources[0].id);
}
#endif
return std::make_unique<DesktopCapturerWrapper>(std::move(capturer));
}
std::unique_ptr<ThumbnailCapturer> MakeWindowCapturer(
bool persistent,
const std::string& restore_token) {
#if BUILDFLAG(IS_MAC)
if (ShouldUseThumbnailCapturerMac(DesktopMediaList::Type::kWindow)) {
return CreateThumbnailCapturerMac(DesktopMediaList::Type::kWindow,
@@ -159,15 +191,15 @@ std::unique_ptr<ThumbnailCapturer> MakeWindowCapturer() {
}
#endif // BUILDFLAG(IS_MAC)
std::unique_ptr<webrtc::DesktopCapturer> window_capturer =
return WrapCapturer(
content::desktop_capture::CreateWindowCapturer(
content::desktop_capture::CreateDesktopCaptureOptions());
return window_capturer ? std::make_unique<DesktopCapturerWrapper>(
std::move(window_capturer))
: nullptr;
content::desktop_capture::CreateDesktopCaptureOptions()),
persistent, restore_token);
}
std::unique_ptr<ThumbnailCapturer> MakeScreenCapturer() {
std::unique_ptr<ThumbnailCapturer> MakeScreenCapturer(
bool persistent,
const std::string& restore_token) {
#if BUILDFLAG(IS_MAC)
if (ShouldUseThumbnailCapturerMac(DesktopMediaList::Type::kScreen)) {
return CreateThumbnailCapturerMac(DesktopMediaList::Type::kScreen,
@@ -175,13 +207,11 @@ std::unique_ptr<ThumbnailCapturer> MakeScreenCapturer() {
}
#endif // BUILDFLAG(IS_MAC)
std::unique_ptr<webrtc::DesktopCapturer> screen_capturer =
return WrapCapturer(
content::desktop_capture::CreateScreenCapturer(
content::desktop_capture::CreateDesktopCaptureOptions(),
/*for_snapshot=*/false);
return screen_capturer ? std::make_unique<DesktopCapturerWrapper>(
std::move(screen_capturer))
: nullptr;
/*for_snapshot=*/false),
persistent, restore_token);
}
#if BUILDFLAG(IS_WIN)
@@ -217,6 +247,7 @@ struct Converter<electron::api::DesktopCapturer::Source> {
electron::api::NativeImage::Create(
isolate, gfx::Image(source.media_list_source.thumbnail)));
dict.Set("display_id", source.display_id);
dict.Set("restoreToken", source.restore_token);
if (source.fetch_icon) {
dict.Set(
"appIcon",
@@ -339,7 +370,9 @@ void DesktopCapturer::FinalizeList(std::unique_ptr<ListObserver>& observer,
void DesktopCapturer::StartHandling(bool capture_window,
bool capture_screen,
const gfx::Size& thumbnail_size,
bool fetch_window_icons) {
bool fetch_window_icons,
bool persistent,
const std::string& restore_token) {
fetch_window_icons_ = fetch_window_icons;
#if BUILDFLAG(IS_WIN)
if (content::desktop_capture::CreateDesktopCaptureOptions()
@@ -373,21 +406,19 @@ void DesktopCapturer::StartHandling(bool capture_window,
std::unique_ptr<ThumbnailCapturer> capturer;
bool use_generic = false;
if (capture_screen) {
auto desktop_capturer = webrtc::DesktopCapturer::CreateGenericCapturer(
content::desktop_capture::CreateDesktopCaptureOptions());
auto wrapper = desktop_capturer
? std::make_unique<DesktopCapturerWrapper>(
std::move(desktop_capturer))
: nullptr;
if (wrapper && wrapper->GetDelegatedSourceListController()) {
capturer = std::move(wrapper);
auto generic = WrapCapturer(
webrtc::DesktopCapturer::CreateGenericCapturer(
content::desktop_capture::CreateDesktopCaptureOptions()),
persistent, restore_token);
if (generic && generic->GetDelegatedSourceListController()) {
capturer = std::move(generic);
use_generic = true;
// Generic capturer handles both types as window capture.
need_screen = false;
}
}
if (!capturer)
capturer = MakeWindowCapturer();
capturer = MakeWindowCapturer(persistent, restore_token);
if (capturer) {
window_capturer_ = std::make_unique<NativeDesktopMediaList>(
@@ -403,7 +434,7 @@ void DesktopCapturer::StartHandling(bool capture_window,
}
if (need_screen) {
auto capturer = MakeScreenCapturer();
auto capturer = MakeScreenCapturer(persistent, restore_token);
if (capturer) {
screen_capturer_ = std::make_unique<NativeDesktopMediaList>(
DesktopMediaList::Type::kScreen, std::move(capturer));
@@ -467,25 +498,17 @@ void DesktopCapturer::OnReadyTimeout() {
}
void DesktopCapturer::CollectSourcesFrom(DesktopMediaList* list) {
const auto type = list->GetMediaListType();
const bool is_screen_list =
list->GetMediaListType() == DesktopMediaList::Type::kScreen;
if (type == DesktopMediaList::Type::kWindow) {
std::vector<DesktopCapturer::Source> window_sources;
window_sources.reserve(list->GetSourceCount());
for (int i = 0; i < list->GetSourceCount(); i++) {
window_sources.emplace_back(list->GetSource(i), std::string(),
fetch_window_icons_);
}
std::move(window_sources.begin(), window_sources.end(),
std::back_inserter(captured_sources_));
std::vector<DesktopCapturer::Source> sources;
sources.reserve(list->GetSourceCount());
for (int i = 0; i < list->GetSourceCount(); i++) {
sources.emplace_back(list->GetSource(i), std::string(),
!is_screen_list && fetch_window_icons_);
}
if (type == DesktopMediaList::Type::kScreen) {
std::vector<DesktopCapturer::Source> screen_sources;
screen_sources.reserve(list->GetSourceCount());
for (int i = 0; i < list->GetSourceCount(); i++) {
screen_sources.emplace_back(list->GetSource(i), std::string());
}
if (is_screen_list) {
#if BUILDFLAG(IS_WIN)
// Gather the same unique screen IDs used by the electron.screen API in
// order to provide an association between it and
@@ -494,13 +517,13 @@ void DesktopCapturer::CollectSourcesFrom(DesktopMediaList* list) {
if (using_directx_capturer_) {
std::vector<std::string> device_names;
// Crucially, this list of device names will be in the same order as
// |screen_sources|.
// |sources|.
if (!webrtc::DxgiDuplicatorController::Instance()->GetDeviceNames(
&device_names)) {
HandleFailure();
return;
}
DCHECK_EQ(device_names.size(), screen_sources.size());
DCHECK_EQ(device_names.size(), sources.size());
std::vector<HMONITOR> monitors;
EnumDisplayMonitors(nullptr, nullptr, EnumDisplayMonitorsCallback,
@@ -520,7 +543,7 @@ void DesktopCapturer::CollectSourcesFrom(DesktopMediaList* list) {
}
int device_name_index = 0;
for (auto& source : screen_sources) {
for (auto& source : sources) {
const auto& device_name = device_names[device_name_index++];
if (auto id_iter = device_name_to_id.find(device_name);
id_iter != device_name_to_id.end()) {
@@ -530,7 +553,7 @@ void DesktopCapturer::CollectSourcesFrom(DesktopMediaList* list) {
}
#elif BUILDFLAG(IS_MAC)
// On Mac, the IDs across the APIs match.
for (auto& source : screen_sources) {
for (auto& source : sources) {
source.display_id = base::NumberToString(source.media_list_source.id.id);
}
#elif BUILDFLAG(SUPPORTS_OZONE_X11)
@@ -539,16 +562,27 @@ void DesktopCapturer::CollectSourcesFrom(DesktopMediaList* list) {
// loop index when that display was found (see
// BuildDisplaysFromXRandRInfo in ui/base/x/x11_display_util.cc)
const auto monitor_atom_to_display_id = MonitorAtomIdToDisplayId();
for (auto& source : screen_sources) {
for (auto& source : sources) {
auto display_id_iter =
monitor_atom_to_display_id.find(source.media_list_source.id.id);
if (display_id_iter != monitor_atom_to_display_id.end())
source.display_id = base::NumberToString(display_id_iter->second);
}
#endif
std::move(screen_sources.begin(), screen_sources.end(),
std::back_inserter(captured_sources_));
}
#if defined(WEBRTC_USE_PIPEWIRE)
if (list->IsSourceListDelegated()) {
for (auto& source : sources) {
source.restore_token = webrtc::RestoreTokenManager::GetInstance()
.GetEntry(source.media_list_source.id.id)
.token;
}
}
#endif
std::move(sources.begin(), sources.end(),
std::back_inserter(captured_sources_));
}
void DesktopCapturer::HandleSuccess() {
@@ -601,6 +635,19 @@ bool DesktopCapturer::IsDisplayMediaSystemPickerAvailable() {
}
#endif
// static
std::string DesktopCapturer::GetRestoreToken(const std::string& source_id) {
#if defined(WEBRTC_USE_PIPEWIRE)
const content::DesktopMediaID id = content::DesktopMediaID::Parse(source_id);
if (id.id_type == content::DesktopMediaID::IdType::kNativePickerSession &&
(id.type == content::DesktopMediaID::TYPE_SCREEN ||
id.type == content::DesktopMediaID::TYPE_WINDOW)) {
return webrtc::RestoreTokenManager::GetInstance().GetEntry(id.id).token;
}
#endif
return {};
}
gin::ObjectTemplateBuilder DesktopCapturer::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<DesktopCapturer>::GetObjectTemplateBuilder(isolate)
@@ -635,6 +682,8 @@ void Initialize(v8::Local<v8::Object> exports,
dict.SetMethod<
&electron::api::DesktopCapturer::IsDisplayMediaSystemPickerAvailable>(
"isDisplayMediaSystemPickerAvailable");
dict.SetMethod<&electron::api::DesktopCapturer::GetRestoreToken>(
"getRestoreToken");
}
} // namespace
@@ -26,16 +26,24 @@ class DesktopCapturer final : public gin::Wrappable<DesktopCapturer> {
// Whether or not this source should provide an icon.
bool fetch_icon = false;
// ScreenCast portal restore token; empty when the source has none.
std::string restore_token;
};
static DesktopCapturer* Create(v8::Isolate* isolate);
static bool IsDisplayMediaSystemPickerAvailable();
[[nodiscard]] static std::string GetRestoreToken(
const std::string& source_id);
void StartHandling(bool capture_window,
bool capture_screen,
const gfx::Size& thumbnail_size,
bool fetch_window_icons);
bool fetch_window_icons,
bool persistent,
const std::string& restore_token);
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
+46 -1
View File
@@ -5,7 +5,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { once } from 'node:events';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe, ifit } from './lib/spec-helpers.ts';
import { ifdescribe, ifit, isWayland } from './lib/spec-helpers.ts';
import { closeAllWindows } from './lib/window-helpers.ts';
function getSourceTypes(): ('window' | 'screen')[] {
@@ -27,6 +27,51 @@ describe('desktopCapturer', { tags: ['serial'] }, () => {
await expect(promise).rejects.toThrow('Invalid options');
});
it('throws an error for an unknown persistMode', async () => {
const promise = desktopCapturer.getSources({ types: getSourceTypes(), persistMode: 'forever' as any });
await expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options');
});
it('throws an error for a non-string restoreToken', async () => {
const promise = desktopCapturer.getSources({ types: getSourceTypes(), restoreToken: 42 as any });
await expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options');
});
it('accepts persistMode and restoreToken and returns a restoreToken for each source', async () => {
const sources = await desktopCapturer.getSources({
types: getSourceTypes(),
thumbnailSize: { width: 0, height: 0 },
persistMode: 'transient',
restoreToken: ''
});
expect(sources).to.be.an('array').that.is.not.empty();
for (const source of sources) {
expect(source.restoreToken).to.be.a('string');
if (!isWayland) expect(source.restoreToken).to.be.empty();
}
});
it('throws when restoreToken is passed without persistMode', async () => {
const promise = desktopCapturer.getSources({ types: getSourceTypes(), restoreToken: 'token' });
await expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options');
});
it('getRestoreToken returns an empty string for a source that has no token', () => {
expect(desktopCapturer.getRestoreToken('window:0:0')).to.equal('');
expect(desktopCapturer.getRestoreToken('not-a-source-id')).to.equal('');
});
it('getRestoreToken returns the restoreToken of a returned source', async () => {
const sources = await desktopCapturer.getSources({
types: getSourceTypes(),
thumbnailSize: { width: 0, height: 0 }
});
expect(sources).to.be.an('array').that.is.not.empty();
for (const source of sources) {
expect(desktopCapturer.getRestoreToken(source.id)).to.equal(source.restoreToken);
}
});
it('does not throw an error when called more than once (regression)', async () => {
const sources1 = await desktopCapturer.getSources({ types: getSourceTypes() });
expect(sources1).toBeInstanceOf(Array);
+1
View File
@@ -258,6 +258,7 @@ declare namespace NodeJS {
_linkedBinding(name: 'electron_browser_desktop_capturer'): {
createDesktopCapturer(): ElectronInternal.DesktopCapturer;
isDisplayMediaSystemPickerAvailable(): boolean;
getRestoreToken(sourceId: string): string;
};
_linkedBinding(name: 'electron_browser_event_emitter'): { setEventEmitterPrototype(prototype: Object): void };
_linkedBinding(name: 'electron_browser_ipc_dispatch'): {
+6 -1
View File
@@ -256,7 +256,9 @@ declare namespace ElectronInternal {
captureWindow: boolean,
captureScreen: boolean,
thumbnailSize: Electron.Size,
fetchWindowIcons: boolean
fetchWindowIcons: boolean,
persistent: boolean,
restoreToken: string
): void;
_onerror?: (error: string) => void;
_onfinished?: (sources: Electron.DesktopCapturerSource[], fetchWindowIcons: boolean) => void;
@@ -267,6 +269,8 @@ declare namespace ElectronInternal {
captureScreen: boolean;
thumbnailSize: Electron.Size;
fetchWindowIcons: boolean;
persistent: boolean;
restoreToken: string;
}
interface GetSourcesResult {
@@ -274,6 +278,7 @@ declare namespace ElectronInternal {
name: string;
thumbnail: Electron.NativeImage;
display_id: string;
restoreToken: string;
appIcon: Electron.NativeImage | null;
}