diff --git a/docs/systems/voice.md b/docs/systems/voice.md index 28b6d647..9ce73a87 100644 --- a/docs/systems/voice.md +++ b/docs/systems/voice.md @@ -206,7 +206,7 @@ ScreenShareConfig { fps: number, // 30-120 mode: 'gaming' | 'text', // Affects bitrate & content hint customBitrateKbps: number | null, // Admin override (if allowed) - shareAudio: boolean // System audio (disabled in Electron) + shareAudio: boolean // System audio loopback (see Platform Support below) } ``` @@ -234,6 +234,19 @@ ScreenShareConfig { - `allowCustomBitrate` toggle - `bitrateMatrixOverrides` (JSON sparse overrides) +### System Audio Loopback (`shareAudio`) + +The "Share system audio" toggle in `ScreenSharePicker` adds an audio track to the screen-share publication. In the browser it maps to `getDisplayMedia({ audio: true })`. In Electron, the `setDisplayMediaRequestHandler` callback (`packages/desktop/src/main.ts`) returns `audio: 'loopback'` to opt into Chromium's system-audio loopback path. + +| Platform | Mechanism | Notes | +|----------|-----------|-------| +| Browser (Chrome/Edge) | `getDisplayMedia({ audio: true })` | Tab/window/system audio per the user's pick | +| Electron / Windows | Chromium native loopback | Works out of the box | +| Electron / macOS 13+ | CoreAudio Tap (Catap) | Requires `NSAudioCaptureUsageDescription` (set by `electron-builder.yml#mac.extendInfo`) | +| Electron / Linux | PulseAudio loopback | **Requires** the `PulseaudioLoopbackForScreenShare` Chromium feature flag — enabled at startup in `main.ts` for Linux. Works on PulseAudio and on PipeWire systems with the `pipewire-pulse` compat layer. PipeWire-only systems without pulse compat will fail. | + +**Failure handling.** When loopback is not supported, Chromium rejects the entire `getDisplayMedia` request — the source-picker selection has already been consumed, so silently retrying without audio would re-prompt the picker. `startScreenShare` (`utils/screenShare.ts`) instead surfaces a warning toast directing the user to disable "Share system audio" if their system does not support loopback. We do **not** auto-mutate the user's `shareAudio` preference. + --- ## Voice Fullscreen diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index f64be95a..a15de47c 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -739,6 +739,11 @@ function handleDeepLink(url: string): void { // libraries are loaded in the same process. Force GTK 3 for compatibility. if (process.platform === 'linux') { app.commandLine.appendSwitch('gtk-version', '3'); + // Chromium ships PulseAudio loopback for screen-share behind a feature flag. + // Without it, returning `audio: 'loopback'` from setDisplayMediaRequestHandler + // fails the whole getDisplayMedia request — screen share never starts when the + // user has "Share system audio" enabled. + app.commandLine.appendSwitch('enable-features', 'PulseaudioLoopbackForScreenShare'); } // Set as default protocol handler @@ -896,8 +901,15 @@ if (!gotTheLock) { return; } - // Provide the selected source — Electron creates the MediaStream - // System audio loopback: Windows/Linux native, macOS 13+ via ScreenCaptureKit + // Provide the selected source — Electron creates the MediaStream. + // System audio loopback support varies: + // - Windows: native (Chromium default). + // - macOS 13+: CoreAudio Tap; requires NSAudioCaptureUsageDescription + // in Info.plist (electron-builder injects it via mac.extendInfo). + // - Linux: PulseAudio loopback, gated behind the + // `PulseaudioLoopbackForScreenShare` feature flag we enable above. + // Fails on PipeWire-only systems without pulse compat — the + // renderer catches that and toasts the user. callback({ video: selected, ...(shareAudio ? { audio: 'loopback' } : {}) }); } catch (err) { console.error('[Main:ScreenShare] Handler error:', err); diff --git a/packages/web/src/utils/screenShare.ts b/packages/web/src/utils/screenShare.ts index 78934ee6..0915637a 100644 --- a/packages/web/src/utils/screenShare.ts +++ b/packages/web/src/utils/screenShare.ts @@ -306,6 +306,16 @@ export async function startScreenShare(room: Room): Promise { } catch (err) { console.error('[ScreenShare] Failed to start screen share:', err); if (hwOverdrive) deactivateHwOverdrive(); + // Loopback unsupported (Linux without pulse, macOS without Catap) makes + // the whole getDisplayMedia call reject. No auto-retry: the picker + // selection was consumed, retrying would re-prompt it. + if (config.shareAudio && err instanceof Error && err.name !== 'NotAllowedError') { + useUIStore.getState().addToast( + 'Could not start stream with system audio. Disable "Share system audio" in the picker if your system does not support it.', + 'warning', + 8000, + ); + } return false; } }