Files
whatsappNucleo/app/composables/useAudioRecorder.ts
josedario87 80d0042c7e
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
Feature: Agregar botón para crear webhook de debug automáticamente
- Agregar botón "Crear Webhook de Debug" en WebhookReceiverSection
- Detectar si ya existe un webhook apuntando al receptor de debug
- Permitir eliminar el webhook de debug
- Incluir todos los eventos disponibles al crear el webhook
- También incluye mejoras previas de manejo de media y mensajes
2025-12-02 21:21:33 -06:00

246 lines
6.0 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
const getMimeType = (): string => {
const types = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/ogg',
'audio/mp4',
'audio/mpeg'
]
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
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
}
}