From f1043734438c8518a302f698d9ae6583a75f0c14 Mon Sep 17 00:00:00 2001 From: cashmere Date: Sun, 16 Aug 2026 06:16:56 +0200 Subject: feat: add transitions, track mixing, and voiceover controls --- apps/web/src/voiceover/devices.ts | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 apps/web/src/voiceover/devices.ts (limited to 'apps/web/src/voiceover/devices.ts') diff --git a/apps/web/src/voiceover/devices.ts b/apps/web/src/voiceover/devices.ts new file mode 100644 index 00000000..dd5b680c --- /dev/null +++ b/apps/web/src/voiceover/devices.ts @@ -0,0 +1,93 @@ +"use client"; + +export const DEFAULT_AUDIO_INPUT_DEVICE_ID = "default"; + +export interface VoiceOverInputDevice { + deviceId: string; + label: string; + isDefault: boolean; +} + +export function buildVoiceOverAudioConstraints({ + deviceId, +}: { + deviceId?: string | null; +}): MediaStreamConstraints { + const audio: MediaTrackConstraints = { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }; + + if (deviceId && deviceId !== DEFAULT_AUDIO_INPUT_DEVICE_ID) { + audio.deviceId = { exact: deviceId }; + } + + return { audio }; +} + +export function normalizeSelectedDeviceId({ + deviceId, + devices, +}: { + deviceId: string | null; + devices: VoiceOverInputDevice[]; +}): string | null { + if (!deviceId) { + return null; + } + + if (deviceId === DEFAULT_AUDIO_INPUT_DEVICE_ID) { + return deviceId; + } + + return devices.some((device) => device.deviceId === deviceId) + ? deviceId + : null; +} + +export async function listVoiceOverInputDevices({ + requestPermission = false, +}: { + requestPermission?: boolean; +} = {}): Promise { + const mediaDevices = navigator.mediaDevices; + if (!mediaDevices?.enumerateDevices) { + throw new Error("Microphone selection is not supported in this browser"); + } + + let permissionStream: MediaStream | null = null; + if (requestPermission) { + permissionStream = await mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + }); + } + + try { + const devices = await mediaDevices.enumerateDevices(); + let inputIndex = 1; + return devices + .filter((device) => device.kind === "audioinput") + .map((device) => { + const isDefault = device.deviceId === DEFAULT_AUDIO_INPUT_DEVICE_ID; + const fallbackLabel = isDefault + ? "System default microphone" + : `Microphone ${inputIndex++}`; + return { + deviceId: device.deviceId, + label: device.label || fallbackLabel, + isDefault, + }; + }); + } finally { + if (permissionStream) { + for (const track of permissionStream.getTracks()) { + track.stop(); + } + } + } +} -- cgit v1.3.1