Files
whatsappNucleo/app/composables/useAudioRecorder.ts
josedario87 f09ce37897
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m11s
Fix: Forzar mimetype ogg para notas de voz PTT
- Preferir grabacion en ogg/opus en el navegador
- Forzar mimetype a audio/ogg; codecs=opus para PTT en servidor
- WhatsApp requiere ogg para notas de voz
2025-12-04 10:24:53 -06:00

249 lines
6.3 KiB
TypeScript

/**
* 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<AudioRecorderState>({
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<boolean> => {
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
}
}