mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(desktop): reuse window capture items on Windows 10 (#3449)
This commit is contained in:
1 parent
d514501d57
commit
8cac3b35c9
14 files changed
+824
-6
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
} from '@electron/main/DisplayMediaValidation';
|
||||
import {isWaylandSession} from '@electron/main/LinuxSession';
|
||||
import {startWindowsScreenCaptureGuardForSource} from '@electron/main/WindowsScreenCaptureGuard';
|
||||
import {reuseWindowsCaptureItemsForSource} from '@electron/main/WindowsWindowCaptureItemReuse';
|
||||
import {BrowserWindow, desktopCapturer, ipcMain, screen} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
@@ -448,6 +449,7 @@ export function registerDisplayMediaHandlers(): void {
|
||||
withAudio: withAudio === true,
|
||||
});
|
||||
startWindowsScreenCaptureGuardForSource(selectedSource, event.sender);
|
||||
reuseWindowsCaptureItemsForSource(selectedSource);
|
||||
const streams: Electron.Streams = {
|
||||
video: selectedSource,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {createRequire} from 'node:module';
|
||||
import os from 'node:os';
|
||||
import {stopWindowsScreenCaptureGuard} from '@electron/main/WindowsScreenCaptureGuard';
|
||||
import {reportWindowsCaptureItemReuse} from '@electron/main/WindowsWindowCaptureItemReuse';
|
||||
import {app, powerSaveBlocker} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
@@ -631,6 +632,7 @@ export function releaseStreamingPriority(): void {
|
||||
restoreProcessPriority();
|
||||
restoreGpuSchedulingPriority();
|
||||
stopWindowsScreenCaptureGuard('streaming-priority-release');
|
||||
reportWindowsCaptureItemReuse('streaming-priority-release');
|
||||
log.info('[StreamingPriority] Released');
|
||||
}
|
||||
}
|
||||
@@ -645,5 +647,6 @@ export function resetStreamingPriority(): void {
|
||||
restoreProcessPriority();
|
||||
restoreGpuSchedulingPriority();
|
||||
stopWindowsScreenCaptureGuard('streaming-priority-reset');
|
||||
reportWindowsCaptureItemReuse('streaming-priority-reset');
|
||||
log.info('[StreamingPriority] Reset');
|
||||
}
|
||||
@@ -21,7 +21,7 @@ function isScreenSource(source: Electron.DesktopCapturerSource): boolean {
|
||||
return source.id.startsWith('screen:');
|
||||
}
|
||||
|
||||
function getWindowsBuildNumber(): number | null {
|
||||
export function getWindowsBuildNumber(): number | null {
|
||||
const build = Number.parseInt(os.release().split('.')[2] ?? '', 10);
|
||||
return Number.isSafeInteger(build) && build > 0 ? build : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import {describe, test} from 'node:test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
function transform(fileName) {
|
||||
const sourcePath = fileURLToPath(new URL(fileName, import.meta.url));
|
||||
return esbuild.transformSync(readFileSync(sourcePath, 'utf8'), {
|
||||
loader: 'ts',
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
logLevel: 'silent',
|
||||
}).code;
|
||||
}
|
||||
|
||||
const reuseSource = transform('./WindowsWindowCaptureItemReuse.ts');
|
||||
const nativeAddonSource = transform('./NativeAddon.ts');
|
||||
|
||||
function loadReuse({platform = 'win32', windowsBuild = 19045, addon}) {
|
||||
const logged = [];
|
||||
const calls = {install: 0, loads: 0};
|
||||
const counts = {forwarded: 0, reused: 0};
|
||||
const fakeAddon = addon ?? {
|
||||
loadError: null,
|
||||
reuseWindowCaptureItems: () => {
|
||||
calls.install += 1;
|
||||
return true;
|
||||
},
|
||||
windowCaptureItemCalls: () => ({...counts}),
|
||||
};
|
||||
const context = vm.createContext({console, process: {platform, argv: []}});
|
||||
const load = (source) => {
|
||||
const module = {exports: {}};
|
||||
vm.runInContext(`(function (module, exports, require) {${source}\n})`, context)(
|
||||
module,
|
||||
module.exports,
|
||||
fakeRequire,
|
||||
);
|
||||
return module.exports;
|
||||
};
|
||||
const fakeRequire = (specifier) => {
|
||||
if (specifier === 'node:module') {
|
||||
return {
|
||||
createRequire: () => (name) => {
|
||||
assert.equal(name, '@fluxer/win-game-capture');
|
||||
calls.loads += 1;
|
||||
return fakeAddon;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (specifier === '@electron/main/NativeAddon') return load(nativeAddonSource);
|
||||
if (specifier === '@electron/main/LaunchOptions') return {isSafeModeLaunch: () => false};
|
||||
if (specifier === '@electron/main/WindowsScreenCaptureGuard') return {getWindowsBuildNumber: () => windowsBuild};
|
||||
if (specifier === 'electron-log') {
|
||||
const record =
|
||||
(level) =>
|
||||
(...args) =>
|
||||
logged.push([level, ...args]);
|
||||
return {info: record('info'), warn: record('warn')};
|
||||
}
|
||||
throw new Error(`unexpected require ${specifier}`);
|
||||
};
|
||||
return {...load(reuseSource), calls, counts, logged};
|
||||
}
|
||||
|
||||
const windowSource = {id: 'window:1234:0', name: 'Notepad'};
|
||||
|
||||
describe('Windows window capture item reuse', () => {
|
||||
test('installs once, only for window shares on Windows 10 2004 to 22H2', () => {
|
||||
const cases = [
|
||||
[17763, 0],
|
||||
[18362, 0],
|
||||
[19040, 0],
|
||||
[19041, 1],
|
||||
[19044, 1],
|
||||
[19045, 1],
|
||||
[19046, 0],
|
||||
[20348, 0],
|
||||
[22000, 0],
|
||||
[26100, 0],
|
||||
[null, 0],
|
||||
];
|
||||
for (const [windowsBuild, installs] of cases) {
|
||||
const reuse = loadReuse({windowsBuild});
|
||||
reuse.reuseWindowsCaptureItemsForSource({id: 'screen:0:0', name: 'Screen 1'});
|
||||
assert.equal(reuse.calls.loads, 0, `build ${windowsBuild} screen share`);
|
||||
reuse.reuseWindowsCaptureItemsForSource(windowSource);
|
||||
reuse.reuseWindowsCaptureItemsForSource({id: 'window:5678:0', name: 'Explorer'});
|
||||
assert.equal(reuse.calls.install, installs, `build ${windowsBuild}`);
|
||||
assert.equal(reuse.calls.loads, installs, `build ${windowsBuild}`);
|
||||
}
|
||||
for (const platform of ['darwin', 'linux']) {
|
||||
const reuse = loadReuse({platform});
|
||||
reuse.reuseWindowsCaptureItemsForSource(windowSource);
|
||||
assert.equal(reuse.calls.loads, 0, platform);
|
||||
}
|
||||
});
|
||||
|
||||
test('reports the capture item requests made since the previous report', () => {
|
||||
const reuse = loadReuse({});
|
||||
reuse.reuseWindowsCaptureItemsForSource(windowSource);
|
||||
Object.assign(reuse.counts, {forwarded: 3, reused: 53999});
|
||||
reuse.reportWindowsCaptureItemReuse('streaming-priority-release');
|
||||
reuse.reportWindowsCaptureItemReuse('streaming-priority-reset');
|
||||
Object.assign(reuse.counts, {forwarded: 6, reused: 54009});
|
||||
reuse.reportWindowsCaptureItemReuse('streaming-priority-release');
|
||||
const reports = reuse.logged
|
||||
.filter(([, message]) => message.includes('since the last report'))
|
||||
.map(([, , fields]) => ({...fields}));
|
||||
assert.deepEqual(reports, [
|
||||
{reason: 'streaming-priority-release', forwardedToCaptureService: 3, reusedFromLiveShare: 53999},
|
||||
{reason: 'streaming-priority-release', forwardedToCaptureService: 3, reusedFromLiveShare: 10},
|
||||
]);
|
||||
});
|
||||
|
||||
test('a failing addon never throws into the share request', () => {
|
||||
const reuse = loadReuse({
|
||||
addon: {
|
||||
loadError: null,
|
||||
reuseWindowCaptureItems: () => {
|
||||
throw new Error('binding exploded');
|
||||
},
|
||||
windowCaptureItemCalls: () => null,
|
||||
},
|
||||
});
|
||||
assert.doesNotThrow(() => reuse.reuseWindowsCaptureItemsForSource(windowSource));
|
||||
assert.doesNotThrow(() => reuse.reuseWindowsCaptureItemsForSource(windowSource));
|
||||
assert.doesNotThrow(() => reuse.reportWindowsCaptureItemReuse('streaming-priority-release'));
|
||||
assert.equal(reuse.logged.filter(([level]) => level === 'warn').length, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createRequire} from 'node:module';
|
||||
import {isSafeModeLaunch} from '@electron/main/LaunchOptions';
|
||||
import {loadOptionalNativeAddon} from '@electron/main/NativeAddon';
|
||||
import {getWindowsBuildNumber} from '@electron/main/WindowsScreenCaptureGuard';
|
||||
import log from 'electron-log';
|
||||
|
||||
const WINDOWS_10_2004_BUILD = 19041;
|
||||
const WINDOWS_10_22H2_BUILD = 19045;
|
||||
|
||||
const requireModule = createRequire(import.meta.url);
|
||||
|
||||
interface WindowCaptureItemCalls {
|
||||
forwarded: number;
|
||||
reused: number;
|
||||
}
|
||||
|
||||
interface WindowCaptureItemReuseModule {
|
||||
reuseWindowCaptureItems: () => boolean;
|
||||
windowCaptureItemCalls: () => WindowCaptureItemCalls | null;
|
||||
loadError?: Error | null;
|
||||
}
|
||||
|
||||
const loadWindowCaptureItemReuseModule = loadOptionalNativeAddon<WindowCaptureItemReuseModule>({
|
||||
platform: 'win32',
|
||||
disabled: () => isSafeModeLaunch(),
|
||||
load: () => requireModule('@fluxer/win-game-capture'),
|
||||
logger: log,
|
||||
level: 'warn',
|
||||
unavailableMessage:
|
||||
'[WindowsWindowCaptureItemReuse] Failed to load @fluxer/win-game-capture; window shares create a capture item per frame',
|
||||
disabledMessage:
|
||||
'[WindowsWindowCaptureItemReuse] Disabled by safe mode; window shares create a capture item per frame',
|
||||
});
|
||||
|
||||
let reuseRequested = false;
|
||||
let reusingModule: WindowCaptureItemReuseModule | null = null;
|
||||
let reportedCalls: WindowCaptureItemCalls = {forwarded: 0, reused: 0};
|
||||
|
||||
function reusesCaptureItemsOnWindowsBuild(windowsBuild: number | null): windowsBuild is number {
|
||||
if (windowsBuild === null) return false;
|
||||
return windowsBuild >= WINDOWS_10_2004_BUILD && windowsBuild <= WINDOWS_10_22H2_BUILD;
|
||||
}
|
||||
|
||||
export function reportWindowsCaptureItemReuse(reason: string): void {
|
||||
const calls = reusingModule?.windowCaptureItemCalls();
|
||||
if (!calls) return;
|
||||
const forwarded = calls.forwarded - reportedCalls.forwarded;
|
||||
const reused = calls.reused - reportedCalls.reused;
|
||||
if (forwarded === 0 && reused === 0) return;
|
||||
reportedCalls = calls;
|
||||
log.info('[WindowsWindowCaptureItemReuse] Capture item requests since the last report', {
|
||||
reason,
|
||||
forwardedToCaptureService: forwarded,
|
||||
reusedFromLiveShare: reused,
|
||||
});
|
||||
}
|
||||
|
||||
export function reuseWindowsCaptureItemsForSource(source: Electron.DesktopCapturerSource): void {
|
||||
try {
|
||||
reuseCaptureItemsForSource(source);
|
||||
} catch (error) {
|
||||
log.warn('[WindowsWindowCaptureItemReuse] Window shares create a capture item per frame', {error});
|
||||
}
|
||||
}
|
||||
|
||||
function reuseCaptureItemsForSource(source: Electron.DesktopCapturerSource): void {
|
||||
if (process.platform !== 'win32') return;
|
||||
if (!source.id.startsWith('window:')) return;
|
||||
if (reuseRequested) {
|
||||
reportWindowsCaptureItemReuse('window-share-start');
|
||||
return;
|
||||
}
|
||||
const windowsBuild = getWindowsBuildNumber();
|
||||
if (!reusesCaptureItemsOnWindowsBuild(windowsBuild)) return;
|
||||
reuseRequested = true;
|
||||
const addon = loadWindowCaptureItemReuseModule();
|
||||
if (!addon) return;
|
||||
if (!addon.reuseWindowCaptureItems()) {
|
||||
log.warn('[WindowsWindowCaptureItemReuse] Window shares create a capture item per frame', {windowsBuild});
|
||||
return;
|
||||
}
|
||||
reusingModule = addon;
|
||||
log.info('[WindowsWindowCaptureItemReuse] Window shares ask for their live capture item first', {windowsBuild});
|
||||
}
|
||||
Reference in new issue
Block a user