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:
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user