Spaces:
Sleeping
Sleeping
// Audio recording and playback functionality using Web Audio API | |
let mediaRecorder = null; | |
let audioChunks = []; | |
let recordingStream = null; | |
let recordingStartTime = null; | |
let recordingTimer = null; | |
let isRecording = false; | |
// Initialize audio recording functionality | |
document.addEventListener('DOMContentLoaded', () => { | |
if (document.body.classList.contains('chat-page')) { | |
initializeAudioRecording(); | |
} | |
}); | |
async function initializeAudioRecording() { | |
try { | |
// Check if getUserMedia is supported | |
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { | |
console.warn('getUserMedia not supported'); | |
return; | |
} | |
console.log('Audio recording initialized'); | |
} catch (error) { | |
console.error('Error initializing audio recording:', error); | |
} | |
} | |
async function toggleAudioRecording() { | |
if (isRecording) { | |
stopAudioRecording(); | |
} else { | |
startAudioRecording(); | |
} | |
} | |
async function startAudioRecording() { | |
if (!window.currentConversation) { | |
MainJS.showError('Please select a conversation first'); | |
return; | |
} | |
try { | |
// Request microphone permission | |
recordingStream = await navigator.mediaDevices.getUserMedia({ | |
audio: { | |
echoCancellation: true, | |
noiseSuppression: true, | |
sampleRate: 44100 | |
} | |
}); | |
// Create MediaRecorder | |
const options = { | |
mimeType: 'audio/webm;codecs=opus' | |
}; | |
// Fallback to other formats if webm is not supported | |
if (!MediaRecorder.isTypeSupported(options.mimeType)) { | |
options.mimeType = 'audio/webm'; | |
if (!MediaRecorder.isTypeSupported(options.mimeType)) { | |
options.mimeType = 'audio/mp4'; | |
if (!MediaRecorder.isTypeSupported(options.mimeType)) { | |
options.mimeType = 'audio/wav'; | |
} | |
} | |
} | |
mediaRecorder = new MediaRecorder(recordingStream, options); | |
audioChunks = []; | |
// Set up event handlers | |
mediaRecorder.ondataavailable = (event) => { | |
if (event.data.size > 0) { | |
audioChunks.push(event.data); | |
} | |
}; | |
mediaRecorder.onstop = () => { | |
handleRecordingStop(); | |
}; | |
mediaRecorder.onerror = (event) => { | |
console.error('MediaRecorder error:', event.error); | |
MainJS.showError('Recording failed: ' + event.error.message); | |
resetRecordingUI(); | |
}; | |
// Start recording | |
mediaRecorder.start(100); // Collect data every 100ms | |
isRecording = true; | |
recordingStartTime = Date.now(); | |
// Update UI | |
updateRecordingUI(true); | |
// Start timer | |
startRecordingTimer(); | |
console.log('Audio recording started'); | |
} catch (error) { | |
console.error('Error starting audio recording:', error); | |
if (error.name === 'NotAllowedError') { | |
MainJS.showError('Microphone access denied. Please allow microphone access to record voice messages.'); | |
} else if (error.name === 'NotFoundError') { | |
MainJS.showError('No microphone found. Please connect a microphone and try again.'); | |
} else { | |
MainJS.showError('Failed to start recording: ' + error.message); | |
} | |
resetRecordingUI(); | |
} | |
} | |
function stopAudioRecording() { | |
if (!isRecording || !mediaRecorder) { | |
return; | |
} | |
try { | |
mediaRecorder.stop(); | |
isRecording = false; | |
// Stop all tracks | |
if (recordingStream) { | |
recordingStream.getTracks().forEach(track => track.stop()); | |
recordingStream = null; | |
} | |
// Stop timer | |
if (recordingTimer) { | |
clearInterval(recordingTimer); | |
recordingTimer = null; | |
} | |
console.log('Audio recording stopped'); | |
} catch (error) { | |
console.error('Error stopping audio recording:', error); | |
MainJS.showError('Failed to stop recording'); | |
resetRecordingUI(); | |
} | |
} | |
function cancelAudioRecording() { | |
if (isRecording && mediaRecorder) { | |
mediaRecorder.stop(); | |
isRecording = false; | |
// Stop all tracks | |
if (recordingStream) { | |
recordingStream.getTracks().forEach(track => track.stop()); | |
recordingStream = null; | |
} | |
// Stop timer | |
if (recordingTimer) { | |
clearInterval(recordingTimer); | |
recordingTimer = null; | |
} | |
// Clear chunks | |
audioChunks = []; | |
// Reset UI | |
resetRecordingUI(); | |
console.log('Audio recording cancelled'); | |
} | |
} | |
async function handleRecordingStop() { | |
if (audioChunks.length === 0) { | |
console.warn('No audio data recorded'); | |
resetRecordingUI(); | |
return; | |
} | |
try { | |
// Create blob from chunks | |
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); | |
const duration = (Date.now() - recordingStartTime) / 1000; // Duration in seconds | |
// Validate minimum duration | |
if (duration < 0.5) { | |
MainJS.showError('Recording too short. Please record for at least 0.5 seconds.'); | |
resetRecordingUI(); | |
return; | |
} | |
// Validate maximum duration (5 minutes) | |
if (duration > 300) { | |
MainJS.showError('Recording too long. Maximum duration is 5 minutes.'); | |
resetRecordingUI(); | |
return; | |
} | |
console.log(`Audio recorded: ${duration.toFixed(2)} seconds, size: ${audioBlob.size} bytes`); | |
// Upload audio | |
await uploadAudioMessage(audioBlob, duration); | |
} catch (error) { | |
console.error('Error handling recording stop:', error); | |
MainJS.showError('Failed to process recording'); | |
} finally { | |
resetRecordingUI(); | |
} | |
} | |
async function uploadAudioMessage(audioBlob, duration) { | |
if (!window.currentConversation) { | |
MainJS.showError('No conversation selected'); | |
return; | |
} | |
try { | |
// Create form data | |
const formData = new FormData(); | |
formData.append('audio', audioBlob, 'voice_message.webm'); | |
formData.append('conversation_id', window.currentConversation); | |
formData.append('duration', duration.toString()); | |
// Show uploading indicator | |
MainJS.showSuccess('Sending voice message...'); | |
// Upload audio | |
const response = await fetch('/api/upload_audio', { | |
method: 'POST', | |
body: formData | |
}); | |
const result = await response.json(); | |
if (result.success) { | |
MainJS.showSuccess('Voice message sent!'); | |
// Reload messages and conversations | |
await loadMessages(window.currentConversation); | |
await loadConversations(); | |
} else { | |
MainJS.showError('Failed to send voice message: ' + result.message); | |
} | |
} catch (error) { | |
console.error('Error uploading audio:', error); | |
MainJS.showError('Failed to send voice message'); | |
} | |
} | |
function startRecordingTimer() { | |
recordingTimer = setInterval(() => { | |
if (!isRecording) return; | |
const elapsed = (Date.now() - recordingStartTime) / 1000; | |
const minutes = Math.floor(elapsed / 60); | |
const seconds = Math.floor(elapsed % 60); | |
const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
const timeElement = document.getElementById('recordingTime'); | |
if (timeElement) { | |
timeElement.textContent = timeString; | |
} | |
// Auto-stop at 5 minutes | |
if (elapsed >= 300) { | |
stopAudioRecording(); | |
} | |
}, 100); | |
} | |
function updateRecordingUI(recording) { | |
const audioButton = document.getElementById('audioButton'); | |
const audioRecording = document.getElementById('audioRecording'); | |
const messageForm = document.getElementById('messageForm'); | |
if (!audioButton || !audioRecording || !messageForm) return; | |
if (recording) { | |
audioButton.innerHTML = '<i class="fas fa-stop text-danger"></i>'; | |
audioButton.classList.add('btn-danger'); | |
audioButton.classList.remove('btn-outline-success'); | |
audioRecording.style.display = 'block'; | |
messageForm.style.display = 'none'; | |
} else { | |
resetRecordingUI(); | |
} | |
} | |
function resetRecordingUI() { | |
const audioButton = document.getElementById('audioButton'); | |
const audioRecording = document.getElementById('audioRecording'); | |
const messageForm = document.getElementById('messageForm'); | |
const recordingTime = document.getElementById('recordingTime'); | |
if (audioButton) { | |
audioButton.innerHTML = '<i class="fas fa-microphone"></i>'; | |
audioButton.classList.remove('btn-danger'); | |
audioButton.classList.add('btn-outline-success'); | |
} | |
if (audioRecording) { | |
audioRecording.style.display = 'none'; | |
} | |
if (messageForm) { | |
messageForm.style.display = 'flex'; | |
} | |
if (recordingTime) { | |
recordingTime.textContent = '00:00'; | |
} | |
} | |
// Audio playback functionality | |
const audioElements = new Map(); | |
async function playAudioMessage(messageId) { | |
try { | |
// Stop any currently playing audio | |
audioElements.forEach(audio => { | |
if (!audio.paused) { | |
audio.pause(); | |
audio.currentTime = 0; | |
} | |
}); | |
// Get or create audio element for this message | |
let audio = audioElements.get(messageId); | |
if (!audio) { | |
// Fetch audio data | |
const response = await fetch(`/api/download/${messageId}`); | |
if (!response.ok) { | |
throw new Error('Failed to load audio'); | |
} | |
const blob = await response.blob(); | |
const audioUrl = URL.createObjectURL(blob); | |
// Create audio element | |
audio = new Audio(audioUrl); | |
audioElements.set(messageId, audio); | |
// Update play button when audio ends | |
audio.addEventListener('ended', () => { | |
updateAudioButton(messageId, false); | |
URL.revokeObjectURL(audioUrl); | |
audioElements.delete(messageId); | |
}); | |
// Handle errors | |
audio.addEventListener('error', (e) => { | |
console.error('Audio playback error:', e); | |
MainJS.showError('Failed to play audio message'); | |
updateAudioButton(messageId, false); | |
URL.revokeObjectURL(audioUrl); | |
audioElements.delete(messageId); | |
}); | |
} | |
// Toggle play/pause | |
if (audio.paused) { | |
updateAudioButton(messageId, true); | |
await audio.play(); | |
} else { | |
audio.pause(); | |
updateAudioButton(messageId, false); | |
} | |
} catch (error) { | |
console.error('Error playing audio message:', error); | |
MainJS.showError('Failed to play audio message'); | |
} | |
} | |
function updateAudioButton(messageId, playing) { | |
const button = document.querySelector(`[onclick*="${messageId}"]`); | |
if (button) { | |
const icon = button.querySelector('i'); | |
if (icon) { | |
if (playing) { | |
icon.className = 'fas fa-pause'; | |
button.style.background = '#128c7e'; | |
} else { | |
icon.className = 'fas fa-play'; | |
button.style.background = '#25d366'; | |
} | |
} | |
} | |
} | |
// Cleanup audio elements on page unload | |
window.addEventListener('beforeunload', () => { | |
audioElements.forEach(audio => { | |
if (!audio.paused) { | |
audio.pause(); | |
} | |
// URLs will be automatically revoked when the page unloads | |
}); | |
audioElements.clear(); | |
}); | |
// Export functions for global access | |
window.AudioJS = { | |
toggleAudioRecording, | |
cancelAudioRecording, | |
stopAudioRecording, | |
playAudioMessage | |
}; | |