refactor: re-implement desktop-capturer in TypeScript (#18580)
This commit is contained in:
parent
4ef8de69ef
commit
370e9522b4
9 changed files with 127 additions and 144 deletions
|
@ -127,7 +127,7 @@ auto_filenames = {
|
||||||
"lib/common/error-utils.js",
|
"lib/common/error-utils.js",
|
||||||
"lib/common/web-view-methods.js",
|
"lib/common/web-view-methods.js",
|
||||||
"lib/renderer/api/crash-reporter.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/ipc-renderer.js",
|
||||||
"lib/renderer/api/remote.js",
|
"lib/renderer/api/remote.js",
|
||||||
"lib/renderer/api/web-frame.ts",
|
"lib/renderer/api/web-frame.ts",
|
||||||
|
@ -231,7 +231,7 @@ auto_filenames = {
|
||||||
"lib/browser/chrome-extension.js",
|
"lib/browser/chrome-extension.js",
|
||||||
"lib/browser/crash-reporter-init.js",
|
"lib/browser/crash-reporter-init.js",
|
||||||
"lib/browser/default-menu.ts",
|
"lib/browser/default-menu.ts",
|
||||||
"lib/browser/desktop-capturer.js",
|
"lib/browser/desktop-capturer.ts",
|
||||||
"lib/browser/devtools.js",
|
"lib/browser/devtools.js",
|
||||||
"lib/browser/guest-view-manager.js",
|
"lib/browser/guest-view-manager.js",
|
||||||
"lib/browser/guest-window-manager.js",
|
"lib/browser/guest-window-manager.js",
|
||||||
|
@ -285,7 +285,7 @@ auto_filenames = {
|
||||||
"lib/common/reset-search-paths.ts",
|
"lib/common/reset-search-paths.ts",
|
||||||
"lib/common/web-view-methods.js",
|
"lib/common/web-view-methods.js",
|
||||||
"lib/renderer/api/crash-reporter.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/exports/electron.js",
|
||||||
"lib/renderer/api/ipc-renderer.js",
|
"lib/renderer/api/ipc-renderer.js",
|
||||||
"lib/renderer/api/module-list.js",
|
"lib/renderer/api/module-list.js",
|
||||||
|
@ -334,7 +334,7 @@ auto_filenames = {
|
||||||
"lib/common/init.ts",
|
"lib/common/init.ts",
|
||||||
"lib/common/reset-search-paths.ts",
|
"lib/common/reset-search-paths.ts",
|
||||||
"lib/renderer/api/crash-reporter.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/exports/electron.js",
|
||||||
"lib/renderer/api/ipc-renderer.js",
|
"lib/renderer/api/ipc-renderer.js",
|
||||||
"lib/renderer/api/module-list.js",
|
"lib/renderer/api/module-list.js",
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
66
lib/browser/desktop-capturer.ts
Normal file
66
lib/browser/desktop-capturer.ts
Normal file
|
@ -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<ElectronInternal.GetSourcesResult[]>;
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
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<ElectronInternal.GetSourcesResult[]>((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
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ const moduleList = require('@electron/internal/common/api/module-list')
|
||||||
|
|
||||||
exports.handleESModule = (loader) => () => {
|
exports.handleESModule = (loader) => () => {
|
||||||
const value = loader()
|
const value = loader()
|
||||||
if (value.__esModule) return value.default
|
if (value.__esModule && value.default) return value.default
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
33
lib/renderer/api/desktop-capturer.ts
Normal file
33
lib/renderer/api/desktop-capturer.ts
Normal file
|
@ -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<ElectronInternal.GetSourcesResult[]>('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
|
||||||
|
}))
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ const moduleList = require('@electron/internal/sandboxed_renderer/api/module-lis
|
||||||
|
|
||||||
const handleESModule = (m) => {
|
const handleESModule = (m) => {
|
||||||
// Handle Typescript modules with an "export default X" statement
|
// 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
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
typings/internal-ambient.d.ts
vendored
2
typings/internal-ambient.d.ts
vendored
|
@ -19,6 +19,7 @@ declare namespace NodeJS {
|
||||||
deleteHiddenValue(obj: any, key: string): void;
|
deleteHiddenValue(obj: any, key: string): void;
|
||||||
requestGarbageCollectionForTesting(): void;
|
requestGarbageCollectionForTesting(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Process {
|
interface Process {
|
||||||
/**
|
/**
|
||||||
* DO NOT USE DIRECTLY, USE process.electronBinding
|
* DO NOT USE DIRECTLY, USE process.electronBinding
|
||||||
|
@ -29,6 +30,7 @@ declare namespace NodeJS {
|
||||||
electronBinding(name: 'v8_util'): V8UtilBinding;
|
electronBinding(name: 'v8_util'): V8UtilBinding;
|
||||||
electronBinding(name: 'app'): { app: Electron.App, App: Function };
|
electronBinding(name: 'app'): { app: Electron.App, App: Function };
|
||||||
electronBinding(name: 'command_line'): Electron.CommandLine;
|
electronBinding(name: 'command_line'): Electron.CommandLine;
|
||||||
|
electronBinding(name: 'desktop_capturer'): { createDesktopCapturer(): ElectronInternal.DesktopCapturer };
|
||||||
log: NodeJS.WriteStream['write'];
|
log: NodeJS.WriteStream['write'];
|
||||||
activateUvLoop(): void;
|
activateUvLoop(): void;
|
||||||
|
|
||||||
|
|
25
typings/internal-electron.d.ts
vendored
25
typings/internal-electron.d.ts
vendored
|
@ -47,11 +47,6 @@ declare namespace Electron {
|
||||||
allFrames: boolean
|
allFrames: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RendererProcessPreference {
|
|
||||||
contentScripts: Array<ContentScript>
|
|
||||||
extensionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IpcRendererInternal extends Electron.IpcRenderer {
|
interface IpcRendererInternal extends Electron.IpcRenderer {
|
||||||
sendToAll(webContentsId: number, channel: string, ...args: any[]): void
|
sendToAll(webContentsId: number, channel: string, ...args: any[]): void
|
||||||
}
|
}
|
||||||
|
@ -88,6 +83,26 @@ declare namespace ElectronInternal {
|
||||||
promisifyMultiArg<T extends (...args: any[]) => any>(fn: T, /*convertPromiseValue: (v: any) => any*/): T;
|
promisifyMultiArg<T extends (...args: any[]) => 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
|
// Internal IPC has _replyInternal and NO reply method
|
||||||
interface IpcMainInternalEvent extends Omit<Electron.IpcMainEvent, 'reply'> {
|
interface IpcMainInternalEvent extends Omit<Electron.IpcMainEvent, 'reply'> {
|
||||||
_replyInternal(...args: any[]): void;
|
_replyInternal(...args: any[]): void;
|
||||||
|
|
Loading…
Reference in a new issue