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/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",
|
||||
|
|
|
@ -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) => () => {
|
||||
const value = loader()
|
||||
if (value.__esModule) return value.default
|
||||
if (value.__esModule && value.default) return value.default
|
||||
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) => {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
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;
|
||||
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;
|
||||
|
||||
|
|
25
typings/internal-electron.d.ts
vendored
25
typings/internal-electron.d.ts
vendored
|
@ -47,11 +47,6 @@ declare namespace Electron {
|
|||
allFrames: boolean
|
||||
}
|
||||
|
||||
interface RendererProcessPreference {
|
||||
contentScripts: Array<ContentScript>
|
||||
extensionId: string;
|
||||
}
|
||||
|
||||
interface IpcRendererInternal extends Electron.IpcRenderer {
|
||||
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;
|
||||
}
|
||||
|
||||
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<Electron.IpcMainEvent, 'reply'> {
|
||||
_replyInternal(...args: any[]): void;
|
||||
|
|
Loading…
Reference in a new issue