feat: expose the desktopCapturer module in the main process (#23548)
This commit is contained in:
parent
4b23a85475
commit
df53816eea
10 changed files with 95 additions and 68 deletions
|
@ -46,6 +46,7 @@ const ignoredModules = []
|
||||||
if (defines['ENABLE_DESKTOP_CAPTURER'] === 'false') {
|
if (defines['ENABLE_DESKTOP_CAPTURER'] === 'false') {
|
||||||
ignoredModules.push(
|
ignoredModules.push(
|
||||||
'@electron/internal/browser/desktop-capturer',
|
'@electron/internal/browser/desktop-capturer',
|
||||||
|
'@electron/internal/browser/api/desktop-capturer',
|
||||||
'@electron/internal/renderer/api/desktop-capturer'
|
'@electron/internal/renderer/api/desktop-capturer'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
> Access information about media sources that can be used to capture audio and
|
> 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.getUserMedia`] API.
|
||||||
|
|
||||||
Process: [Renderer](../glossary.md#renderer-process)
|
Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process)
|
||||||
|
|
||||||
The following example shows how to capture video from a desktop window whose
|
The following example shows how to capture video from a desktop window whose
|
||||||
title is `Electron`:
|
title is `Electron`:
|
||||||
|
|
|
@ -193,6 +193,7 @@ auto_filenames = {
|
||||||
"lib/browser/api/browser-window.js",
|
"lib/browser/api/browser-window.js",
|
||||||
"lib/browser/api/content-tracing.js",
|
"lib/browser/api/content-tracing.js",
|
||||||
"lib/browser/api/crash-reporter.ts",
|
"lib/browser/api/crash-reporter.ts",
|
||||||
|
"lib/browser/api/desktop-capturer.ts",
|
||||||
"lib/browser/api/dialog.js",
|
"lib/browser/api/dialog.js",
|
||||||
"lib/browser/api/exports/electron.ts",
|
"lib/browser/api/exports/electron.ts",
|
||||||
"lib/browser/api/global-shortcut.js",
|
"lib/browser/api/global-shortcut.js",
|
||||||
|
|
5
lib/browser/api/desktop-capturer.ts
Normal file
5
lib/browser/api/desktop-capturer.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { getSourcesImpl } from '@electron/internal/browser/desktop-capturer';
|
||||||
|
|
||||||
|
export async function getSources (options: Electron.SourcesOptions) {
|
||||||
|
return getSourcesImpl(null, options);
|
||||||
|
}
|
|
@ -33,6 +33,12 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
|
||||||
{ name: 'WebContentsView', loader: () => require('./web-contents-view') }
|
{ name: 'WebContentsView', loader: () => require('./web-contents-view') }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {
|
||||||
|
browserModuleList.push(
|
||||||
|
{ name: 'desktopCapturer', loader: () => require('./desktop-capturer') }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (BUILDFLAG(ENABLE_VIEWS_API)) {
|
if (BUILDFLAG(ENABLE_VIEWS_API)) {
|
||||||
browserModuleList.push(
|
browserModuleList.push(
|
||||||
{ name: 'ImageView', loader: () => require('./views/image-view') }
|
{ name: 'ImageView', loader: () => require('./views/image-view') }
|
||||||
|
|
|
@ -36,6 +36,10 @@ export const browserModuleNames = [
|
||||||
'WebContentsView'
|
'WebContentsView'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {
|
||||||
|
browserModuleNames.push('desktopCapturer');
|
||||||
|
}
|
||||||
|
|
||||||
if (BUILDFLAG(ENABLE_VIEWS_API)) {
|
if (BUILDFLAG(ENABLE_VIEWS_API)) {
|
||||||
browserModuleNames.push(
|
browserModuleNames.push(
|
||||||
'ImageView'
|
'ImageView'
|
||||||
|
|
|
@ -7,7 +7,28 @@ let currentlyRunning: {
|
||||||
getSources: Promise<ElectronInternal.GetSourcesResult[]>;
|
getSources: Promise<ElectronInternal.GetSourcesResult[]>;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
export const getSources = (event: Electron.IpcMainEvent, options: ElectronInternal.GetSourcesOptions) => {
|
// |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 = (event: Electron.IpcMainEvent | 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) {
|
for (const running of currentlyRunning) {
|
||||||
if (deepEqual(running.options, options)) {
|
if (deepEqual(running.options, options)) {
|
||||||
// If a request is currently running for the same options
|
// If a request is currently running for the same options
|
||||||
|
@ -39,12 +60,14 @@ export const getSources = (event: Electron.IpcMainEvent, options: ElectronIntern
|
||||||
resolve(sources);
|
resolve(sources);
|
||||||
};
|
};
|
||||||
|
|
||||||
capturer.startHandling(options.captureWindow, options.captureScreen, options.thumbnailSize, options.fetchWindowIcons);
|
capturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons);
|
||||||
|
|
||||||
// If the WebContents is destroyed before receiving result, just remove the
|
// If the WebContents is destroyed before receiving result, just remove the
|
||||||
// reference to emit and the capturer itself so that it never dispatches
|
// reference to emit and the capturer itself so that it never dispatches
|
||||||
// back to the renderer
|
// back to the renderer
|
||||||
event.sender.once('destroyed', () => stopRunning());
|
if (event) {
|
||||||
|
event.sender.once('destroyed', () => stopRunning());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
currentlyRunning.push({
|
currentlyRunning.push({
|
||||||
|
|
|
@ -71,7 +71,7 @@ if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeUtils.serialize(await desktopCapturer.getSources(event, options));
|
return typeUtils.serialize(await desktopCapturer.getSourcesImpl(event, options));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,6 @@ import { deserialize } from '@electron/internal/common/type-utils';
|
||||||
|
|
||||||
const { hasSwitch } = process.electronBinding('command_line');
|
const { hasSwitch } = process.electronBinding('command_line');
|
||||||
|
|
||||||
// |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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableStacks = hasSwitch('enable-api-filtering-logging');
|
const enableStacks = hasSwitch('enable-api-filtering-logging');
|
||||||
|
|
||||||
function getCurrentStack () {
|
function getCurrentStack () {
|
||||||
|
@ -20,20 +14,5 @@ function getCurrentStack () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources (options: Electron.SourcesOptions) {
|
export async function getSources (options: Electron.SourcesOptions) {
|
||||||
if (!isValid(options)) throw new Error('Invalid options');
|
return deserialize(await ipcRendererInternal.invoke('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', options, getCurrentStack()));
|
||||||
|
|
||||||
const captureWindow = options.types.includes('window');
|
|
||||||
const captureScreen = options.types.includes('screen');
|
|
||||||
|
|
||||||
const { thumbnailSize = { width: 150, height: 150 } } = options;
|
|
||||||
const { fetchWindowIcons = false } = options;
|
|
||||||
|
|
||||||
const sources = await ipcRendererInternal.invoke<ElectronInternal.GetSourcesResult[]>('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', {
|
|
||||||
captureWindow,
|
|
||||||
captureScreen,
|
|
||||||
thumbnailSize,
|
|
||||||
fetchWindowIcons
|
|
||||||
} as ElectronInternal.GetSourcesOptions, getCurrentStack());
|
|
||||||
|
|
||||||
return deserialize(sources);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { screen, BrowserWindow, SourcesOptions } from 'electron/main';
|
import { screen, BrowserWindow, SourcesOptions } from 'electron/main';
|
||||||
import { desktopCapturer } from 'electron/renderer';
|
import { desktopCapturer } from 'electron/common';
|
||||||
import { emittedOnce } from './events-helpers';
|
import { emittedOnce } from './events-helpers';
|
||||||
import { ifdescribe, ifit } from './spec-helpers';
|
import { ifdescribe, ifit } from './spec-helpers';
|
||||||
import { closeAllWindows } from './window-helpers';
|
import { closeAllWindows } from './window-helpers';
|
||||||
|
@ -21,55 +21,63 @@ ifdescribe(features.isDesktopCapturerEnabled() && !process.arch.includes('arm')
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO(nornagon): figure out why this test is failing on Linux and re-enable it.
|
const generateSpecs = (description: string, getSources: typeof desktopCapturer.getSources) => {
|
||||||
ifit(process.platform !== 'linux')('should return a non-empty array of sources', async () => {
|
describe(description, () => {
|
||||||
const sources = await getSources({ types: ['window', 'screen'] });
|
// TODO(nornagon): figure out why this test is failing on Linux and re-enable it.
|
||||||
expect(sources).to.be.an('array').that.is.not.empty();
|
ifit(process.platform !== 'linux')('should return a non-empty array of sources', async () => {
|
||||||
});
|
const sources = await getSources({ types: ['window', 'screen'] });
|
||||||
|
expect(sources).to.be.an('array').that.is.not.empty();
|
||||||
|
});
|
||||||
|
|
||||||
it('throws an error for invalid options', async () => {
|
it('throws an error for invalid options', async () => {
|
||||||
const promise = getSources(['window', 'screen'] as any);
|
const promise = getSources(['window', 'screen'] as any);
|
||||||
await expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options');
|
await expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(nornagon): figure out why this test is failing on Linux and re-enable it.
|
// TODO(nornagon): figure out why this test is failing on Linux and re-enable it.
|
||||||
ifit(process.platform !== 'linux')('does not throw an error when called more than once (regression)', async () => {
|
ifit(process.platform !== 'linux')('does not throw an error when called more than once (regression)', async () => {
|
||||||
const sources1 = await getSources({ types: ['window', 'screen'] });
|
const sources1 = await getSources({ types: ['window', 'screen'] });
|
||||||
expect(sources1).to.be.an('array').that.is.not.empty();
|
expect(sources1).to.be.an('array').that.is.not.empty();
|
||||||
|
|
||||||
const sources2 = await getSources({ types: ['window', 'screen'] });
|
const sources2 = await getSources({ types: ['window', 'screen'] });
|
||||||
expect(sources2).to.be.an('array').that.is.not.empty();
|
expect(sources2).to.be.an('array').that.is.not.empty();
|
||||||
});
|
});
|
||||||
|
|
||||||
ifit(process.platform !== 'linux')('responds to subsequent calls of different options', async () => {
|
ifit(process.platform !== 'linux')('responds to subsequent calls of different options', async () => {
|
||||||
const promise1 = getSources({ types: ['window'] });
|
const promise1 = getSources({ types: ['window'] });
|
||||||
await expect(promise1).to.eventually.be.fulfilled();
|
await expect(promise1).to.eventually.be.fulfilled();
|
||||||
|
|
||||||
const promise2 = getSources({ types: ['screen'] });
|
const promise2 = getSources({ types: ['screen'] });
|
||||||
await expect(promise2).to.eventually.be.fulfilled();
|
await expect(promise2).to.eventually.be.fulfilled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Linux doesn't return any window sources.
|
// Linux doesn't return any window sources.
|
||||||
ifit(process.platform !== 'linux')('returns an empty display_id for window sources on Windows and Mac', async () => {
|
ifit(process.platform !== 'linux')('returns an empty display_id for window sources on Windows and Mac', async () => {
|
||||||
const w = new BrowserWindow({ width: 200, height: 200 });
|
const w = new BrowserWindow({ width: 200, height: 200 });
|
||||||
|
await w.loadURL('about:blank');
|
||||||
|
|
||||||
const sources = await getSources({ types: ['window'] });
|
const sources = await getSources({ types: ['window'] });
|
||||||
w.destroy();
|
w.destroy();
|
||||||
expect(sources).to.be.an('array').that.is.not.empty();
|
expect(sources).to.be.an('array').that.is.not.empty();
|
||||||
for (const { display_id: displayId } of sources) {
|
for (const { display_id: displayId } of sources) {
|
||||||
expect(displayId).to.be.a('string').and.be.empty();
|
expect(displayId).to.be.a('string').and.be.empty();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ifit(process.platform !== 'linux')('returns display_ids matching the Screen API on Windows and Mac', async () => {
|
ifit(process.platform !== 'linux')('returns display_ids matching the Screen API on Windows and Mac', async () => {
|
||||||
const displays = screen.getAllDisplays();
|
const displays = screen.getAllDisplays();
|
||||||
const sources = await getSources({ types: ['screen'] });
|
const sources = await getSources({ types: ['screen'] });
|
||||||
expect(sources).to.be.an('array').of.length(displays.length);
|
expect(sources).to.be.an('array').of.length(displays.length);
|
||||||
|
|
||||||
for (let i = 0; i < sources.length; i++) {
|
for (let i = 0; i < sources.length; i++) {
|
||||||
expect(sources[i].display_id).to.equal(displays[i].id.toString());
|
expect(sources[i].display_id).to.equal(displays[i].id.toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSpecs('in renderer process', getSources);
|
||||||
|
generateSpecs('in main process', desktopCapturer.getSources);
|
||||||
|
|
||||||
ifit(process.platform !== 'linux')('returns an empty source list if blocked by the main process', async () => {
|
ifit(process.platform !== 'linux')('returns an empty source list if blocked by the main process', async () => {
|
||||||
w.webContents.once('desktop-capturer-get-sources', (event) => {
|
w.webContents.once('desktop-capturer-get-sources', (event) => {
|
||||||
|
|
Loading…
Reference in a new issue