fix: eliminate screen share audio feedback loop + upgrade Electron 33→40
Screen sharing with audio captured the app's own voice playback, causing users to hear themselves echoed back. Fixed via two layers: - Add restrictOwnAudio constraint (Chrome 141+/Chromium 144) to exclude the app's own audio from system audio capture - Add shareAudio toggle so users can disable system audio entirely - Remove outdated macOS audio block (now supported via ScreenCaptureKit) - Upgrade Electron 33→40 (Chromium 130→144) so restrictOwnAudio works natively in the desktop app - Add NSAudioCaptureUsageDescription for macOS 14.2+ audio capture - Add GTK 3 fallback for Linux GNOME compatibility (Electron 36+)
This commit is contained in:
@@ -17,6 +17,9 @@ protocols:
|
||||
- backspace
|
||||
mac:
|
||||
category: public.app-category.social-networking
|
||||
minimumSystemVersion: "12.0"
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: "Backspace needs access to system audio to share sound during screen sharing."
|
||||
target:
|
||||
- dmg
|
||||
- zip
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"electron-updater": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^33.2.0",
|
||||
"electron": "^40.0.0",
|
||||
"electron-builder": "^25.1.8",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ function registerIpcHandlers(): void {
|
||||
});
|
||||
|
||||
// Screen share picker coordination (used by setDisplayMediaRequestHandler)
|
||||
ipcMain.on('screen-share-selected', (_event, sourceId: string | null) => {
|
||||
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
|
||||
});
|
||||
@@ -487,6 +487,12 @@ function handleDeepLink(url: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Electron 36+ defaults to GTK 4 on GNOME, which crashes if GTK 2/3
|
||||
// libraries are loaded in the same process. Force GTK 3 for compatibility.
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('gtk-version', '3');
|
||||
}
|
||||
|
||||
// Set as default protocol handler
|
||||
app.setAsDefaultProtocolClient('backspace');
|
||||
|
||||
@@ -621,12 +627,12 @@ if (!gotTheLock) {
|
||||
// Send sources to renderer, wait for user selection
|
||||
mainWindow?.webContents.send('screen-share-sources', serialized);
|
||||
|
||||
const sourceId = await new Promise<string | null>((resolve) => {
|
||||
ipcMain.once('screen-share-selected', (_event, id: string | null) => {
|
||||
resolve(id);
|
||||
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);
|
||||
console.log('[Main:ScreenShare] User selected:', sourceId, 'audio:', shareAudio);
|
||||
|
||||
if (!sourceId) {
|
||||
// @ts-ignore — deny the request without crashing
|
||||
@@ -642,12 +648,9 @@ if (!gotTheLock) {
|
||||
}
|
||||
|
||||
// Provide the selected source — Electron creates the MediaStream
|
||||
// Enable system audio loopback on Windows/Linux (macOS blocks at OS level)
|
||||
if (process.platform === 'darwin') {
|
||||
callback({ video: selected });
|
||||
} else {
|
||||
callback({ video: selected, audio: 'loopback' });
|
||||
}
|
||||
// System audio loopback: Windows/Linux native, macOS 13+ via ScreenCaptureKit
|
||||
// Controlled by user's shareAudio preference from the picker UI
|
||||
callback({ video: selected, ...(shareAudio ? { audio: 'loopback' } : {}) });
|
||||
} catch (err) {
|
||||
console.error('[Main:ScreenShare] Handler error:', err);
|
||||
// @ts-ignore — deny the request without crashing
|
||||
|
||||
@@ -54,8 +54,8 @@ contextBridge.exposeInMainWorld('backspace', {
|
||||
onScreenShareSources: (callback: (sources: unknown[]) => void) => {
|
||||
ipcRenderer.on('screen-share-sources', (_event, sources) => callback(sources));
|
||||
},
|
||||
selectScreenSource: (sourceId: string | null) => {
|
||||
ipcRenderer.send('screen-share-selected', sourceId);
|
||||
selectScreenSource: (sourceId: string | null, shareAudio?: boolean) => {
|
||||
ipcRenderer.send('screen-share-selected', sourceId, shareAudio ?? true);
|
||||
},
|
||||
|
||||
// Instance URL management
|
||||
|
||||
Reference in New Issue
Block a user