import { ref, readonly, type Ref } from 'vue' /** * Push-to-talk recorder wrapping the browser MediaRecorder API. * * Usage: * const { recording, error, isSupported, startRecording, stopRecording } = useVoiceRecorder() * await startRecording() * const blob = await stopRecording() // resolves with the recorded audio Blob */ export function useVoiceRecorder() { const recording = ref(false) const error = ref(null) const isSupported = typeof MediaRecorder !== 'undefined' && !!navigator.mediaDevices?.getUserMedia let mediaRecorder: MediaRecorder | null = null let chunks: Blob[] = [] const streamRef = ref(null) let resolveStop: ((blob: Blob) => void) | null = null let rejectStop: ((err: Error) => void) | null = null async function startRecording(): Promise { error.value = null if (!isSupported) { error.value = 'Audio recording is not supported in this browser' return } if (recording.value) return try { streamRef.value = await navigator.mediaDevices.getUserMedia({ audio: true }) } catch (e) { error.value = 'Microphone access denied' return } chunks = [] const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : '' mediaRecorder = mimeType ? new MediaRecorder(streamRef.value!, { mimeType }) : new MediaRecorder(streamRef.value!) mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data) } mediaRecorder.onstop = () => { const blob = new Blob(chunks, { type: mediaRecorder?.mimeType ?? 'audio/webm' }) chunks = [] streamRef.value?.getTracks().forEach((t) => t.stop()) streamRef.value = null recording.value = false resolveStop?.(blob) resolveStop = null rejectStop = null } mediaRecorder.onerror = () => { recording.value = false error.value = 'Recording error' rejectStop?.(new Error('MediaRecorder error')) resolveStop = null rejectStop = null } mediaRecorder.start(100) // collect in 100ms chunks recording.value = true } function stopRecording(): Promise { return new Promise((resolve, reject) => { if (!mediaRecorder || !recording.value) { reject(new Error('Not recording')) return } resolveStop = resolve rejectStop = reject mediaRecorder.stop() }) } return { recording: readonly(recording), error: readonly(error), isSupported, startRecording, stopRecording, stream: readonly(streamRef) as Readonly>, } }