// SPDX-License-Identifier: AGPL-3.0-or-later import {randomUUID} from 'node:crypto'; import {createRequire} from 'node:module'; import {createChildLogger} from '@electron/common/Logger'; import type { NativeAudioApplication, NativeAudioAvailability, NativeAudioEndReason, NativeAudioRoutingGraphResult, NativeAudioStartOptions, NativeAudioStartResult, VirtmicLinkOptions, VirtmicNode, VirtmicRoutingGraph, } from '@electron/common/Types'; import {getNativeAudioMode} from '@electron/main/LaunchOptions'; import {ipcMain} from 'electron'; import {buildFluxerAudioExcludePatterns, isKnownFluxerAudioProcessPid} from './FluxerAudioIdentity'; import {resolveVirtmicWindowPid} from './LinuxAudioCapture'; import {parseWindowSourceToken as parseDesktopWindowSourceToken} from './LinuxAudioCaptureHelpers'; import {getTccStatus} from './MacTcc'; import { audioFrameDebugDetails, isValidAudioFrame, isValidLinuxRule, isValidTargetPid, normalizeTimestampUs, } from './NativeAudioValidation'; import {createScreenAudioSinkHandleForSender, hasActiveNativeEngineForSender} from './NativeVoiceEngine'; const logger = createChildLogger('NativeAudio'); const requireModule = createRequire(import.meta.url); interface NativeCaptureInstance { on(event: 'frame', listener: (frame: unknown) => void): this; on(event: 'error', listener: (error: Error) => void): this; on(event: 'closed', listener: () => void): this; removeListener(event: 'frame', listener: (frame: unknown) => void): this; removeListener(event: 'error', listener: (error: Error) => void): this; removeListener(event: 'closed', listener: () => void): this; start(): Promise | void; stop(): Promise | void; setScreenAudioSink?: (handle: unknown) => boolean; clearScreenAudioSink?: () => void; routingGraph?: () => VirtmicRoutingGraph | null; } interface WindowsNativeAudioModule { isSupported: () => boolean; getBackendInfo?: () => { supported: boolean; reason?: string; processSupported?: boolean; systemSupported?: boolean; systemExcludesSelf?: boolean; processIncludeSupported?: boolean; processExcludeSupported?: boolean; sessionMixerSupported?: boolean; systemLoopbackMode?: string; minWindowsBuild?: number; minWindowsVersionLabel?: string; detectedWindowsBuild?: number; }; pidFromHwnd: (hwnd: bigint) => number; resolveAudioRootPid: (pid: number) => number; ProcessLoopback: new ( pid: number, options?: { includeProcessTree?: boolean; captureScope?: 'process' | 'system' | 'session-mixer'; winCaptureScope?: 'process' | 'system' | 'session-mixer'; }, ) => NativeCaptureInstance; loadError?: Error | null; } interface MacApplicationDescriptor { pid: number; bundleId?: string; name: string; } interface MacBackendAvailability { sck?: { supported: boolean; macosVersion?: string; }; coreaudio?: { supported: boolean; }; screenPermission?: string; audioPermission?: string; } interface MacNativeAudioModule { listAudibleApplications: () => Promise>; getBackendAvailability: () => Promise; pidFromWindowId: (windowId: number) => number; ProcessLoopback: new ( pid: number, options?: { excludeSelf?: boolean; includeProcessTree?: boolean; backend?: 'sck' | 'coreaudio' | 'auto'; captureScope?: 'process' | 'system'; macCaptureScope?: 'process' | 'system'; }, ) => NativeCaptureInstance; loadError?: Error | null; } interface LinuxRoutingRule extends VirtmicLinkOptions { include?: Array; exclude?: Array; onlySpeakers?: boolean; onlyDefaultSpeakers?: boolean; } interface LinuxNativeAudioModule { pipeWireAvailable: () => boolean; audioBackend?: () => 'pipewire' | 'none'; ProcessLoopback: new ( target: | number | { linuxRule: LinuxRoutingRule; }, options?: { includeProcessTree?: boolean; ignoreDevices?: boolean; }, ) => NativeCaptureInstance; } interface NativeAddonLoadResult { addon?: LinuxNativeAudioModule | MacNativeAudioModule | WindowsNativeAudioModule; availability: NativeAudioAvailability; platform: NodeJS.Platform; } interface ActiveNativeAudioSession { captureId: string; capture: NativeCaptureInstance; sender: Electron.WebContents; onFrame: (frame: unknown) => void; onError: (error: Error) => void; onClosed: () => void; onSenderDestroyed: () => void; finalized: boolean; stopping?: Promise; stoppingReason?: NativeAudioEndReason; stoppingDetail?: string; } const MAX_NATIVE_AUDIO_SESSIONS_PER_SENDER = 2; let cachedLoadResult: NativeAddonLoadResult | undefined; let handlersRegistered = false; const activeSessions = new Map(); const activeSessionIdsBySenderId = new Map>(); function isModuleNotFoundError(error: unknown): boolean { const message = String( ( error as { message?: string; } )?.message ?? error ?? '', ).toLowerCase(); return message.includes('cannot find module') || message.includes('module_not_found'); } type LinuxRoutingPattern = Record; function appendUniquePattern(out: Array, pattern: LinuxRoutingPattern): void { if (Object.keys(pattern).length === 0) return; const key = JSON.stringify(pattern); if (!out.some((existing) => JSON.stringify(existing) === key)) { out.push(pattern); } } function normalizeLinuxRoutingRule(rule: LinuxRoutingRule): LinuxRoutingRule { const include = rule.include?.map((pattern) => ({...pattern})) ?? []; if (include.length === 0 && rule.onlySpeakers === false) { appendUniquePattern(include, {'media.class': 'Stream/Output/Audio'}); } const exclude: Array = []; for (const pattern of rule.exclude ?? []) { appendUniquePattern(exclude, {...pattern}); } for (const pattern of buildFluxerAudioExcludePatterns()) { appendUniquePattern(exclude, pattern); } if (rule.ignoreInputMedia ?? true) { appendUniquePattern(exclude, {'media.class': 'Stream/Input/Audio'}); } if (rule.ignoreVirtual) { appendUniquePattern(exclude, {'node.virtual': 'true'}); } return { ...rule, include, exclude, }; } function describeAddonLoadError(error: unknown): string { if (error instanceof Error) { const code = (error as Error & {code?: string}).code; const message = error.message ?? ''; return code ? `${code}: ${message}` : message; } return String(error); } function loadNativeAudioAddon(): NativeAddonLoadResult { if (cachedLoadResult) return cachedLoadResult; if (getNativeAudioMode(process.argv) === 'off') { cachedLoadResult = { platform: process.platform, availability: { available: false, reason: 'disabled-by-launch', detail: 'Native audio disabled by launch diagnostics', }, }; return cachedLoadResult; } if (process.platform === 'win32') { try { const addon = requireModule('@fluxer/win-process-loopback') as WindowsNativeAudioModule; if (addon.loadError) { const detail = describeAddonLoadError(addon.loadError); logger.warn('Windows native audio addon reported load error', addon.loadError); cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'windows-wasapi-loopback', reason: 'load-failed', detail, }, }; return cachedLoadResult; } const backendInfo = addon.getBackendInfo?.(); const processInclude = backendInfo?.processIncludeSupported ?? backendInfo?.processSupported ?? addon.isSupported(); const processExclude = backendInfo?.processExcludeSupported ?? backendInfo?.systemLoopbackMode === 'process-exclude'; const sessionMixer = backendInfo?.sessionMixerSupported ?? processInclude; const processSupported = backendInfo?.processSupported ?? processInclude; const systemSupported = backendInfo?.systemSupported ?? (processExclude || sessionMixer); const systemExcludesSelf = backendInfo?.systemExcludesSelf ?? (processExclude || sessionMixer); const systemLoopbackMode = backendInfo?.systemLoopbackMode ?? (processExclude ? 'process-exclude' : sessionMixer ? 'session-mixer' : 'unavailable'); if (!processSupported && !systemSupported) { cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'windows-wasapi-loopback', reason: 'os-version-too-old', detail: backendInfo?.reason, capabilities: { process: false, system: false, systemExcludesSelf: false, processInclude: false, processExclude: false, sessionMixer: false, systemLoopbackMode: 'unavailable', }, }, }; return cachedLoadResult; } cachedLoadResult = { platform: process.platform, addon, availability: { available: true, backend: 'windows-wasapi-loopback', detail: backendInfo?.reason, capabilities: { process: processSupported, system: systemSupported, systemExcludesSelf, processInclude, processExclude, sessionMixer, systemLoopbackMode: systemLoopbackMode === 'process-exclude' || systemLoopbackMode === 'session-mixer' ? systemLoopbackMode : 'unavailable', }, }, }; return cachedLoadResult; } catch (error) { const detail = describeAddonLoadError(error); const reason = isModuleNotFoundError(error) ? 'addon-not-installed' : 'load-failed'; logger.warn('Failed to load Windows native audio addon', error); cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'windows-wasapi-loopback', reason, detail, }, }; return cachedLoadResult; } } if (process.platform === 'darwin') { try { const addon = requireModule('@fluxer/mac-app-audio') as MacNativeAudioModule; if (addon.loadError) { const detail = describeAddonLoadError(addon.loadError); logger.warn('macOS native audio addon reported load error', addon.loadError); cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'macos-sck', reason: 'load-failed', detail, }, }; return cachedLoadResult; } cachedLoadResult = { platform: process.platform, addon, availability: { available: true, backend: 'macos-sck', }, }; return cachedLoadResult; } catch (error) { const detail = describeAddonLoadError(error); const reason = isModuleNotFoundError(error) ? 'addon-not-installed' : 'load-failed'; logger.warn('Failed to load macOS native audio addon', error); cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'macos-sck', reason, detail, }, }; return cachedLoadResult; } } if (process.platform === 'linux') { try { const addon = requireModule('@fluxer/linux-audio-capture') as LinuxNativeAudioModule; if (typeof addon.pipeWireAvailable !== 'function' || typeof addon.ProcessLoopback !== 'function') { cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'linux-pipewire', reason: 'load-failed', }, }; return cachedLoadResult; } const pipewireAvailable = addon.pipeWireAvailable(); const backend = addon.audioBackend?.() ?? (pipewireAvailable ? 'pipewire' : 'none'); if (!pipewireAvailable || backend !== 'pipewire') { cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'linux-pipewire', reason: 'no-pipewire', }, }; return cachedLoadResult; } cachedLoadResult = { platform: process.platform, addon, availability: { available: true, backend: 'linux-pipewire', capabilities: { process: true, system: true, systemExcludesSelf: true, }, }, }; return cachedLoadResult; } catch (error) { const detail = describeAddonLoadError(error); const reason = isModuleNotFoundError(error) ? 'addon-not-installed' : 'load-failed'; logger.warn('Failed to load Linux native audio addon', error); cachedLoadResult = { platform: process.platform, availability: { available: false, backend: 'linux-pipewire', reason, detail, }, }; return cachedLoadResult; } } cachedLoadResult = { platform: process.platform, availability: { available: false, reason: 'unsupported-platform', }, }; return cachedLoadResult; } function getMacScreenPermissionStatus(): string | null { try { return getTccStatus('screen-recording'); } catch (error) { logger.debug('Failed to read macOS screen capture status', error); return null; } } async function getNativeAudioAvailability(): Promise { const loadResult = loadNativeAudioAddon(); if (!loadResult.availability.available || !loadResult.addon) { return loadResult.availability; } if (loadResult.platform !== 'darwin') { return loadResult.availability; } try { const addon = loadResult.addon as MacNativeAudioModule; const backendAvailability = await addon.getBackendAvailability(); const coreAudioSupported = backendAvailability.coreaudio?.supported === true; const sckSupported = backendAvailability.sck?.supported === true; const backend = sckSupported ? 'macos-sck' : 'macos-coreaudio'; const capabilities = { process: sckSupported || coreAudioSupported, system: sckSupported || coreAudioSupported, systemExcludesSelf: sckSupported || coreAudioSupported, }; if (!coreAudioSupported && !sckSupported) { return { available: false, backend, reason: 'os-version-too-old', detail: backendAvailability.sck?.macosVersion, capabilities, }; } const screenPermissionStatus = getMacScreenPermissionStatus(); if (screenPermissionStatus === 'denied' || screenPermissionStatus === 'restricted') { return { available: false, backend, reason: 'permission-denied', detail: `screen:${screenPermissionStatus}`, capabilities, }; } return { available: true, backend, detail: backendAvailability.sck?.macosVersion, capabilities, }; } catch (error) { logger.warn('Failed to query macOS native audio availability', error); return { available: false, backend: 'macos-sck', reason: 'load-failed', }; } } async function listNativeAudioApplications(): Promise> { const loadResult = loadNativeAudioAddon(); if (!loadResult.availability.available || !loadResult.addon) { return []; } if (loadResult.platform !== 'darwin') { return []; } try { const addon = loadResult.addon as MacNativeAudioModule; const applications = await addon.listAudibleApplications(); return applications .filter((application) => Number.isFinite(application.pid) && application.pid > 0) .map((application) => ({ pid: application.pid, identifier: application.bundleId || `pid:${application.pid}`, name: application.name || application.bundleId || `PID ${application.pid}`, })); } catch (error) { logger.warn('Failed to list native audio applications', error); return []; } } async function resolveAudioRootPidForSource(sourceId: unknown): Promise { const windowToken = parseDesktopWindowSourceToken(sourceId); if (!windowToken) { return null; } const loadResult = loadNativeAudioAddon(); if (!loadResult.availability.available || !loadResult.addon) { return null; } try { if (loadResult.platform === 'win32') { const addon = loadResult.addon as WindowsNativeAudioModule; const pid = addon.pidFromHwnd(BigInt(windowToken)); if (!Number.isFinite(pid) || pid <= 0) return null; if (isKnownFluxerAudioProcessPid(pid)) return null; const rootPid = addon.resolveAudioRootPid(pid); const resolved = Number.isFinite(rootPid) && rootPid > 0 ? rootPid : pid; return isKnownFluxerAudioProcessPid(resolved) ? null : resolved; } if (loadResult.platform === 'darwin') { const addon = loadResult.addon as MacNativeAudioModule; const pid = addon.pidFromWindowId(Number(windowToken)); if (!Number.isFinite(pid) || pid <= 0) return null; return isKnownFluxerAudioProcessPid(pid) ? null : pid; } if (loadResult.platform === 'linux') { return resolveVirtmicWindowPid(sourceId); } } catch (error) { logger.warn('Failed to resolve native-audio source PID', {sourceId, error}); } return null; } function copySamplesBuffer(samples: Float32Array): ArrayBuffer { const copy = new Float32Array(samples.length); copy.set(samples); return copy.buffer; } function removeSessionListeners(session: ActiveNativeAudioSession): void { session.capture.removeListener('frame', session.onFrame); session.capture.removeListener('error', session.onError); session.capture.removeListener('closed', session.onClosed); session.sender.removeListener('destroyed', session.onSenderDestroyed); } function rememberSenderSession(senderId: number, captureId: string): void { let senderSessions = activeSessionIdsBySenderId.get(senderId); if (!senderSessions) { senderSessions = new Set(); activeSessionIdsBySenderId.set(senderId, senderSessions); } senderSessions.add(captureId); } function forgetSenderSession(senderId: number, captureId: string): void { const senderSessions = activeSessionIdsBySenderId.get(senderId); if (!senderSessions) return; senderSessions.delete(captureId); if (senderSessions.size === 0) { activeSessionIdsBySenderId.delete(senderId); } } function finalizeSession(session: ActiveNativeAudioSession, reason: NativeAudioEndReason, detail?: string): void { if (session.finalized) return; session.finalized = true; removeSessionListeners(session); activeSessions.delete(session.captureId); forgetSenderSession(session.sender.id, session.captureId); if (!session.sender.isDestroyed()) { try { session.sender.send('native-audio:end', { captureId: session.captureId, reason, detail, }); } catch (error) { logger.warn('Failed to send native audio end event to renderer', { captureId: session.captureId, reason, error, }); } } } async function stopActiveSession( session: ActiveNativeAudioSession, reason: NativeAudioEndReason = 'stopped', detail?: string, ): Promise { if (session.finalized) return; if (session.stopping) return session.stopping; session.stoppingReason = reason; session.stoppingDetail = detail; session.stopping = (async () => { try { await Promise.resolve(session.capture.stop()); } catch (error) { logger.warn('Failed to stop native audio capture', {captureId: session.captureId, error}); } finally { finalizeSession(session, reason, detail); } })(); return session.stopping; } async function stopCaptureById( captureId: string, reason: NativeAudioEndReason = 'stopped', detail?: string, ): Promise { const session = activeSessions.get(captureId); if (!session) return; await stopActiveSession(session, reason, detail); } async function makeRoomForSenderSession(senderId: number): Promise { const senderSessions = activeSessionIdsBySenderId.get(senderId); if (!senderSessions) return; while (senderSessions.size >= MAX_NATIVE_AUDIO_SESSIONS_PER_SENDER) { const oldestCaptureId = senderSessions.values().next().value as string | undefined; if (!oldestCaptureId) return; await stopCaptureById(oldestCaptureId, 'stopped', 'sender-session-limit'); } } async function startNativeAudioCapture( sender: Electron.WebContents, options: NativeAudioStartOptions, ): Promise { if (sender.isDestroyed()) { throw new Error('Cannot start native audio capture for destroyed renderer'); } const loadResult = loadNativeAudioAddon(); const linuxRule = loadResult.platform === 'linux' && isValidLinuxRule(options?.linuxRule) ? normalizeLinuxRoutingRule(options.linuxRule) : null; const macCaptureScope = loadResult.platform === 'darwin' && options?.macCaptureScope === 'system' ? 'system' : 'process'; const macSystemCapture = loadResult.platform === 'darwin' && macCaptureScope === 'system'; const winCaptureScope = loadResult.platform === 'win32' && options?.winCaptureScope === 'session-mixer' ? 'session-mixer' : loadResult.platform === 'win32' && options?.winCaptureScope === 'system' ? 'system' : 'process'; const winSystemCapture = loadResult.platform === 'win32' && (winCaptureScope === 'system' || winCaptureScope === 'session-mixer'); const targetPid = options?.targetPid; if (!options || (!linuxRule && !macSystemCapture && !winSystemCapture && !isValidTargetPid(targetPid))) { throw new Error('Invalid native audio target PID'); } if (!macSystemCapture && !winSystemCapture && targetPid === process.pid) { throw new Error('Refusing to capture native audio from own process'); } if ( !macSystemCapture && !winSystemCapture && isValidTargetPid(targetPid) && isKnownFluxerAudioProcessPid(targetPid) ) { throw new Error('Refusing to capture native audio from Fluxer process'); } const availability = await getNativeAudioAvailability(); if (!availability.available) { throw new Error(`Native audio capture unavailable (${availability.reason ?? 'unknown'})`); } const requestedScope: 'process' | 'system' = winSystemCapture || macSystemCapture ? 'system' : linuxRule ? linuxRule.include && linuxRule.include.length > 0 ? 'process' : 'system' : 'process'; const capability = availability.capabilities?.[requestedScope]; if (capability === false) { throw new Error( `Native ${loadResult.platform} ${requestedScope} audio capture unavailable (${availability.reason ?? 'unsupported-scope'})`, ); } if (requestedScope === 'system' && availability.capabilities?.systemExcludesSelf !== true) { throw new Error(`Native ${loadResult.platform} system audio capture does not guarantee Fluxer self-exclusion`); } const effectiveWinCaptureScope = loadResult.platform === 'win32' && winCaptureScope === 'system' && availability.capabilities?.processExclude === false && availability.capabilities?.sessionMixer === true ? 'session-mixer' : winCaptureScope; if (sender.isDestroyed()) { throw new Error('Cannot start native audio capture for destroyed renderer'); } if (!loadResult.addon) { throw new Error('Native audio addon unavailable'); } await makeRoomForSenderSession(sender.id); if (sender.isDestroyed()) { throw new Error('Cannot start native audio capture for destroyed renderer'); } const captureId = randomUUID(); const targetPidForCapture = macSystemCapture || winSystemCapture ? process.pid : targetPid; let capture: NativeCaptureInstance; if (loadResult.platform === 'win32') { if (!isValidTargetPid(targetPidForCapture)) throw new Error('Invalid native audio target PID'); capture = new (loadResult.addon as WindowsNativeAudioModule).ProcessLoopback(targetPidForCapture, { includeProcessTree: winSystemCapture ? false : (options.includeProcessTree ?? true), captureScope: effectiveWinCaptureScope, }); } else if (loadResult.platform === 'darwin') { if (!isValidTargetPid(targetPidForCapture)) throw new Error('Invalid native audio target PID'); capture = new (loadResult.addon as MacNativeAudioModule).ProcessLoopback(targetPidForCapture, { excludeSelf: true, includeProcessTree: macSystemCapture ? true : (options.includeProcessTree ?? true), backend: options.macBackend ?? (macSystemCapture ? 'sck' : 'auto'), captureScope: macCaptureScope, }); } else if (loadResult.platform === 'linux') { const addon = loadResult.addon as LinuxNativeAudioModule; capture = linuxRule ? new addon.ProcessLoopback({linuxRule}) : new addon.ProcessLoopback(targetPid as number, { includeProcessTree: options.includeProcessTree ?? true, ignoreDevices: true, }); } else { throw new Error('Native audio addon unavailable for this platform'); } let firstFrameLogged = false; let nativeSinkAttached = false; let slowPathReported = false; const session: ActiveNativeAudioSession = { captureId, capture, sender, finalized: false, onFrame: (frame) => { if (session.finalized) return; if (!isValidAudioFrame(frame)) { logger.warn('Native audio capture emitted invalid frame', { captureId, ...audioFrameDebugDetails(frame), }); stopActiveSession(session, 'addon-error', 'Native audio addon emitted an invalid PCM frame').catch((error) => logger.warn('stopActiveSession failed after invalid native audio frame', {captureId, error}), ); return; } if (sender.isDestroyed()) { stopActiveSession(session, 'stopped').catch((error) => logger.warn('stopActiveSession failed after sender destroyed', {captureId, error}), ); return; } if (nativeSinkAttached) { if (!slowPathReported) { slowPathReported = true; logger.error( 'Native screen-audio fast path stopped engaging: a captured frame reached the JS bridge even though the native engine sink is attached. Failing the capture so this critical regression surfaces immediately instead of silently downgrading to the crackle-prone path.', {captureId, senderId: sender.id, sampleRate: frame.sampleRate, channels: frame.channels}, ); stopActiveSession(session, 'addon-error', 'native screen-audio fast path stopped engaging').catch((error) => logger.warn('stopActiveSession failed after native fast-path regression', {captureId, error}), ); } return; } try { sender.send('native-audio:frame', { captureId, sampleRate: frame.sampleRate, channels: frame.channels, timestampUs: normalizeTimestampUs(frame.timestampUs), samples: copySamplesBuffer(frame.samples), }); } catch (error) { logger.warn('Failed to send native audio frame to renderer', {captureId, error}); stopActiveSession(session, 'stopped').catch((stopError) => logger.warn('stopActiveSession failed after native audio send failure', {captureId, error: stopError}), ); return; } if (!firstFrameLogged) { firstFrameLogged = true; } }, onError: (error) => { const detail = error instanceof Error ? error.message : String(error); logger.warn('Native audio capture emitted error', {captureId, error}); stopActiveSession(session, 'addon-error', detail).catch((stopError) => logger.warn('stopActiveSession failed after addon error', {captureId, error: stopError}), ); }, onClosed: () => { const reason = session.stoppingReason ?? 'target-exited'; finalizeSession(session, reason, session.stoppingDetail); }, onSenderDestroyed: () => { stopActiveSession(session, 'stopped').catch((error) => logger.warn('stopActiveSession failed on sender-destroyed', {captureId, error}), ); }, }; capture.on('frame', session.onFrame); capture.on('error', session.onError); capture.on('closed', session.onClosed); sender.once('destroyed', session.onSenderDestroyed); activeSessions.set(captureId, session); rememberSenderSession(sender.id, captureId); try { if (hasActiveNativeEngineForSender(sender.id)) { if (typeof capture.setScreenAudioSink !== 'function') { throw new Error( `Native screen-audio fast path unavailable for capture ${captureId}: capture binding lacks setScreenAudioSink (stale native build)`, ); } const screenAudioSink = createScreenAudioSinkHandleForSender(sender.id); if (!screenAudioSink) { throw new Error( `Native screen-audio fast path unavailable for capture ${captureId}: native voice engine produced no sink handle for the active session`, ); } if (capture.setScreenAudioSink(screenAudioSink) !== true) { throw new Error( `Native screen-audio fast path unavailable for capture ${captureId}: native capture binding does not implement setScreenAudioSink (stale native build)`, ); } nativeSinkAttached = true; } await Promise.resolve(capture.start()); } catch (error) { logger.error('Native audio capture failed to start', {captureId, error}); removeSessionListeners(session); try { await Promise.resolve(capture.stop()); } catch (stopError) { logger.warn('Failed to stop native audio capture after start failure', {captureId, error: stopError}); } activeSessions.delete(captureId); forgetSenderSession(sender.id, captureId); throw error; } if (sender.isDestroyed()) { await stopActiveSession(session, 'stopped', 'sender-destroyed-during-start'); throw new Error('Native audio capture sender was destroyed during startup'); } if (session.finalized || activeSessions.get(captureId) !== session) { if (!session.finalized) { await stopActiveSession(session, 'stopped', 'session-replaced-during-start'); } throw new Error('Native audio capture ended before startup completed'); } return { captureId, sampleRate: 48000, channels: 2, }; } function getNativeAudioRoutingGraph(sender: Electron.WebContents, captureId?: string): NativeAudioRoutingGraphResult { const loadResult = loadNativeAudioAddon(); const availability = loadResult.availability; if (loadResult.platform !== 'linux' || !availability.available) { return {ok: false, graphs: [], availability}; } const ownedIds = activeSessionIdsBySenderId.get(sender.id); const candidateIds = typeof captureId === 'string' && captureId ? ownedIds?.has(captureId) ? [captureId] : [] : Array.from(ownedIds ?? []); const graphs = candidateIds .map((id) => { const session = activeSessions.get(id); if (!session || session.sender.id !== sender.id) return null; return { captureId: id, graph: session.capture.routingGraph?.() ?? null, }; }) .filter((entry): entry is {captureId: string; graph: VirtmicRoutingGraph | null} => entry !== null); return {ok: true, graphs, availability}; } export function registerNativeAudioHandlers(): void { if (handlersRegistered) return; handlersRegistered = true; ipcMain.handle('native-audio:get-availability', (): Promise => getNativeAudioAvailability()); ipcMain.handle( 'native-audio:list-applications', (): Promise> => listNativeAudioApplications(), ); ipcMain.handle( 'native-audio:resolve-root-pid', (_event, sourceId: unknown): Promise => resolveAudioRootPidForSource(sourceId), ); ipcMain.handle( 'native-audio:start', (event, options: NativeAudioStartOptions): Promise => startNativeAudioCapture(event.sender, options), ); ipcMain.handle('native-audio:stop', async (event, captureId: string): Promise => { const session = activeSessions.get(captureId); if (!session || session.sender.id !== event.sender.id) { return; } await stopActiveSession(session, 'stopped'); }); ipcMain.handle( 'native-audio:get-routing-graph', (event, captureId?: string): NativeAudioRoutingGraphResult => getNativeAudioRoutingGraph(event.sender, captureId), ); } export function cleanupNativeAudio(): void { if (!handlersRegistered) return; ipcMain.removeHandler('native-audio:get-availability'); ipcMain.removeHandler('native-audio:list-applications'); ipcMain.removeHandler('native-audio:resolve-root-pid'); ipcMain.removeHandler('native-audio:start'); ipcMain.removeHandler('native-audio:stop'); ipcMain.removeHandler('native-audio:get-routing-graph'); handlersRegistered = false; for (const session of [...activeSessions.values()]) { stopActiveSession(session, 'stopped').catch((error) => logger.warn('stopActiveSession failed during cleanup', {captureId: session.captureId, error}), ); } }