fix: restore setDisplayMediaRequestHandler with useSystemPicker

Electron requires setDisplayMediaRequestHandler for getDisplayMedia() to
work — removing it broke screen sharing entirely. Restored the handler
with useSystemPicker: true, which on macOS 15+ uses the native system
picker (honoring restrictOwnAudio), while Windows/Linux fall back to
the custom picker with the shareAudio toggle for echo control.
This commit is contained in:
Jannis Braun
2026-03-16 18:34:50 +01:00
parent 9ff3761640
commit bda7930e61
5 changed files with 379 additions and 4 deletions
+68 -4
View File
@@ -9,6 +9,7 @@ import {
shell,
screen,
session,
desktopCapturer,
} from 'electron';
import path from 'path';
import fs from 'fs';
@@ -430,6 +431,11 @@ function registerIpcHandlers(): void {
}
});
// Screen share picker coordination (used by setDisplayMediaRequestHandler)
ipcMain.on('screen-share-selected', (_event, _sourceId: string | null, _shareAudio?: boolean) => {
// Handled via ipcMain.once in the display media handler — this is just
// a safety net to prevent unhandled-message warnings
});
}
// ─── Auto-Update ────────────────────────────────────────────────────────────
@@ -591,10 +597,68 @@ if (!gotTheLock) {
await session.defaultSession.clearStorageData({ storages: ['serviceworkers'] });
await session.defaultSession.clearCache();
// Screen sharing: let Chromium's native getDisplayMedia() pipeline handle
// source selection and audio capture. This ensures restrictOwnAudio is
// applied natively, preventing the app's own voice playback from being
// captured in the system audio loopback.
// Intercept getDisplayMedia() — show custom picker in renderer.
// useSystemPicker: on macOS 15+, the native system picker runs instead of
// our handler, allowing Chromium's restrictOwnAudio constraint to work
// natively (no audio feedback). On Windows/Linux the handler is called.
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
console.log('[Main:ScreenShare] Handler invoked');
try {
const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
thumbnailSize: { width: 320, height: 180 },
fetchWindowIcons: true,
});
console.log('[Main:ScreenShare] Got', sources.length, 'sources');
if (sources.length === 0) {
console.warn('[Main:ScreenShare] No sources — Screen Recording permission may not be granted');
// @ts-ignore — Electron throws if we pass {} when video was requested; pass nothing to deny
callback();
return;
}
const serialized = sources.map((source) => ({
id: source.id,
name: source.name,
thumbnailDataUrl: source.thumbnail.toDataURL(),
appIconDataUrl: source.appIcon && !source.appIcon.isEmpty()
? source.appIcon.toDataURL() : null,
isScreen: source.id.startsWith('screen:'),
}));
// Send sources to renderer, wait for user selection
mainWindow?.webContents.send('screen-share-sources', serialized);
const { sourceId, shareAudio } = await new Promise<{ sourceId: string | null; shareAudio: boolean }>((resolve) => {
ipcMain.once('screen-share-selected', (_event, id: string | null, wantAudio?: boolean) => {
resolve({ sourceId: id, shareAudio: wantAudio ?? true });
});
});
console.log('[Main:ScreenShare] User selected:', sourceId, 'audio:', shareAudio);
if (!sourceId) {
// @ts-ignore — deny the request without crashing
callback();
return;
}
const selected = sources.find((s) => s.id === sourceId);
if (!selected) {
// @ts-ignore — deny the request without crashing
callback();
return;
}
// Provide the selected source — Electron creates the MediaStream
// System audio loopback: Windows/Linux native, macOS 13+ via ScreenCaptureKit
callback({ video: selected, ...(shareAudio ? { audio: 'loopback' } : {}) });
} catch (err) {
console.error('[Main:ScreenShare] Handler error:', err);
// @ts-ignore — deny the request without crashing
callback();
}
}, { useSystemPicker: true });
registerIpcHandlers();
createWindow();
+8
View File
@@ -50,6 +50,14 @@ contextBridge.exposeInMainWorld('backspace', {
ipcRenderer.on('deep-link', (_event, url) => callback(url));
},
// Screen share picker coordination
onScreenShareSources: (callback: (sources: unknown[]) => void) => {
ipcRenderer.on('screen-share-sources', (_event, sources) => callback(sources));
},
selectScreenSource: (sourceId: string | null, shareAudio?: boolean) => {
ipcRenderer.send('screen-share-selected', sourceId, shareAudio ?? true);
},
// Instance URL management
getInstanceUrl: () => ipcRenderer.invoke('get-instance-url'),
setInstanceUrl: (url: string) => ipcRenderer.invoke('set-instance-url', url),