diff --git a/filenames.auto.gni b/filenames.auto.gni index 78976452acb9..fd74881e17f1 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -127,7 +127,7 @@ auto_filenames = { "lib/common/error-utils.js", "lib/common/web-view-methods.js", "lib/renderer/api/crash-reporter.js", - "lib/renderer/api/desktop-capturer.js", + "lib/renderer/api/desktop-capturer.ts", "lib/renderer/api/ipc-renderer.js", "lib/renderer/api/remote.js", "lib/renderer/api/web-frame.ts", @@ -231,7 +231,7 @@ auto_filenames = { "lib/browser/chrome-extension.js", "lib/browser/crash-reporter-init.js", "lib/browser/default-menu.ts", - "lib/browser/desktop-capturer.js", + "lib/browser/desktop-capturer.ts", "lib/browser/devtools.js", "lib/browser/guest-view-manager.js", "lib/browser/guest-window-manager.js", @@ -285,7 +285,7 @@ auto_filenames = { "lib/common/reset-search-paths.ts", "lib/common/web-view-methods.js", "lib/renderer/api/crash-reporter.js", - "lib/renderer/api/desktop-capturer.js", + "lib/renderer/api/desktop-capturer.ts", "lib/renderer/api/exports/electron.js", "lib/renderer/api/ipc-renderer.js", "lib/renderer/api/module-list.js", @@ -334,7 +334,7 @@ auto_filenames = { "lib/common/init.ts", "lib/common/reset-search-paths.ts", "lib/renderer/api/crash-reporter.js", - "lib/renderer/api/desktop-capturer.js", + "lib/renderer/api/desktop-capturer.ts", "lib/renderer/api/exports/electron.js", "lib/renderer/api/ipc-renderer.js", "lib/renderer/api/module-list.js", diff --git a/lib/browser/desktop-capturer.js b/lib/browser/desktop-capturer.js deleted file mode 100644 index 4c8373524de5..000000000000 --- a/lib/browser/desktop-capturer.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict' - -const { createDesktopCapturer } = process.electronBinding('desktop_capturer') - -const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b) - -let currentlyRunning = [] - -exports.getSources = (event, captureWindow, captureScreen, thumbnailSize, fetchWindowIcons) => { - 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((resolve, reject) => { - const stopRunning = () => { - // Remove from currentlyRunning once we resolve or reject - currentlyRunning = currentlyRunning.filter(running => running.options !== options) - } - const request = { - options, - resolve: (value) => { - stopRunning() - resolve(value) - }, - reject: (err) => { - stopRunning() - reject(err) - }, - capturer: createDesktopCapturer() - } - request.capturer.emit = createCapturerEmitHandler(request.capturer, request) - request.capturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons) - - // If the WebContents is destroyed before receiving result, just remove the - // reference to resolve, emit and the capturer itself so that it never dispatches - // back to the renderer - event.sender.once('destroyed', () => { - request.resolve = null - delete request.capturer.emit - delete request.capturer - stopRunning() - }) - }) - - currentlyRunning.push({ - options, - getSources - }) - - return getSources -} - -const createCapturerEmitHandler = (capturer, request) => { - return function handlEmitOnCapturer (event, name, sources, fetchWindowIcons) { - // Ensure that this capturer instance can only ever receive a single event - // if we get more than one it is a bug but will also cause strange behavior - // if we still try to handle it - delete capturer.emit - - if (name === 'error') { - const error = sources - request.reject(error) - return - } - - const result = sources.map(source => { - return { - id: source.id, - name: source.name, - thumbnail: source.thumbnail.toDataURL(), - display_id: source.display_id, - appIcon: (fetchWindowIcons && source.appIcon) ? source.appIcon.toDataURL() : null - } - }) - - if (request.resolve) { - request.resolve(result) - } - } -} diff --git a/lib/browser/desktop-capturer.ts b/lib/browser/desktop-capturer.ts new file mode 100644 index 000000000000..adcba1f86bab --- /dev/null +++ b/lib/browser/desktop-capturer.ts @@ -0,0 +1,66 @@ +import { EventEmitter } from 'events' + +const { createDesktopCapturer } = process.electronBinding('desktop_capturer') + +const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b) + +let currentlyRunning: { + options: ElectronInternal.GetSourcesOptions; + getSources: Promise; +}[] = [] + +export const getSources = (event: Electron.IpcMainEvent, options: ElectronInternal.GetSourcesOptions) => { + 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((resolve, reject) => { + const stopRunning = () => { + // Remove from currentlyRunning once we resolve or reject + currentlyRunning = currentlyRunning.filter(running => running.options !== options) + } + + const emitter = new EventEmitter() + + emitter.once('error', (event, error: string) => { + stopRunning() + reject(error) + }) + + emitter.once('finished', (event, sources: Electron.DesktopCapturerSource[], fetchWindowIcons: boolean) => { + stopRunning() + resolve(sources.map(source => ({ + id: source.id, + name: source.name, + thumbnail: source.thumbnail.toDataURL(), + display_id: source.display_id, + appIcon: (fetchWindowIcons && source.appIcon) ? source.appIcon.toDataURL() : null + }))) + }) + + let capturer: ElectronInternal.DesktopCapturer | null = createDesktopCapturer() + + capturer.emit = emitter.emit.bind(emitter) + capturer.startHandling(options.captureWindow, options.captureScreen, options.thumbnailSize, options.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', () => { + capturer!.emit = null + capturer = null + stopRunning() + }) + }) + + currentlyRunning.push({ + options, + getSources + }) + + return getSources +} diff --git a/lib/common/api/exports/electron.js b/lib/common/api/exports/electron.js index 0dc364081281..1113f12c899a 100644 --- a/lib/common/api/exports/electron.js +++ b/lib/common/api/exports/electron.js @@ -4,7 +4,7 @@ const moduleList = require('@electron/internal/common/api/module-list') exports.handleESModule = (loader) => () => { const value = loader() - if (value.__esModule) return value.default + if (value.__esModule && value.default) return value.default return value } diff --git a/lib/renderer/api/desktop-capturer.js b/lib/renderer/api/desktop-capturer.js deleted file mode 100644 index 13f89f875562..000000000000 --- a/lib/renderer/api/desktop-capturer.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -const { nativeImage, deprecate } = require('electron') -const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') - -// |options.types| can't be empty and must be an array -function isValid (options) { - const types = options ? options.types : undefined - return Array.isArray(types) -} - -function mapSources (sources) { - return sources.map(source => ({ - id: source.id, - name: source.name, - thumbnail: nativeImage.createFromDataURL(source.thumbnail), - display_id: source.display_id, - appIcon: source.appIcon ? nativeImage.createFromDataURL(source.appIcon) : null - })) -} - -exports.getSources = (options) => { - return new Promise((resolve, reject) => { - if (!isValid(options)) throw new Error('Invalid options') - - const captureWindow = options.types.includes('window') - const captureScreen = options.types.includes('screen') - - if (options.thumbnailSize == null) { - options.thumbnailSize = { - width: 150, - height: 150 - } - } - if (options.fetchWindowIcons == null) { - options.fetchWindowIcons = false - } - - ipcRendererUtils.invoke('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, options.fetchWindowIcons) - .then(sources => resolve(mapSources(sources)), reject) - }) -} diff --git a/lib/renderer/api/desktop-capturer.ts b/lib/renderer/api/desktop-capturer.ts new file mode 100644 index 000000000000..f39cc007373e --- /dev/null +++ b/lib/renderer/api/desktop-capturer.ts @@ -0,0 +1,33 @@ +import { nativeImage } from 'electron' +import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils' + +// |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 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 ipcRendererUtils.invoke('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', { + captureWindow, + captureScreen, + thumbnailSize, + fetchWindowIcons + } as ElectronInternal.GetSourcesOptions) + + return sources.map(source => ({ + id: source.id, + name: source.name, + thumbnail: nativeImage.createFromDataURL(source.thumbnail), + display_id: source.display_id, + appIcon: source.appIcon ? nativeImage.createFromDataURL(source.appIcon) : null + })) +} diff --git a/lib/sandboxed_renderer/api/exports/electron.js b/lib/sandboxed_renderer/api/exports/electron.js index a4d3e840dd77..d428563c5d9b 100644 --- a/lib/sandboxed_renderer/api/exports/electron.js +++ b/lib/sandboxed_renderer/api/exports/electron.js @@ -4,7 +4,7 @@ const moduleList = require('@electron/internal/sandboxed_renderer/api/module-lis const handleESModule = (m) => { // Handle Typescript modules with an "export default X" statement - if (m.__esModule) return m.default + if (m.__esModule && m.default) return m.default return m } diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 395a7bbc3576..f536f33a4fa9 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -19,6 +19,7 @@ declare namespace NodeJS { deleteHiddenValue(obj: any, key: string): void; requestGarbageCollectionForTesting(): void; } + interface Process { /** * DO NOT USE DIRECTLY, USE process.electronBinding @@ -29,6 +30,7 @@ declare namespace NodeJS { electronBinding(name: 'v8_util'): V8UtilBinding; electronBinding(name: 'app'): { app: Electron.App, App: Function }; electronBinding(name: 'command_line'): Electron.CommandLine; + electronBinding(name: 'desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer }; log: NodeJS.WriteStream['write']; activateUvLoop(): void; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index a2254a406008..b27369653db3 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -47,11 +47,6 @@ declare namespace Electron { allFrames: boolean } - interface RendererProcessPreference { - contentScripts: Array - extensionId: string; - } - interface IpcRendererInternal extends Electron.IpcRenderer { sendToAll(webContentsId: number, channel: string, ...args: any[]): void } @@ -88,6 +83,26 @@ declare namespace ElectronInternal { promisifyMultiArg any>(fn: T, /*convertPromiseValue: (v: any) => any*/): T; } + interface DesktopCapturer { + startHandling(captureWindow: boolean, captureScreen: boolean, thumbnailSize: Electron.Size, fetchWindowIcons: boolean): void; + emit: typeof NodeJS.EventEmitter.prototype.emit | null; + } + + interface GetSourcesOptions { + captureWindow: boolean; + captureScreen: boolean; + thumbnailSize: Electron.Size; + fetchWindowIcons: boolean; + } + + interface GetSourcesResult { + id: string; + name: string; + thumbnail: string; + display_id: string; + appIcon: string | null; + } + // Internal IPC has _replyInternal and NO reply method interface IpcMainInternalEvent extends Omit { _replyInternal(...args: any[]): void;