import { useState, useRef, useCallback, useEffect } from 'react'; import { isTauri } from '@/lib/tauri'; interface UseAudioRecordingOptions { maxDurationSeconds?: number; onRecordingComplete?: (blob: Blob) => void; } export function useAudioRecording({ maxDurationSeconds = 30, onRecordingComplete, }: UseAudioRecordingOptions = {}) { const [isRecording, setIsRecording] = useState(false); const [duration, setDuration] = useState(0); const [error, setError] = useState(null); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const streamRef = useRef(null); const timerRef = useRef(null); const startTimeRef = useRef(null); const startRecording = useCallback(async () => { try { setError(null); chunksRef.current = []; setDuration(0); // Check if getUserMedia is available // In Tauri, navigator.mediaDevices might not be available immediately if (typeof navigator === 'undefined') { const errorMsg = 'Navigator API is not available. This might be a Tauri configuration issue.'; setError(errorMsg); throw new Error(errorMsg); } if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { // Try waiting a bit for Tauri webview to initialize await new Promise((resolve) => setTimeout(resolve, 100)); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { const isTauriEnv = isTauri(); console.error('MediaDevices check:', { hasNavigator: typeof navigator !== 'undefined', hasMediaDevices: !!navigator?.mediaDevices, hasGetUserMedia: !!navigator?.mediaDevices?.getUserMedia, isTauri: isTauriEnv, }); const errorMsg = isTauriEnv ? 'Microphone access is not available. Please ensure:\n1. The app has microphone permissions in System Settings (macOS: System Settings > Privacy & Security > Microphone)\n2. You restart the app after granting permissions\n3. You are using Tauri v2 with a webview that supports getUserMedia' : 'Microphone access is not available. Please ensure you are using a secure context (HTTPS or localhost) and that your browser has microphone permissions enabled.'; setError(errorMsg); throw new Error(errorMsg); } } // Request microphone access const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); streamRef.current = stream; // Create MediaRecorder with preferred MIME type const options: MediaRecorderOptions = { mimeType: 'audio/webm;codecs=opus', }; // Fallback to default if webm not supported if (!MediaRecorder.isTypeSupported(options.mimeType!)) { delete options.mimeType; } const mediaRecorder = new MediaRecorder(stream, options); mediaRecorderRef.current = mediaRecorder; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { chunksRef.current.push(event.data); } }; mediaRecorder.onstop = () => { const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); onRecordingComplete?.(blob); // Stop all tracks streamRef.current?.getTracks().forEach((track) => { track.stop(); }); streamRef.current = null; }; mediaRecorder.onerror = (event) => { setError('Recording error occurred'); console.error('MediaRecorder error:', event); }; // Start recording mediaRecorder.start(100); // Collect data every 100ms setIsRecording(true); startTimeRef.current = Date.now(); // Start timer timerRef.current = window.setInterval(() => { if (startTimeRef.current) { const elapsed = (Date.now() - startTimeRef.current) / 1000; setDuration(elapsed); // Auto-stop at max duration if (elapsed >= maxDurationSeconds) { if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { mediaRecorderRef.current.stop(); setIsRecording(false); if (timerRef.current !== null) { clearInterval(timerRef.current); timerRef.current = null; } } } } }, 100); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to access microphone. Please check permissions.'; setError(errorMessage); setIsRecording(false); } }, [maxDurationSeconds, onRecordingComplete]); const stopRecording = useCallback(() => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); if (timerRef.current !== null) { clearInterval(timerRef.current); timerRef.current = null; } } }, [isRecording]); const cancelRecording = useCallback(() => { if (mediaRecorderRef.current) { mediaRecorderRef.current.stop(); setIsRecording(false); chunksRef.current = []; setDuration(0); } // Stop all tracks streamRef.current?.getTracks().forEach((track) => { track.stop(); }); streamRef.current = null; if (timerRef.current !== null) { clearInterval(timerRef.current); timerRef.current = null; } }, []); // Cleanup on unmount useEffect(() => { return () => { if (timerRef.current !== null) { clearInterval(timerRef.current); } streamRef.current?.getTracks().forEach((track) => { track.stop(); }); }; }, []); return { isRecording, duration, error, startRecording, stopRecording, cancelRecording, }; }