// 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 = ''; 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 = ''; 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 };