/** * Composable for recording audio (voice messages) * Uses MediaRecorder API to capture audio from microphone */ interface AudioRecorderState { isRecording: boolean isPaused: boolean duration: number audioBlob: Blob | null audioUrl: string | null error: string | null } export function useAudioRecorder() { const state = reactive({ isRecording: false, isPaused: false, duration: 0, audioBlob: null, audioUrl: null, error: null }) let mediaRecorder: MediaRecorder | null = null let audioChunks: Blob[] = [] let stream: MediaStream | null = null let durationInterval: NodeJS.Timeout | null = null let startTime: number = 0 // Check if browser supports audio recording const isSupported = computed(() => { return typeof navigator !== 'undefined' && navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function' }) // Get preferred MIME type for recording // Prefer ogg/opus as it's required by WhatsApp for voice notes const getMimeType = (): string => { const types = [ 'audio/ogg;codecs=opus', // Preferred for WhatsApp PTT 'audio/ogg', 'audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/mpeg' ] for (const type of types) { if (MediaRecorder.isTypeSupported(type)) { console.log('[AudioRecorder] Using MIME type:', type) return type } } console.warn('[AudioRecorder] No preferred type supported, using webm fallback') return 'audio/webm' // fallback } // Start recording const startRecording = async (): Promise => { if (!isSupported.value) { state.error = 'La grabación de audio no está soportada en este navegador' return false } try { // Reset state state.error = null state.audioBlob = null state.audioUrl = null audioChunks = [] // Request microphone access stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }) // Create MediaRecorder const mimeType = getMimeType() mediaRecorder = new MediaRecorder(stream, { mimeType }) // Handle data available mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.push(event.data) } } // Handle recording stop mediaRecorder.onstop = () => { // Create blob from chunks const mimeType = mediaRecorder?.mimeType || 'audio/webm' state.audioBlob = new Blob(audioChunks, { type: mimeType }) state.audioUrl = URL.createObjectURL(state.audioBlob) // Stop all tracks if (stream) { stream.getTracks().forEach(track => track.stop()) stream = null } // Clear interval if (durationInterval) { clearInterval(durationInterval) durationInterval = null } state.isRecording = false state.isPaused = false } // Handle errors mediaRecorder.onerror = (event: any) => { console.error('MediaRecorder error:', event.error) state.error = 'Error durante la grabación' stopRecording() } // Start recording mediaRecorder.start(100) // Collect data every 100ms state.isRecording = true startTime = Date.now() // Update duration every 100ms durationInterval = setInterval(() => { if (state.isRecording && !state.isPaused) { state.duration = Math.floor((Date.now() - startTime) / 1000) } }, 100) return true } catch (error: any) { console.error('Error starting recording:', error) if (error.name === 'NotAllowedError') { state.error = 'Permiso de micrófono denegado' } else if (error.name === 'NotFoundError') { state.error = 'No se encontró micrófono' } else { state.error = 'Error al iniciar la grabación' } return false } } // Stop recording and finalize const stopRecording = (): void => { if (mediaRecorder && state.isRecording) { mediaRecorder.stop() } } // Cancel recording without saving const cancelRecording = (): void => { if (mediaRecorder && state.isRecording) { mediaRecorder.stop() } // Clean up without keeping the audio state.audioBlob = null state.audioUrl = null state.duration = 0 if (stream) { stream.getTracks().forEach(track => track.stop()) stream = null } if (durationInterval) { clearInterval(durationInterval) durationInterval = null } audioChunks = [] state.isRecording = false state.isPaused = false } // Pause recording const pauseRecording = (): void => { if (mediaRecorder && mediaRecorder.state === 'recording') { mediaRecorder.pause() state.isPaused = true } } // Resume recording const resumeRecording = (): void => { if (mediaRecorder && mediaRecorder.state === 'paused') { mediaRecorder.resume() state.isPaused = false } } // Convert blob to File for upload const getAudioFile = (filename?: string): File | null => { if (!state.audioBlob) return null const extension = state.audioBlob.type.includes('ogg') ? 'ogg' : 'webm' const name = filename || `audio-${Date.now()}.${extension}` return new File([state.audioBlob], name, { type: state.audioBlob.type }) } // Clear recorded audio const clearAudio = (): void => { if (state.audioUrl) { URL.revokeObjectURL(state.audioUrl) } state.audioBlob = null state.audioUrl = null state.duration = 0 state.error = null } // Cleanup on unmount onUnmounted(() => { cancelRecording() clearAudio() }) return { // State isRecording: computed(() => state.isRecording), isPaused: computed(() => state.isPaused), duration: computed(() => state.duration), audioBlob: computed(() => state.audioBlob), audioUrl: computed(() => state.audioUrl), error: computed(() => state.error), isSupported, // Methods startRecording, stopRecording, cancelRecording, pauseRecording, resumeRecording, getAudioFile, clearAudio } }