feat: expose the desktopCapturer module in the main process (#23548)

This commit is contained in:
Milan Burda 2020-05-21 02:25:49 +02:00 committed by GitHub
parent 4b23a85475
commit df53816eea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 95 additions and 68 deletions

View file

@ -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'
) )
} }

View file

@ -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`:

View file

@ -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",

View file

@ -0,0 +1,5 @@
import { getSourcesImpl } from '@electron/internal/browser/desktop-capturer';
export async function getSources (options: Electron.SourcesOptions) {
return getSourcesImpl(null, options);
}

View file

@ -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') }

View file

@ -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'

View file

@ -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({

View file

@ -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));
}); });
} }

View file

@ -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);
} }

View file

@ -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) => {