Feature: Agregar botón para crear webhook de debug automáticamente
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
- 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
This commit is contained in:
245
app/composables/useAudioRecorder.ts
Normal file
245
app/composables/useAudioRecorder.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user