// System-audio loopback. Under Electron this is renderer-driven: we // ask main for the primary screen's capturer id, then call // getUserMedia with Chromium's `chromeMediaSource: 'desktop'` // constraint to obtain the OS-mixer MediaStream directly. The whole // Tauri WASAPI + AudioWorklet base64 pipeline is gone. // // Windows-only in practice (loopback audio is a Windows feature of // Chromium's desktop source). On other platforms `startSystemAudioCapture` // throws `SystemAudioUnavailable`; callers are expected to fall back // to the standard getDisplayMedia flow. import { isTauriRuntime } from './globalShortcut'; export interface SystemAudioHandle { /** Monotonic id, used by callers to correlate stop() with start. */ captureId: number; /** MediaStream with a single audio track carrying the OS mixer. */ stream: MediaStream; /** Teardown — stops the MediaStreamTrack. Idempotent. */ stop: () => Promise; } export class SystemAudioUnavailable extends Error { constructor(reason: string) { super('system audio unavailable: ' + reason); this.name = 'SystemAudioUnavailable'; } } interface ChromiumAudioConstraint { mandatory: { chromeMediaSource: 'desktop'; chromeMediaSourceId: string; }; } export async function startSystemAudioCapture(): Promise { if (!isTauriRuntime()) { throw new SystemAudioUnavailable('not an electron runtime'); } if (typeof navigator === 'undefined' || !navigator.mediaDevices) { throw new SystemAudioUnavailable('mediaDevices unavailable'); } const resolved = await window.electronAPI.resolveLoopbackSource(); if (!resolved) { throw new SystemAudioUnavailable('no screen source available'); } const audioConstraint: ChromiumAudioConstraint = { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: resolved.sourceId, }, }; let stream: MediaStream; try { // Cast: the Chromium `mandatory` constraint is non-standard and // not covered by lib.dom.d.ts typings. stream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraint as unknown as MediaTrackConstraints, video: false, }); } catch (err: unknown) { throw new SystemAudioUnavailable( err instanceof Error ? err.message : String(err), ); } const tracks = stream.getAudioTracks(); if (tracks.length === 0) { for (const t of stream.getTracks()) t.stop(); throw new SystemAudioUnavailable('no audio track in returned stream'); } const captureId = Date.now(); let stopped = false; const stop = async (): Promise => { if (stopped) return; stopped = true; for (const track of stream.getTracks()) { try { track.stop(); } catch { /* already stopped */ } } }; return { captureId, stream, stop }; }