diff --git a/build/webpack/webpack.config.base.js b/build/webpack/webpack.config.base.js index d24776a9604..c257c3d913f 100644 --- a/build/webpack/webpack.config.base.js +++ b/build/webpack/webpack.config.base.js @@ -46,6 +46,7 @@ const ignoredModules = [] if (defines['ENABLE_DESKTOP_CAPTURER'] === 'false') { ignoredModules.push( '@electron/internal/browser/desktop-capturer', + '@electron/internal/browser/api/desktop-capturer', '@electron/internal/renderer/api/desktop-capturer' ) } diff --git a/docs/api/desktop-capturer.md b/docs/api/desktop-capturer.md index bf376ce46a9..42b7d09250b 100644 --- a/docs/api/desktop-capturer.md +++ b/docs/api/desktop-capturer.md @@ -3,7 +3,7 @@ > Access information about media sources that can be used to capture audio and > 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 title is `Electron`: diff --git a/filenames.auto.gni b/filenames.auto.gni index e2e909cae6f..bae1d68171a 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -193,6 +193,7 @@ auto_filenames = { "lib/browser/api/browser-window.js", "lib/browser/api/content-tracing.js", "lib/browser/api/crash-reporter.ts", + "lib/browser/api/desktop-capturer.ts", "lib/browser/api/dialog.js", "lib/browser/api/exports/electron.ts", "lib/browser/api/global-shortcut.js", diff --git a/lib/browser/api/desktop-capturer.ts b/lib/browser/api/desktop-capturer.ts new file mode 100644 index 00000000000..2a958210e5f --- /dev/null +++ b/lib/browser/api/desktop-capturer.ts @@ -0,0 +1,5 @@ +import { getSourcesImpl } from '@electron/internal/browser/desktop-capturer'; + +export async function getSources (options: Electron.SourcesOptions) { + return getSourcesImpl(null, options); +} diff --git a/lib/browser/api/module-list.ts b/lib/browser/api/module-list.ts index 76aa6ff7111..e931cff3ee9 100644 --- a/lib/browser/api/module-list.ts +++ b/lib/browser/api/module-list.ts @@ -33,6 +33,12 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [ { 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)) { browserModuleList.push( { name: 'ImageView', loader: () => require('./views/image-view') } diff --git a/lib/browser/api/module-names.ts b/lib/browser/api/module-names.ts index acf6418c1aa..d8d24d0d4c1 100644 --- a/lib/browser/api/module-names.ts +++ b/lib/browser/api/module-names.ts @@ -36,6 +36,10 @@ export const browserModuleNames = [ 'WebContentsView' ]; +if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) { + browserModuleNames.push('desktopCapturer'); +} + if (BUILDFLAG(ENABLE_VIEWS_API)) { browserModuleNames.push( 'ImageView' diff --git a/lib/browser/desktop-capturer.ts b/lib/browser/desktop-capturer.ts index 6bfe0bd6324..9d9569ea27f 100644 --- a/lib/browser/desktop-capturer.ts +++ b/lib/browser/desktop-capturer.ts @@ -7,7 +7,28 @@ let currentlyRunning: { getSources: Promise; }[] = []; -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) { if (deepEqual(running.options, 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); }; - 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 // reference to emit and the capturer itself so that it never dispatches // back to the renderer - event.sender.once('destroyed', () => stopRunning()); + if (event) { + event.sender.once('destroyed', () => stopRunning()); + } }); currentlyRunning.push({ diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index bfa565a3696..36f603a22ac 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -71,7 +71,7 @@ if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) { return []; } - return typeUtils.serialize(await desktopCapturer.getSources(event, options)); + return typeUtils.serialize(await desktopCapturer.getSourcesImpl(event, options)); }); } diff --git a/lib/renderer/api/desktop-capturer.ts b/lib/renderer/api/desktop-capturer.ts index 1e7c0b10984..7e0379c21da 100644 --- a/lib/renderer/api/desktop-capturer.ts +++ b/lib/renderer/api/desktop-capturer.ts @@ -3,12 +3,6 @@ import { deserialize } from '@electron/internal/common/type-utils'; 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'); function getCurrentStack () { @@ -20,20 +14,5 @@ function getCurrentStack () { } export async function getSources (options: Electron.SourcesOptions) { - if (!isValid(options)) throw new Error('Invalid options'); - - 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('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', { - captureWindow, - captureScreen, - thumbnailSize, - fetchWindowIcons - } as ElectronInternal.GetSourcesOptions, getCurrentStack()); - - return deserialize(sources); + return deserialize(await ipcRendererInternal.invoke('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', options, getCurrentStack())); } diff --git a/spec-main/api-desktop-capturer-spec.ts b/spec-main/api-desktop-capturer-spec.ts index ac43c69eff2..38829c2ce1b 100644 --- a/spec-main/api-desktop-capturer-spec.ts +++ b/spec-main/api-desktop-capturer-spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { screen, BrowserWindow, SourcesOptions } from 'electron/main'; -import { desktopCapturer } from 'electron/renderer'; +import { desktopCapturer } from 'electron/common'; import { emittedOnce } from './events-helpers'; import { ifdescribe, ifit } from './spec-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. - 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(); - }); + const generateSpecs = (description: string, getSources: typeof desktopCapturer.getSources) => { + describe(description, () => { + // TODO(nornagon): figure out why this test is failing on Linux and re-enable it. + 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 () => { - const promise = getSources(['window', 'screen'] as any); - await expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options'); - }); + it('throws an error for invalid options', async () => { + const promise = getSources(['window', 'screen'] as any); + 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. - ifit(process.platform !== 'linux')('does not throw an error when called more than once (regression)', async () => { - const sources1 = await getSources({ types: ['window', 'screen'] }); - expect(sources1).to.be.an('array').that.is.not.empty(); + // 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 () => { + const sources1 = await getSources({ types: ['window', 'screen'] }); + expect(sources1).to.be.an('array').that.is.not.empty(); - const sources2 = await getSources({ types: ['window', 'screen'] }); - expect(sources2).to.be.an('array').that.is.not.empty(); - }); + const sources2 = await getSources({ types: ['window', 'screen'] }); + expect(sources2).to.be.an('array').that.is.not.empty(); + }); - ifit(process.platform !== 'linux')('responds to subsequent calls of different options', async () => { - const promise1 = getSources({ types: ['window'] }); - await expect(promise1).to.eventually.be.fulfilled(); + ifit(process.platform !== 'linux')('responds to subsequent calls of different options', async () => { + const promise1 = getSources({ types: ['window'] }); + await expect(promise1).to.eventually.be.fulfilled(); - const promise2 = getSources({ types: ['screen'] }); - await expect(promise2).to.eventually.be.fulfilled(); - }); + const promise2 = getSources({ types: ['screen'] }); + await expect(promise2).to.eventually.be.fulfilled(); + }); - // Linux doesn't return any window sources. - 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 }); + // Linux doesn't return any window sources. + 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 }); + await w.loadURL('about:blank'); - const sources = await getSources({ types: ['window'] }); - w.destroy(); - expect(sources).to.be.an('array').that.is.not.empty(); - for (const { display_id: displayId } of sources) { - expect(displayId).to.be.a('string').and.be.empty(); - } - }); + const sources = await getSources({ types: ['window'] }); + w.destroy(); + expect(sources).to.be.an('array').that.is.not.empty(); + for (const { display_id: displayId } of sources) { + 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 () => { - const displays = screen.getAllDisplays(); - const sources = await getSources({ types: ['screen'] }); - expect(sources).to.be.an('array').of.length(displays.length); + ifit(process.platform !== 'linux')('returns display_ids matching the Screen API on Windows and Mac', async () => { + const displays = screen.getAllDisplays(); + const sources = await getSources({ types: ['screen'] }); + expect(sources).to.be.an('array').of.length(displays.length); - for (let i = 0; i < sources.length; i++) { - expect(sources[i].display_id).to.equal(displays[i].id.toString()); - } - }); + for (let i = 0; i < sources.length; i++) { + 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 () => { w.webContents.once('desktop-capturer-get-sources', (event) => {