From 37d0d2b4cee4ecc07571e0546b20990a449f66f5 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 19 Oct 2026 19:46:12 +0200 Subject: [PATCH] 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. --- docs/api/desktop-capturer.md | 111 ++++++++++++++ .../api/structures/desktop-capturer-source.md | 4 + lib/browser/api/desktop-capturer.ts | 20 ++- .../api/electron_api_desktop_capturer.cc | 141 ++++++++++++------ .../api/electron_api_desktop_capturer.h | 10 +- spec/api-desktop-capturer.spec.ts | 47 +++++- typings/internal-ambient.d.ts | 1 + typings/internal-electron.d.ts | 7 +- 8 files changed, 286 insertions(+), 55 deletions(-) diff --git a/docs/api/desktop-capturer.md b/docs/api/desktop-capturer.md index 2e0ef601ff..c68b078a29 100644 --- a/docs/api/desktop-capturer.md +++ b/docs/api/desktop-capturer.md @@ -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` - 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` - 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_ + + + +* `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` - 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 diff --git a/docs/api/structures/desktop-capturer-source.md b/docs/api/structures/desktop-capturer-source.md index e898ebd523..c5c03a48c4 100644 --- a/docs/api/structures/desktop-capturer-source.md +++ b/docs/api/structures/desktop-capturer-source.md @@ -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). diff --git a/lib/browser/api/desktop-capturer.ts b/lib/browser/api/desktop-capturer.ts index 53d2f93f6a..34e3b6b205 100644 --- a/lib/browser/api/desktop-capturer.ts +++ b/lib/browser/api/desktop-capturer.ts @@ -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; }[] = []; -// |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; } diff --git a/shell/browser/api/electron_api_desktop_capturer.cc b/shell/browser/api/electron_api_desktop_capturer.cc index 3b03db80cc..898d87667a 100644 --- a/shell/browser/api/electron_api_desktop_capturer.cc +++ b/shell/browser/api/electron_api_desktop_capturer.cc @@ -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 MonitorAtomIdToDisplayId() { } #endif -std::unique_ptr 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 WrapCapturer( + std::unique_ptr 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(std::move(capturer)); +} + +std::unique_ptr 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 MakeWindowCapturer() { } #endif // BUILDFLAG(IS_MAC) - std::unique_ptr window_capturer = + return WrapCapturer( content::desktop_capture::CreateWindowCapturer( - content::desktop_capture::CreateDesktopCaptureOptions()); - return window_capturer ? std::make_unique( - std::move(window_capturer)) - : nullptr; + content::desktop_capture::CreateDesktopCaptureOptions()), + persistent, restore_token); } -std::unique_ptr MakeScreenCapturer() { +std::unique_ptr 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 MakeScreenCapturer() { } #endif // BUILDFLAG(IS_MAC) - std::unique_ptr screen_capturer = + return WrapCapturer( content::desktop_capture::CreateScreenCapturer( content::desktop_capture::CreateDesktopCaptureOptions(), - /*for_snapshot=*/false); - return screen_capturer ? std::make_unique( - std::move(screen_capturer)) - : nullptr; + /*for_snapshot=*/false), + persistent, restore_token); } #if BUILDFLAG(IS_WIN) @@ -217,6 +247,7 @@ struct Converter { 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& 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 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( - 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( @@ -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( 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 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 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 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 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 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::GetObjectTemplateBuilder(isolate) @@ -635,6 +682,8 @@ void Initialize(v8::Local exports, dict.SetMethod< &electron::api::DesktopCapturer::IsDisplayMediaSystemPickerAvailable>( "isDisplayMediaSystemPickerAvailable"); + dict.SetMethod<&electron::api::DesktopCapturer::GetRestoreToken>( + "getRestoreToken"); } } // namespace diff --git a/shell/browser/api/electron_api_desktop_capturer.h b/shell/browser/api/electron_api_desktop_capturer.h index d1101e4449..4ec673ade1 100644 --- a/shell/browser/api/electron_api_desktop_capturer.h +++ b/shell/browser/api/electron_api_desktop_capturer.h @@ -26,16 +26,24 @@ class DesktopCapturer final : public gin::Wrappable { // 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; diff --git a/spec/api-desktop-capturer.spec.ts b/spec/api-desktop-capturer.spec.ts index a386697ee3..b1a56e6a11 100644 --- a/spec/api-desktop-capturer.spec.ts +++ b/spec/api-desktop-capturer.spec.ts @@ -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); diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 7a1069dfa0..67bcdf6bfa 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -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'): { diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 96a8adfe85..367a65b5d4 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -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; }