Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add electron.DesktopCapturer.setSkipCursor() method #30231

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/api/desktop-capturer.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,52 @@ which can detected by [`systemPreferences.getMediaAccessStatus`].
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-windows-macos

### `desktopCapturer.setSkipCursor(sourceId, skip)`

Used to dynamically show or stop cursor capture in the mediastream when sharing content.

* `sourceId` String - Device id in the format of [`DesktopCapturerSource`](structures/desktop-capturer-source.md) 's id.
* `skip` Boolean

By default the cursor is captured. Following example shows how to toggle the cursor capture when sharing content.

```javascript
// In the renderer process.
const { desktopCapturer } = require('electron')
let capturedSourceId
desktopCapturer.getSources({ types: ['window', 'screen'] }).then(async sources => {
for (const source of sources) {
if (source.name === 'Electron') {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: source.id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720
}
}
})
// store the captured source id
capturedSourceId = source.id
// handle stream
} catch (e) {
// handle error
}
return
}
}
})
// On some condition like say button click we want to toggle cursor capture
function onToggleCursor (isSkipCursor) {
desktopCapturer.setSkipCursor(capturedSourceId, isSkipCursor)
}
```

## Caveats

`navigator.mediaDevices.getUserMedia` does not work on macOS for audio capture due to a fundamental limitation whereby apps that want to access the system's audio require a [signed kernel extension](https://developer.apple.com/library/archive/documentation/Security/Conceptual/System_Integrity_Protection_Guide/KernelExtensions/KernelExtensions.html). Chromium, and by extension Electron, does not provide this.
Expand Down
4 changes: 4 additions & 0 deletions lib/browser/api/desktop-capturer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ export async function getSources (args: Electron.SourcesOptions) {

return getSources;
}

export async function setSkipCursor (sourceId: string, skipCursor: boolean) {
return setSkipCursorImpl(null, sourceId, skipCursor);
}
86 changes: 86 additions & 0 deletions lib/browser/desktop-capturer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { createDesktopCapturer, setSkipCursor } = process._linkedBinding('electron_browser_desktop_capturer');

const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b);

let currentlyRunning: {
options: ElectronInternal.GetSourcesOptions;
getSources: Promise<ElectronInternal.GetSourcesResult[]>;
}[] = [];

// |options.types| can't be empty and must be an array
function isValid (options: Electron.SourcesOptions) {
const types = options ? options.types : undefined;
return Array.isArray(types);
}

export const getSourcesImpl = (sender: Electron.WebContents | null, args: Electron.SourcesOptions) => {
if (!isValid(args)) throw new Error('Invalid options');

const captureWindow = args.types.includes('window');
const captureScreen = args.types.includes('screen');

const { thumbnailSize = { width: 150, height: 150 } } = args;
const { fetchWindowIcons = false } = args;

const options = {
captureWindow,
captureScreen,
thumbnailSize,
fetchWindowIcons
};

for (const running of currentlyRunning) {
if (deepEqual(running.options, options)) {
// If a request is currently running for the same options
// return that promise
return running.getSources;
}
}

const getSources = new Promise<ElectronInternal.GetSourcesResult[]>((resolve, reject) => {
let capturer: ElectronInternal.DesktopCapturer | null = createDesktopCapturer();

const stopRunning = () => {
if (capturer) {
delete capturer._onerror;
delete capturer._onfinished;
capturer = null;
}
// Remove from currentlyRunning once we resolve or reject
currentlyRunning = currentlyRunning.filter(running => running.options !== options);
if (sender) {
sender.removeListener('destroyed', stopRunning);
}
};

capturer._onerror = (error: string) => {
stopRunning();
reject(error);
};

capturer._onfinished = (sources: Electron.DesktopCapturerSource[]) => {
stopRunning();
resolve(sources);
};

capturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons);

// If the WebContents is destroyed before receiving result, just remove the
// reference to emit and the capturer itself so that it never dispatches
// back to the renderer
if (sender) {
sender.once('destroyed', stopRunning);
}
});

currentlyRunning.push({
options,
getSources
});

return getSources;
};

export const setSkipCursorImpl = (event: Electron.IpcMainEvent | null, sourceId: string, skipCursor: boolean) => {
setSkipCursor(sourceId, skipCursor);
};
21 changes: 21 additions & 0 deletions lib/browser/rpc-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_CLIPBOARD_SYNC, function (event, me
return (clipboard as any)[method](...args);
});

if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {
const desktopCapturer = require('@electron/internal/browser/desktop-capturer') as typeof desktopCapturerModule;

ipcMainInternal.handle(IPC_MESSAGES.DESKTOP_CAPTURER_GET_SOURCES, async function (event, options: Electron.SourcesOptions, stack: string) {
logStack(event.sender, 'desktopCapturer.getSources()', stack);
const customEvent = emitCustomEvent(event.sender, 'desktop-capturer-get-sources');

if (customEvent.defaultPrevented) {
console.error('Blocked desktopCapturer.getSources()');
return [];
}

return typeUtils.serialize(await desktopCapturer.getSourcesImpl(event.sender, options));
});

ipcMainInternal.handle(IPC_MESSAGES.DESKTOP_CAPTURER_SET_SKIP_CURSOR, function (event, sourceId, skipCursor, stack) {
logStack(event.sender, 'desktopCapturer.setSkipCursor()', stack);
desktopCapturer.setSkipCursorImpl(event, sourceId, skipCursor);
});
}

const getPreloadScript = async function (preloadPath: string) {
let preloadSrc = null;
let preloadError = null;
Expand Down
4 changes: 4 additions & 0 deletions lib/common/ipc-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ export const enum IPC_MESSAGES {
INSPECTOR_CONFIRM = 'INSPECTOR_CONFIRM',
INSPECTOR_CONTEXT_MENU = 'INSPECTOR_CONTEXT_MENU',
INSPECTOR_SELECT_FILE = 'INSPECTOR_SELECT_FILE',

DESKTOP_CAPTURER_GET_SOURCES = 'DESKTOP_CAPTURER_GET_SOURCES',
DESKTOP_CAPTURER_SET_SKIP_CURSOR = 'DESKTOP_CAPTURER_SET_SKIP_CURSOR',
NATIVE_IMAGE_CREATE_THUMBNAIL_FROM_PATH = 'NATIVE_IMAGE_CREATE_THUMBNAIL_FROM_PATH',
}
23 changes: 23 additions & 0 deletions lib/renderer/api/desktop-capturer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import { deserialize } from '@electron/internal/common/type-utils';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';

const { hasSwitch } = process._linkedBinding('electron_common_command_line');

const enableStacks = hasSwitch('enable-api-filtering-logging');

function getCurrentStack () {
const target = {};
if (enableStacks) {
Error.captureStackTrace(target, getCurrentStack);
}
return (target as any).stack;
}

export async function getSources (options: Electron.SourcesOptions) {
return deserialize(await ipcRendererInternal.invoke(IPC_MESSAGES.DESKTOP_CAPTURER_GET_SOURCES, options, getCurrentStack()));
}

export function setSkipCursor (sourceId: string, skipCursor: boolean) {
ipcRendererInternal.invoke(IPC_MESSAGES.DESKTOP_CAPTURER_SET_SKIP_CURSOR, sourceId, skipCursor, getCurrentStack());
}
2 changes: 2 additions & 0 deletions patches/chromium/.patches
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ build_do_not_depend_on_packed_resource_integrity.patch
refactor_restore_base_adaptcallbackforrepeating.patch
hack_to_allow_gclient_sync_with_host_os_mac_on_linux_in_ci.patch
don_t_run_pcscan_notifythreadcreated_if_pcscan_is_disabled.patch
add_gin_wrappable_crash_key.patch
feat_implement_dynamic_setskipcursor_feature.patch
logging_win32_only_create_a_console_if_logging_to_stderr.patch
fix_media_key_usage_with_globalshortcuts.patch
feat_expose_raw_response_headers_from_urlloader.patch
Expand Down