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/recorder.ts | 40 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) (limited to 'apps/web/src/voiceover/recorder.ts') diff --git a/apps/web/src/voiceover/recorder.ts b/apps/web/src/voiceover/recorder.ts index b71ca74f..48322898 100644 --- a/apps/web/src/voiceover/recorder.ts +++ b/apps/web/src/voiceover/recorder.ts @@ -5,6 +5,8 @@ * actual microphone stream. */ +import { buildVoiceOverAudioConstraints } from "@/voiceover/devices"; + export const PREFERRED_MIME_TYPES = [ "audio/webm;codecs=opus", "audio/webm", @@ -95,6 +97,11 @@ interface ActiveRecording { startedAtMs: number; } +export interface VoiceOverRecordingSession { + stream: MediaStream; + mimeType: string; +} + let activeRecording: ActiveRecording | null = null; function stopStreamTracks({ stream }: { stream: MediaStream }): void { @@ -103,7 +110,15 @@ function stopStreamTracks({ stream }: { stream: MediaStream }): void { } } -export async function startVoiceOverRecording(): Promise { +export function getActiveVoiceOverRecordingStream(): MediaStream | null { + return activeRecording?.stream ?? null; +} + +export async function startVoiceOverRecording({ + deviceId, +}: { + deviceId?: string | null; +} = {}): Promise { if (activeRecording) { throw new Error("A voice-over recording is already in progress"); } @@ -113,7 +128,24 @@ export async function startVoiceOverRecording(): Promise { throw new Error("Voice-over recording is not supported in this browser"); } - const stream = await mediaDevices.getUserMedia({ audio: true }); + let stream: MediaStream; + try { + stream = await mediaDevices.getUserMedia( + buildVoiceOverAudioConstraints({ deviceId }), + ); + } catch (error) { + if ( + deviceId && + error instanceof DOMException && + (error.name === "NotFoundError" || error.name === "OverconstrainedError") + ) { + stream = await mediaDevices.getUserMedia( + buildVoiceOverAudioConstraints({ deviceId: null }), + ); + } else { + throw error; + } + } try { const mimeType = pickSupportedMimeType({ isTypeSupported: (candidate) => MediaRecorder.isTypeSupported(candidate), @@ -135,6 +167,10 @@ export async function startVoiceOverRecording(): Promise { mimeType: recorder.mimeType || mimeType || "audio/webm", startedAtMs: performance.now(), }; + return { + stream, + mimeType: activeRecording.mimeType, + }; } catch (error) { stopStreamTracks({ stream }); throw error; -- cgit v1.3.1