feat(voice): rework screen share delivery behind an experiment (#2859)

This commit is contained in:
Hampus
2026-09-20 06:10:20 +02:00
committed by GitHub
parent 574a93257c
commit c157ab5752
121 changed files with 7205 additions and 5502 deletions
-10
View File
@@ -71,14 +71,6 @@ export interface GpuInfo {
export type StreamingPriorityDiagnostics = Record<string, unknown>;
export interface OpenH264Status {
enabled: boolean;
downloaded: boolean;
downloading: boolean;
version: string | null;
error: string | null;
}
export interface CpuInfo {
model: string;
speed: number;
@@ -640,8 +632,6 @@ export interface ElectronAPI {
buildChannel: 'stable' | 'canary';
getDesktopInfo: () => Promise<DesktopInfo>;
getGpuInfo: () => Promise<GpuInfo>;
getOpenH264Status: () => Promise<OpenH264Status>;
setOpenH264Enabled: (enabled: boolean) => Promise<OpenH264Status>;
getDesktopWindowBehaviorSettings: () => Promise<DesktopWindowBehaviorSettings>;
setDesktopWindowBehaviorSettings: (
settings: Partial<DesktopWindowBehaviorSettings>,
@@ -57,6 +57,17 @@ function loadChromiumRuntime(platform = 'win32') {
return {appendedSwitches, module: module.exports};
}
function collectChromiumFeatureNames(platform) {
const {module} = loadChromiumRuntime(platform);
const features = new Set(module.BASE_DISABLED_CHROMIUM_FEATURES);
for (const [name, value] of Object.entries(module)) {
if (typeof value !== 'function') continue;
if (!name.startsWith('add') || !name.endsWith('Features')) continue;
value(features);
}
return [...features];
}
describe('ChromiumRuntime Windows capture policy', () => {
test('leaves the choice of Windows graphics capture to Chromium', () => {
const {module} = loadChromiumRuntime('win32');
@@ -75,6 +86,35 @@ describe('ChromiumRuntime Windows capture policy', () => {
});
});
describe('ChromiumRuntime macOS capture policy', () => {
test('leaves the choice of macOS screen capture device to Chromium', () => {
assert.deepEqual(
collectChromiumFeatureNames('darwin').filter(
(feature) => feature.includes('ScreenCaptureKit') || feature.includes('SCContentSharingPicker'),
),
[],
);
});
test('exposes no macOS release-gated screen capture override', () => {
const {module} = loadChromiumRuntime('darwin');
assert.deepEqual(
Object.keys(module).filter((name) => name.includes('ScreenCapture')),
[],
);
});
});
describe('ChromiumRuntime Linux capture policy', () => {
test('leaves the choice of PipeWire screen capture to Chromium', () => {
assert.deepEqual(
collectChromiumFeatureNames('linux').filter((feature) => feature.includes('PipeWire')),
[],
);
});
});
describe('ChromiumRuntime configured switch allowlist', () => {
test('rejects settings-supplied switches that would disable or shrink the HTTP cache', () => {
const {appendedSwitches, module} = loadChromiumRuntime('win32');
@@ -27,15 +27,6 @@ const RUNTIME_CACHE_DIRECTORIES = [
'Service Worker/ScriptCache',
'Default/Service Worker/ScriptCache',
];
const MACOS_PRE_SEQUOIA_SCREEN_CAPTURE_DISABLED_FEATURES = [
'ScreenCaptureKitMac',
'ScreenCaptureKitMacWindow',
'ScreenCaptureKitMacScreen',
'ScreenCaptureKitPickerScreen',
'ScreenCaptureKitStreamPickerSonoma',
'WarmScreenCaptureSonoma',
'UseSCContentSharingPicker',
];
const WINDOWS_NVIDIA_HEVC_DECODE_WORKAROUND_DEVICE_IDS = new Set([
4928, 4929, 4932, 4934, 4935, 4936, 4937, 4939, 4941, 4942, 4943, 4986, 4987, 4992, 4993, 4994, 5008, 5009, 5010,
5011, 5016, 5017, 5018, 5019, 5020, 5021, 5040, 5041, 5042, 5043, 5044, 5046, 5049, 5050, 5051, 5052, 5056, 5058,
@@ -296,15 +287,6 @@ export function appendLinuxChromiumFlagsConfig(channel: 'stable' | 'canary'): vo
}
}
export function addMacosPreSequoiaScreenCaptureDisabledFeatures(features: Set<string>): void {
if (process.platform !== 'darwin') return;
const majorRelease = Number.parseInt(os.release().split('.')[0] ?? '', 10);
if (!Number.isFinite(majorRelease) || majorRelease >= 24) return;
for (const feature of MACOS_PRE_SEQUOIA_SCREEN_CAPTURE_DISABLED_FEATURES) {
features.add(feature);
}
}
export function addLinuxHardwareVideoEncodeFeatures(features: Set<string>): void {
if (process.platform !== 'linux') return;
features.add('AcceleratedVideoEncoder');
@@ -316,11 +298,6 @@ export interface ChromiumCommandLine {
hasSwitch(name: string): boolean;
}
export function addLinuxScreenCapturePipeWireFeature(features: Set<string>): void {
if (process.platform !== 'linux') return;
features.add('WebRTCPipeWireCapturer');
}
export function hasConfiguredOzonePlatformSwitch(commandLine: ChromiumCommandLine = app.commandLine): boolean {
return commandLine.hasSwitch('ozone-platform') || commandLine.getSwitchValue('ozone-platform').length > 0;
}
+1 -1
View File
@@ -461,7 +461,7 @@ export function registerDisplayMediaHandlers(): void {
name: selectedSource.name,
withAudio: withAudio === true,
});
startWindowsScreenCaptureGuardForSource(selectedSource);
startWindowsScreenCaptureGuardForSource(selectedSource, event.sender);
const _attachAudio = shouldHonorSelectedAudio(pending.audioRequested, withAudio);
const streams: Electron.Streams = {
video: selectedSource,
-3
View File
@@ -32,7 +32,6 @@ import {setNativeStrings} from '@electron/main/MainI18n';
import {copyRemoteFileToClipboard, parseClipboardWriteFileOptions} from '@electron/main/MediaClipboard';
import {registerNotificationIpcHandlers} from '@electron/main/NotificationsIpc';
import {openExternalDeduped} from '@electron/main/OpenExternal';
import {getStatus as getOpenH264Status, setEnabled as setOpenH264Enabled} from '@electron/main/OpenH264Manager';
import {registerPasskeyHandlers} from '@electron/main/Passkeys';
import {getAppMetricsSnapshot, getDesktopInfo, getGpuInfo} from '@electron/main/PlatformInfo';
import {requirePrivilegedRendererDocumentSender} from '@electron/main/PrivilegedRendererDocuments';
@@ -140,8 +139,6 @@ export function registerIpcHandlers(): void {
ipcMain.handle('get-desktop-info', () => getDesktopInfo());
ipcMain.handle('get-gpu-info', () => getGpuInfo());
ipcMain.handle('get-app-metrics', () => getAppMetricsSnapshot());
ipcMain.handle('get-openh264-status', () => getOpenH264Status());
ipcMain.handle('set-openh264-enabled', (_event, enabled: unknown) => setOpenH264Enabled(Boolean(enabled)));
ipcMain.handle('streamer-mode:get-capture-app-status', () => getStreamerModeCaptureAppStatus());
ipcMain.handle('system-idle-time-ms', (): number => {
return Math.max(0, powerMonitor.getSystemIdleTime() * 1000);
-236
View File
@@ -1,236 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFile} from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import type {OpenH264Status} from '@electron/common/Types';
import {downloadFile} from '@electron/main/FileDownloads';
import {app} from 'electron';
import log from 'electron-log';
const OPENH264_VERSION = '2.5.1';
const CISCO_CDN_BASE = 'https://ciscobinary.openh264.org';
const PREFERENCES_FILE = 'preferences.json';
const STATUS_FILE = 'status.json';
const DECOMPRESS_TIMEOUT_MS = 30_000;
interface PersistedPreferences {
enabled?: boolean;
}
interface PersistedStatus {
version: string;
binaryPath: string;
downloadedAt: string;
}
function getOpenH264Dir(): string {
return path.join(app.getPath('userData'), 'openh264');
}
function getStatusFilePath(): string {
return path.join(getOpenH264Dir(), STATUS_FILE);
}
function getPreferencesFilePath(): string {
return path.join(getOpenH264Dir(), PREFERENCES_FILE);
}
function getBinaryFilename(): string | null {
if (process.platform !== 'linux') return null;
return `libopenh264-${OPENH264_VERSION}-linux64.7.so`;
}
function getDownloadUrl(): string | null {
const filename = getBinaryFilename();
if (!filename) return null;
return `${CISCO_CDN_BASE}/${filename}.bz2`;
}
function _getBinaryPath(): string | null {
const filename = getBinaryFilename();
if (!filename) return null;
return path.join(getOpenH264Dir(), filename);
}
function readPersistedStatus(): PersistedStatus | null {
try {
const data = JSON.parse(fs.readFileSync(getStatusFilePath(), 'utf8')) as Partial<PersistedStatus>;
if (typeof data.version === 'string' && typeof data.binaryPath === 'string') {
return data as PersistedStatus;
}
return null;
} catch {
return null;
}
}
function readPersistedEnabled(): boolean {
try {
const data = JSON.parse(fs.readFileSync(getPreferencesFilePath(), 'utf8')) as PersistedPreferences;
return typeof data.enabled === 'boolean' ? data.enabled : true;
} catch {
return true;
}
}
function writePersistedEnabled(value: boolean): void {
try {
fs.mkdirSync(getOpenH264Dir(), {recursive: true});
fs.writeFileSync(getPreferencesFilePath(), JSON.stringify({enabled: value}, null, 2), 'utf8');
} catch (error) {
log.warn('[OpenH264] Failed to write preferences file', {error});
}
}
function writePersistedStatus(status: PersistedStatus): void {
try {
fs.mkdirSync(getOpenH264Dir(), {recursive: true});
fs.writeFileSync(getStatusFilePath(), JSON.stringify(status, null, 2), 'utf8');
} catch (error) {
log.warn('[OpenH264] Failed to write status file', {error});
}
}
function binaryExists(): boolean {
const persisted = readPersistedStatus();
if (!persisted) return false;
if (persisted.version !== OPENH264_VERSION) return false;
try {
fs.accessSync(persisted.binaryPath, fs.constants.R_OK);
return true;
} catch {
return false;
}
}
function getBinaryPathIfReady(): string | null {
const persisted = readPersistedStatus();
if (!persisted) return null;
if (persisted.version !== OPENH264_VERSION) return null;
try {
fs.accessSync(persisted.binaryPath, fs.constants.R_OK);
return persisted.binaryPath;
} catch {
return null;
}
}
function decompressBz2(compressedPath: string): Promise<void> {
return new Promise((resolve, reject) => {
execFile('bunzip2', ['--keep', '--force', compressedPath], {timeout: DECOMPRESS_TIMEOUT_MS}, (error) => {
if (error) {
reject(new Error(`bunzip2 failed: ${error.message}`));
return;
}
resolve();
});
});
}
let enabled: boolean | null = null;
let downloading = false;
let lastError: string | null = null;
function getEnabled(): boolean {
if (enabled === null) {
enabled = readPersistedEnabled();
}
return enabled;
}
async function downloadOpenH264(): Promise<void> {
const url = getDownloadUrl();
const filename = getBinaryFilename();
if (!url || !filename) {
lastError = 'Unsupported platform';
return;
}
const dir = getOpenH264Dir();
const compressedPath = path.join(dir, `${filename}.bz2`);
const binaryPath = path.join(dir, filename);
downloading = true;
lastError = null;
try {
fs.mkdirSync(dir, {recursive: true});
log.info('[OpenH264] Downloading from Cisco CDN', {url, version: OPENH264_VERSION});
await downloadFile(url, compressedPath);
log.info('[OpenH264] Decompressing', {compressedPath});
await decompressBz2(compressedPath);
if (fs.existsSync(binaryPath)) {
writePersistedStatus({
version: OPENH264_VERSION,
binaryPath,
downloadedAt: new Date().toISOString(),
});
log.info('[OpenH264] Binary ready', {binaryPath, version: OPENH264_VERSION});
} else {
lastError = 'Decompression produced no output file';
log.warn('[OpenH264] Binary not found after decompression', {binaryPath});
}
try {
fs.unlinkSync(compressedPath);
} catch {}
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
log.warn('[OpenH264] Download failed', {error: lastError});
} finally {
downloading = false;
}
}
export function getStatus(): OpenH264Status {
if (process.platform !== 'linux') {
return {
enabled: false,
downloaded: false,
downloading: false,
version: null,
error: null,
};
}
const binaryPath = getBinaryPathIfReady();
return {
enabled: getEnabled(),
downloaded: binaryPath !== null,
downloading,
version: binaryPath !== null ? OPENH264_VERSION : null,
error: lastError,
};
}
export async function setEnabled(value: boolean): Promise<OpenH264Status> {
if (process.platform !== 'linux') {
enabled = false;
return getStatus();
}
enabled = value;
writePersistedEnabled(value);
if (getEnabled() && !binaryExists() && !downloading) {
void downloadOpenH264();
}
return getStatus();
}
export function initOpenH264(): void {
if (process.platform !== 'linux') return;
if (getEnabled() && !binaryExists()) {
void downloadOpenH264();
}
}
export function appendOpenH264Switches(): void {
if (process.platform !== 'linux' || !getEnabled()) return;
const binaryPath = getBinaryPathIfReady();
if (!binaryPath) return;
app.commandLine.appendSwitch('enable-libopenh264');
app.commandLine.appendSwitch('openh264-library-path', binaryPath);
log.info('[OpenH264] Chromium OpenH264 library path configured', {binaryPath, version: OPENH264_VERSION});
}
@@ -87,7 +87,7 @@ describe('PlatformInfo Chromium runtime diagnostics', () => {
switches.filter((name) => name.startsWith('enable-h264-mf')),
[],
);
assert.equal(switches.includes('enable-libopenh264'), true);
assert.equal(switches.includes('disable_accelerated_h264_encode'), true);
});
});
-2
View File
@@ -52,10 +52,8 @@ const CHROMIUM_RUNTIME_SWITCHES = [
'disable_metal',
'disable-renderer-backgrounding',
'disable_nv12_dxgi_video',
'enable-libopenh264',
'force_high_performance_gpu',
'force_low_power_gpu',
'openh264-library-path',
];
interface DesktopInfoOptions {
@@ -53,7 +53,6 @@ function loadStreamingPriority({
clearedIntervals: [],
powerStart: [],
powerStop: [],
guardRetain: 0,
guardStop: [],
setPriority: [],
nativeModuleImports: [],
@@ -84,8 +83,8 @@ function loadStreamingPriority({
const osModule = {
constants: {priority: {PRIORITY_ABOVE_NORMAL: -7}},
getPriority: () => currentPriority,
setPriority(priority) {
calls.setPriority.push(priority);
setPriority(pid, priority) {
calls.setPriority.push({processId: pid, priority});
},
};
const log = {
@@ -94,9 +93,6 @@ function loadStreamingPriority({
warn: (...args) => calls.logs.warn.push(args),
};
const guard = {
retainWindowsScreenCaptureGuard() {
calls.guardRetain += 1;
},
stopWindowsScreenCaptureGuard(reason) {
calls.guardStop.push(reason);
},
@@ -167,7 +163,10 @@ describe('StreamingPriority GPU scheduling priority', () => {
{processId: 3002, priorityClass: 'high'},
{processId: 3003, priorityClass: 'high'},
]);
assert.deepEqual(calls.setPriority, [-7]);
assert.deepEqual(calls.setPriority, [
{processId: 1000, priority: -7},
{processId: 2001, priority: -7},
]);
assert.deepEqual(webContents.backgroundThrottlingAllowed, [false]);
assert.equal(calls.intervals.length, 1);
assert.equal(calls.intervals[0].delay, 20000);
@@ -180,6 +179,10 @@ describe('StreamingPriority GPU scheduling priority', () => {
streamingPriority: -7,
savedPriority: 0,
elevated: true,
elevatedProcesses: [
{processId: 1000, savedPriority: 0},
{processId: 2001, savedPriority: 0},
],
},
gpuScheduling: {
supported: true,
@@ -234,7 +237,12 @@ describe('StreamingPriority GPU scheduling priority', () => {
{processId: 3002},
{processId: 3003},
]);
assert.deepEqual(calls.setPriority, [-7, 0]);
assert.deepEqual(calls.setPriority, [
{processId: 1000, priority: -7},
{processId: 2001, priority: -7},
{processId: 1000, priority: 0},
{processId: 2001, priority: 0},
]);
assert.deepEqual(normalize(module.getStreamingPriorityDiagnostics().gpuScheduling.elevatedProcesses), []);
assert.deepEqual(normalize(module.getStreamingPriorityDiagnostics().gpuScheduling.lastRestore), {
status: 'succeeded',
@@ -332,6 +340,28 @@ describe('StreamingPriority GPU scheduling priority', () => {
assert.equal(module.getStreamingPriorityDiagnostics().processPriority.savedPriority, null);
});
test('forgets a renderer whose process is gone instead of rewriting a dead pid', () => {
const webContents = makeWebContents(2001);
const {calls, module} = loadStreamingPriority();
module.acquireStreamingPriority(webContents);
assert.deepEqual(calls.setPriority, [
{processId: 1000, priority: -7},
{processId: 2001, priority: -7},
]);
webContents.emit('render-process-gone');
module.releaseStreamingPriority();
assert.deepEqual(calls.setPriority, [
{processId: 1000, priority: -7},
{processId: 2001, priority: -7},
{processId: 1000, priority: 0},
]);
assert.deepEqual(calls.restore, [{processId: 1000}]);
});
test('records the real native module load failure detail', () => {
const webContents = makeWebContents(2001);
const {calls, module} = loadStreamingPriority({
+60 -29
View File
@@ -2,7 +2,7 @@
import {createRequire} from 'node:module';
import os from 'node:os';
import {retainWindowsScreenCaptureGuard, stopWindowsScreenCaptureGuard} from '@electron/main/WindowsScreenCaptureGuard';
import {stopWindowsScreenCaptureGuard} from '@electron/main/WindowsScreenCaptureGuard';
import {app, powerSaveBlocker} from 'electron';
import log from 'electron-log';
@@ -24,6 +24,12 @@ type WindowsGpuPriorityTargetReason =
| 'chromium-video-encode'
| 'chromium-media-utility';
const PROCESS_PRIORITY_TARGET_REASONS: ReadonlySet<WindowsGpuPriorityTargetReason> = new Set([
'browser-main',
'renderer',
'tracked-renderer',
]);
type GpuPriorityModuleStatus = 'disabled' | 'unsupported-platform' | 'not-loaded' | 'loaded' | 'unavailable';
type GpuPriorityAttemptStatus =
| 'disabled'
@@ -80,6 +86,7 @@ export interface StreamingPriorityDiagnostics {
streamingPriority: number;
savedPriority: number | null;
elevated: boolean;
elevatedProcesses: Array<{processId: number; savedPriority: number}>;
};
gpuScheduling: {
supported: boolean;
@@ -98,7 +105,7 @@ export interface StreamingPriorityDiagnostics {
let refCount = 0;
let powerBlockerId: number | null = null;
let savedPriority: number | null = null;
const savedProcessPriorities = new Map<number, number>();
let gpuPriorityModule: WindowsGpuPriorityModule | null | undefined;
let gpuPriorityModuleLoadErrorDetail: string | null = null;
const gpuPriorityElevatedProcessIds = new Map<number, WindowsGpuSchedulingPriority>();
@@ -165,29 +172,37 @@ function stopPowerSaveBlocker(): void {
powerBlockerId = null;
}
function elevateProcessPriority(): void {
if (!CAN_ELEVATE_PROCESS_PRIORITY) return;
function elevateProcessPriority(processId: number): void {
if (!CAN_ELEVATE_PROCESS_PRIORITY || savedProcessPriorities.has(processId)) return;
try {
savedPriority = os.getPriority();
if (savedPriority > STREAMING_PRIORITY) {
os.setPriority(STREAMING_PRIORITY);
} else {
savedPriority = null;
}
const currentPriority = os.getPriority(processId);
if (currentPriority <= STREAMING_PRIORITY) return;
os.setPriority(processId, STREAMING_PRIORITY);
savedProcessPriorities.set(processId, currentPriority);
} catch (error) {
log.debug('[StreamingPriority] Failed to elevate process priority', {error});
savedPriority = null;
log.debug('[StreamingPriority] Failed to elevate process priority', {processId, error});
}
}
function elevateProcessPriorityTargets(targets: Array<GpuPriorityTarget>): void {
if (!CAN_ELEVATE_PROCESS_PRIORITY) return;
for (const target of targets) {
if (!target.reasons.some((reason) => PROCESS_PRIORITY_TARGET_REASONS.has(reason))) continue;
elevateProcessPriority(target.processId);
}
}
function restoreProcessPriority(): void {
if (savedPriority === null) return;
try {
os.setPriority(savedPriority);
} catch (error) {
log.debug('[StreamingPriority] Failed to restore process priority', {error});
if (savedProcessPriorities.size === 0) return;
const entries = [...savedProcessPriorities.entries()];
savedProcessPriorities.clear();
for (const [processId, priority] of entries) {
try {
os.setPriority(processId, priority);
} catch (error) {
log.debug('[StreamingPriority] Failed to restore process priority', {processId, error});
}
}
savedPriority = null;
}
function loadWindowsGpuPriorityModule(): WindowsGpuPriorityModule | null {
@@ -263,19 +278,30 @@ function restoreBackgroundThrottling(): void {
function rememberStreamingPriorityWebContents(webContents?: Electron.WebContents): void {
if (!webContents || webContents.isDestroyed() || streamingPriorityWebContents.has(webContents)) return;
streamingPriorityWebContents.add(webContents);
const processId = getRendererProcessId(webContents);
const forgetProcessPriority = (): void => {
if (processId === undefined) return;
savedProcessPriorities.delete(processId);
gpuPriorityElevatedProcessIds.delete(processId);
};
const cleanup = (): void => {
streamingPriorityWebContents.delete(webContents);
streamingPriorityWebContentsCleanup.delete(webContents);
backgroundThrottlingBypassedWebContents.delete(webContents);
forgetProcessPriority();
};
streamingPriorityWebContentsCleanup.set(webContents, cleanup);
streamingPriorityWebContentsCleanup.set(webContents, () => {
webContents.removeListener('destroyed', cleanup);
webContents.removeListener('render-process-gone', forgetProcessPriority);
});
webContents.once('destroyed', cleanup);
webContents.once('render-process-gone', forgetProcessPriority);
}
function clearStreamingPriorityWebContents(): void {
for (const [webContents, cleanup] of streamingPriorityWebContentsCleanup) {
for (const [webContents, detachListeners] of streamingPriorityWebContentsCleanup) {
if (webContents && !webContents.isDestroyed()) {
webContents.removeListener('destroyed', cleanup);
detachListeners();
}
}
streamingPriorityWebContents.clear();
@@ -367,7 +393,7 @@ function cloneGpuPriorityRestoreDiagnostics(
};
}
function elevateGpuSchedulingPriority(webContents?: Electron.WebContents): void {
function elevateGpuSchedulingPriority(targets: Array<GpuPriorityTarget>): void {
if (GPU_SCHEDULING_PRIORITY === null) {
lastGpuPriorityAcquire = {
status: 'disabled',
@@ -380,7 +406,6 @@ function elevateGpuSchedulingPriority(webContents?: Electron.WebContents): void
};
return;
}
const targets = collectGpuSchedulingPriorityTargets(webContents);
if (process.platform !== 'win32') {
lastGpuPriorityAcquire = {
status: 'unsupported-platform',
@@ -467,7 +492,9 @@ function elevateGpuSchedulingPriority(webContents?: Electron.WebContents): void
function refreshGpuSchedulingPriority(): void {
if (refCount <= 0) return;
elevateGpuSchedulingPriority();
const targets = collectGpuSchedulingPriorityTargets();
elevateProcessPriorityTargets(targets);
elevateGpuSchedulingPriority(targets);
}
function startGpuPriorityRefresh(): void {
@@ -551,8 +578,12 @@ export function getStreamingPriorityDiagnostics(): StreamingPriorityDiagnostics
processPriority: {
supported: CAN_ELEVATE_PROCESS_PRIORITY,
streamingPriority: STREAMING_PRIORITY,
savedPriority,
elevated: savedPriority !== null,
savedPriority: savedProcessPriorities.get(process.pid) ?? null,
elevated: savedProcessPriorities.size > 0,
elevatedProcesses: [...savedProcessPriorities.entries()].map(([processId, savedPriority]) => ({
processId,
savedPriority,
})),
},
gpuScheduling: {
supported: process.platform === 'win32' && GPU_SCHEDULING_PRIORITY !== null,
@@ -581,12 +612,12 @@ export function acquireStreamingPriority(webContents?: Electron.WebContents): vo
refCount++;
if (refCount === 1) {
startPowerSaveBlocker();
elevateProcessPriority();
retainWindowsScreenCaptureGuard();
startGpuPriorityRefresh();
log.info('[StreamingPriority] Acquired', {refCount});
}
elevateGpuSchedulingPriority(webContents);
const targets = collectGpuSchedulingPriorityTargets(webContents);
elevateProcessPriorityTargets(targets);
elevateGpuSchedulingPriority(targets);
}
export function releaseStreamingPriority(): void {
@@ -1,26 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import os from 'node:os';
import {BrowserWindow, screen} from 'electron';
import log from 'electron-log';
const PENDING_GUARD_TTL_MS = 120000;
const GUARD_SIZE_DIP = 2;
const WGC_SCREEN_CAPTURE_MIN_WINDOWS_BUILD = 26100;
interface ActiveWindowsScreenCaptureGuard {
interface ActiveMonitorCompositionGuard {
window: BrowserWindow;
sourceId: string;
displayId?: string;
retained: boolean;
timeout: NodeJS.Timeout | null;
owner: Electron.WebContents;
onOwnerDestroyed: () => void;
}
let activeGuard: ActiveWindowsScreenCaptureGuard | null = null;
let guardRetentionRequested = false;
let activeGuard: ActiveMonitorCompositionGuard | null = null;
function isScreenSource(source: Electron.DesktopCapturerSource): boolean {
return source.id.startsWith('screen:');
}
function getWindowsBuildNumber(): number | null {
const build = Number.parseInt(os.release().split('.')[2] ?? '', 10);
return Number.isSafeInteger(build) && build > 0 ? build : null;
}
function monitorCaptureUsesDxgiDuplication(): boolean {
const build = getWindowsBuildNumber();
return build === null || build < WGC_SCREEN_CAPTURE_MIN_WINDOWS_BUILD;
}
function parseScreenSourceOrdinal(sourceId: string): number | null {
const match = /^screen:([0-9]+):(?:0|1)$/.exec(sourceId);
if (!match) return null;
@@ -41,31 +51,15 @@ function resolveDisplayForSource(source: Electron.DesktopCapturerSource): Electr
return screen.getPrimaryDisplay();
}
function clearGuardTimeout(guard: ActiveWindowsScreenCaptureGuard): void {
if (guard.timeout) {
clearTimeout(guard.timeout);
guard.timeout = null;
function detachOwnerListener(guard: ActiveMonitorCompositionGuard): void {
try {
guard.owner.removeListener('destroyed', guard.onOwnerDestroyed);
} catch (error) {
log.debug('[WindowsScreenCaptureGuard] Failed to detach capturer listener', {error});
}
}
function armPendingGuardTimeout(guard: ActiveWindowsScreenCaptureGuard): void {
clearGuardTimeout(guard);
guard.timeout = setTimeout(() => {
if (activeGuard !== guard || guard.retained) return;
destroyActiveGuard('pending-timeout');
}, PENDING_GUARD_TTL_MS);
}
function retainGuardIfRequested(guard: ActiveWindowsScreenCaptureGuard): void {
guard.retained = guardRetentionRequested;
if (guard.retained) {
clearGuardTimeout(guard);
} else {
armPendingGuardTimeout(guard);
}
}
function createGuardWindow(display: Electron.Display): BrowserWindow {
function createCompositionGuardWindow(display: Electron.Display): BrowserWindow {
const {x, y, width, height} = display.bounds;
const guardWindow = new BrowserWindow({
x: x + Math.max(0, width - GUARD_SIZE_DIP),
@@ -118,57 +112,67 @@ function createGuardWindow(display: Electron.Display): BrowserWindow {
return guardWindow;
}
export function startWindowsScreenCaptureGuardForSource(source: Electron.DesktopCapturerSource): void {
export function startWindowsScreenCaptureGuardForSource(
source: Electron.DesktopCapturerSource,
owner: Electron.WebContents,
): void {
if (process.platform !== 'win32') return;
if (!isScreenSource(source)) return;
if (activeGuard?.sourceId === source.id && !activeGuard.window.isDestroyed()) {
retainGuardIfRequested(activeGuard);
if (owner.isDestroyed()) return;
if (!monitorCaptureUsesDxgiDuplication()) {
log.info('[WindowsScreenCaptureGuard] Not forcing DWM composition; Chromium captures monitors with WGC here', {
sourceId: source.id,
windowsBuild: getWindowsBuildNumber(),
});
return;
}
if (activeGuard?.sourceId === source.id && activeGuard.owner === owner && !activeGuard.window.isDestroyed()) {
return;
}
destroyActiveGuard('replace');
const display = resolveDisplayForSource(source);
if (!display) return;
try {
const guardWindow = createGuardWindow(display);
const guard: ActiveWindowsScreenCaptureGuard = {
const guardWindow = createCompositionGuardWindow(display);
const onOwnerDestroyed = (): void => {
if (activeGuard?.window === guardWindow) {
destroyActiveGuard('capturer-destroyed');
}
};
const guard: ActiveMonitorCompositionGuard = {
window: guardWindow,
sourceId: source.id,
displayId: source.display_id,
retained: guardRetentionRequested,
timeout: null,
owner,
onOwnerDestroyed,
};
guardWindow.once('closed', () => {
if (activeGuard === guard) {
clearGuardTimeout(guard);
detachOwnerListener(guard);
activeGuard = null;
}
});
owner.once('destroyed', onOwnerDestroyed);
activeGuard = guard;
retainGuardIfRequested(guard);
log.info('[WindowsScreenCaptureGuard] Armed monitor composition guard', {
log.info('[WindowsScreenCaptureGuard] Forcing DWM composition on the captured monitor for DXGI duplication', {
sourceId: source.id,
displayId: source.display_id,
retained: guard.retained,
windowsBuild: getWindowsBuildNumber(),
bounds: display.bounds,
});
} catch (error) {
log.warn('[WindowsScreenCaptureGuard] Failed to arm monitor composition guard', {sourceId: source.id, error});
log.warn('[WindowsScreenCaptureGuard] Failed to force DWM composition on the captured monitor', {
sourceId: source.id,
error,
});
}
}
export function retainWindowsScreenCaptureGuard(): void {
if (process.platform !== 'win32') return;
guardRetentionRequested = true;
if (!activeGuard) return;
activeGuard.retained = true;
clearGuardTimeout(activeGuard);
}
function destroyActiveGuard(reason: string): void {
const guard = activeGuard;
if (!guard) return;
activeGuard = null;
clearGuardTimeout(guard);
detachOwnerListener(guard);
if (!guard.window.isDestroyed()) {
try {
guard.window.destroy();
@@ -176,7 +180,7 @@ function destroyActiveGuard(reason: string): void {
log.debug('[WindowsScreenCaptureGuard] Failed to destroy guard window', {reason, error});
}
}
log.info('[WindowsScreenCaptureGuard] Stopped monitor composition guard', {
log.info('[WindowsScreenCaptureGuard] Stopped forcing DWM composition on the captured monitor', {
reason,
sourceId: guard.sourceId,
displayId: guard.displayId,
@@ -184,6 +188,5 @@ function destroyActiveGuard(reason: string): void {
}
export function stopWindowsScreenCaptureGuard(reason: string): void {
guardRetentionRequested = false;
destroyActiveGuard(reason);
}
-14
View File
@@ -19,8 +19,6 @@ import {configureUserDataPath} from '@electron/common/UserDataPath';
import {registerAutostartHandlers} from '@electron/main/Autostart';
import {
addLinuxHardwareVideoEncodeFeatures,
addLinuxScreenCapturePipeWireFeature,
addMacosPreSequoiaScreenCaptureDisabledFeatures,
addWindowsHardwareVideoEncodeFeatures,
appendConfiguredChromiumSwitches,
appendDisabledChromiumFeatures,
@@ -67,7 +65,6 @@ import {
} from '@electron/main/NativeHardwareEncoder';
import {runNativeModulePreflight} from '@electron/main/NativeModulePreflight';
import {cleanupNativeScreenCapture, registerNativeScreenCaptureHandlers} from '@electron/main/NativeScreenCapture';
import {appendOpenH264Switches} from '@electron/main/OpenH264Manager';
import {cleanupLinuxChromiumSpellcheckDictionaries} from '@electron/main/Spellcheck';
import {registerUpdater} from '@electron/main/Updater';
import {
@@ -265,10 +262,6 @@ if (launchConfigurationError) {
addLinuxHardwareVideoEncodeFeatures(enabledChromiumFeatures);
addWindowsHardwareVideoEncodeFeatures(enabledChromiumFeatures);
}
addLinuxScreenCapturePipeWireFeature(enabledChromiumFeatures);
if (process.platform === 'darwin') {
addMacosPreSequoiaScreenCaptureDisabledFeatures(disabledChromiumFeatures);
}
appendDisabledChromiumFeatures(disabledChromiumFeatures);
if (enabledChromiumFeatures.size > 0) {
appendEnabledChromiumFeatures(enabledChromiumFeatures);
@@ -282,7 +275,6 @@ if (launchConfigurationError) {
app.setToastActivatorCLSID(WINDOWS_TOAST_ACTIVATOR_CLSID);
app.setAppUserModelId(WINDOWS_APP_USER_MODEL_ID);
}
appendOpenH264Switches();
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
@@ -339,12 +331,6 @@ if (launchConfigurationError) {
} catch (error) {
log.error('[Init] Failed to register IPC handlers:', error);
}
try {
const {initOpenH264} = await import('@electron/main/OpenH264Manager');
initOpenH264();
} catch (error) {
log.warn('[Init] OpenH264 initialization skipped:', error);
}
try {
runStartupPhase('autostart-handlers', registerAutostartHandlers);
} catch (error) {
-4
View File
@@ -42,7 +42,6 @@ import type {
NativeScreenCaptureStartResult,
NotificationOptions,
NotificationResult,
OpenH264Status,
SetDesktopTroubleshootingDisableHardwareAccelerationOptions,
SpellcheckBundledDictionary,
SpellcheckResolvedEngineInfo,
@@ -340,9 +339,6 @@ const api: ElectronAPI = {
getDesktopInfo: (): Promise<DesktopInfo> => ipcRenderer.invoke('get-desktop-info'),
getGpuInfo: (): Promise<GpuInfo> => ipcRenderer.invoke('get-gpu-info'),
getAppMetrics: (): Promise<AppMetricsSnapshot> => ipcRenderer.invoke('get-app-metrics'),
getOpenH264Status: (): Promise<OpenH264Status> => ipcRenderer.invoke('get-openh264-status'),
setOpenH264Enabled: (enabled: boolean): Promise<OpenH264Status> =>
ipcRenderer.invoke('set-openh264-enabled', enabled),
getSystemIdleTimeMs: (): Promise<number> => ipcRenderer.invoke('system-idle-time-ms'),
getDesktopWindowBehaviorSettings: (): Promise<DesktopWindowBehaviorSettings> =>
ipcRenderer.invoke('desktop-window-behavior-get'),