Skip to content

Audio Capture

@cosella/audio-capture handles acquiring audio streams from the user’s microphone and device/system audio. It abstracts the platform differences between Electron (silent loopback) and web browsers (screen share picker) behind a unified AudioCaptureManager interface.

Terminal window
pnpm add @cosella/audio-capture
import { createCaptureManager, useMicCapture, useDeviceCapture } from '@cosella/audio-capture';
const manager = createCaptureManager(); // Auto-detects Electron vs web
function CallSetup() {
const mic = useMicCapture(manager);
const device = useDeviceCapture(manager);
const handleStart = async () => {
await mic.acquire(); // getUserMedia — works everywhere
await device.acquire(); // Electron: silent loopback; Web: screen share picker
};
return (
<div>
<p>Mic: {mic.state}</p>
<p>Device: {device.state}</p>
<button onClick={handleStart}>Start Capture</button>
</div>
);
}
CapabilityDesktop (Electron)Web (Chrome)
Mic capturegetUserMedia()getUserMedia()
Device audioSilent loopback (auto-approved)Screen share picker (user must check “Share audio”)
System audioAll system audio (WASAPI/CoreAudio)Windows: system audio; macOS/Linux: tab audio only
User interactionNone (auto-approved by main process)Required (screen share picker)

createCaptureManager() checks for window.cosella (the Electron preload bridge). If present, it returns an ElectronCaptureManager; otherwise, a WebCaptureManager.

interface AudioCaptureManager {
acquireMic(deviceId?: string): Promise<MediaStream>;
acquireDeviceAudio(): Promise<MediaStream>;
getDevices(): Promise<AudioDeviceInfo[]>;
onStateChange(cb: (event: CaptureStateEvent) => void): () => void;
getState(channel: 'mic' | 'loopback'): CaptureState;
release(channel: 'mic' | 'loopback'): void;
dispose(): void;
}
type CaptureState = 'idle' | 'requesting' | 'active' | 'ended' | 'error' | 'denied';

Extends Error with typed error codes:

CodeRecoverableMeaning
PERMISSION_DENIEDYesUser denied mic/screen permission
DEVICE_NOT_FOUNDYesSelected device unavailable
DEVICE_IN_USEYesDevice locked by another app
AUDIO_NOT_SHAREDYesUser didn’t check “Share audio” in picker
NOT_SUPPORTEDNoPlatform can’t capture this channel
const { stream, state, error, acquire, release } = useMicCapture(manager);
const { stream, state, error, acquire, release } = useDeviceCapture(manager);
const { devices, refresh } = useAudioDevices(manager);

Note: enumerateDevices() returns blank labels before mic permission is granted. Call acquire() on the mic first, then enumerate.

  • acquireDeviceAudio() must be called from a user gesture context (click/tap handler)
  • The 16kHz sample rate constraint is a hint — browsers typically deliver 48kHz. The audio-engine package handles resampling.
  • v1 assumes headphones. No echo cancellation for the loopback stream.