Spaces:
Sleeping
Sleeping
Download frontend/src/hooks/useInterviewRecorder.js from chowdary1/ai-interviewer-version-2: direct link, hf CLI and curl.
- Browser
- Download file 9.37 kB
-
https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/hooks/useInterviewRecorder.js
- Command line
-
hf download hf://spaces/chowdary1/ai-interviewer-version-2/frontend/src/hooks/useInterviewRecorder.js
-
curl -L -o useInterviewRecorder.js https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/hooks/useInterviewRecorder.js
9.37 kB
| /** | |
| * Interview Recording Hook | |
| * | |
| * Records user video + audio during the interview using MediaRecorder API. | |
| * Auto-starts and stops with the interview. | |
| * | |
| * Features: | |
| * - Records video + audio from webcam | |
| * - Stores recording as Blob | |
| * - Provides download capability | |
| * - Upload-ready structure for backend | |
| */ | |
| import { useState, useRef, useCallback, useEffect } from 'react'; | |
| const MIME_TYPES = [ | |
| 'video/webm;codecs=vp9', | |
| 'video/webm;codecs=vp8', | |
| 'video/webm', | |
| 'video/mp4', | |
| ]; | |
| function getSupportedMimeType() { | |
| for (const mimeType of MIME_TYPES) { | |
| if (MediaRecorder.isTypeSupported(mimeType)) { | |
| return mimeType; | |
| } | |
| } | |
| return 'video/webm'; | |
| } | |
| export default function useInterviewRecorder() { | |
| const [isRecording, setIsRecording] = useState(false); | |
| const [isPaused, setIsPaused] = useState(false); | |
| const [recordingDuration, setRecordingDuration] = useState(0); | |
| const [recordingBlob, setRecordingBlob] = useState(null); | |
| const [error, setError] = useState(null); | |
| const [isSupported, setIsSupported] = useState(true); | |
| const mediaRecorderRef = useRef(null); | |
| const chunksRef = useRef([]); | |
| const streamRef = useRef(null); | |
| const durationIntervalRef = useRef(null); | |
| const startTimeRef = useRef(null); | |
| const stopResolverRef = useRef(null); // Promise resolver for stopRecording | |
| // Check browser support | |
| useEffect(() => { | |
| setIsSupported( | |
| typeof MediaRecorder !== 'undefined' && | |
| navigator.mediaDevices && | |
| typeof navigator.mediaDevices.getUserMedia === 'function' | |
| ); | |
| }, []); | |
| // Cleanup on unmount | |
| useEffect(() => { | |
| return () => { | |
| stopRecording(); | |
| if (durationIntervalRef.current) { | |
| clearInterval(durationIntervalRef.current); | |
| } | |
| }; | |
| }, []); | |
| /** | |
| * Start recording with existing stream or request new one | |
| */ | |
| const startRecording = useCallback(async (existingStream = null) => { | |
| if (!isSupported) { | |
| setError('Recording is not supported in this browser'); | |
| return false; | |
| } | |
| try { | |
| setError(null); | |
| chunksRef.current = []; | |
| // Use existing stream or request new one | |
| let stream = existingStream; | |
| if (!stream) { | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| width: { ideal: 1280, max: 1920 }, | |
| height: { ideal: 720, max: 1080 }, | |
| facingMode: 'user', | |
| }, | |
| audio: { | |
| echoCancellation: true, | |
| noiseSuppression: true, | |
| sampleRate: 44100, | |
| }, | |
| }); | |
| } | |
| streamRef.current = stream; | |
| const mimeType = getSupportedMimeType(); | |
| const recorder = new MediaRecorder(stream, { | |
| mimeType, | |
| videoBitsPerSecond: 2500000, // 2.5 Mbps | |
| audioBitsPerSecond: 128000, // 128 Kbps | |
| }); | |
| recorder.ondataavailable = (event) => { | |
| if (event.data && event.data.size > 0) { | |
| chunksRef.current.push(event.data); | |
| } | |
| }; | |
| recorder.onstop = () => { | |
| let blob = null; | |
| if (chunksRef.current.length > 0) { | |
| blob = new Blob(chunksRef.current, { type: mimeType }); | |
| setRecordingBlob(blob); | |
| } | |
| setIsRecording(false); | |
| setIsPaused(false); | |
| // Resolve the stop promise with the blob | |
| if (stopResolverRef.current) { | |
| stopResolverRef.current(blob); | |
| stopResolverRef.current = null; | |
| } | |
| }; | |
| recorder.onerror = (event) => { | |
| console.error('MediaRecorder error:', event.error); | |
| setError(`Recording error: ${event.error?.message || 'Unknown error'}`); | |
| setIsRecording(false); | |
| }; | |
| mediaRecorderRef.current = recorder; | |
| recorder.start(1000); // Record in 1-second chunks | |
| setIsRecording(true); | |
| setIsPaused(false); | |
| startTimeRef.current = Date.now(); | |
| // Track duration | |
| durationIntervalRef.current = setInterval(() => { | |
| if (startTimeRef.current) { | |
| setRecordingDuration(Math.floor((Date.now() - startTimeRef.current) / 1000)); | |
| } | |
| }, 1000); | |
| console.log('[Recording] Started with', mimeType); | |
| return true; | |
| } catch (err) { | |
| console.error('Failed to start recording:', err); | |
| setError(err.message || 'Failed to start recording'); | |
| return false; | |
| } | |
| }, [isSupported]); | |
| /** | |
| * Stop recording - Returns a Promise that resolves with the blob when ready | |
| */ | |
| const stopRecording = useCallback(() => { | |
| return new Promise((resolve) => { | |
| if (durationIntervalRef.current) { | |
| clearInterval(durationIntervalRef.current); | |
| durationIntervalRef.current = null; | |
| } | |
| if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { | |
| // Store the resolver to be called in onstop | |
| stopResolverRef.current = resolve; | |
| try { | |
| mediaRecorderRef.current.stop(); | |
| } catch (e) { | |
| console.warn('Error stopping recorder:', e); | |
| resolve(null); | |
| } | |
| } else { | |
| // Already stopped, resolve immediately | |
| setIsRecording(false); | |
| setIsPaused(false); | |
| resolve(null); | |
| } | |
| }); | |
| }, []); | |
| /** | |
| * Pause recording | |
| */ | |
| const pauseRecording = useCallback(() => { | |
| if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { | |
| mediaRecorderRef.current.pause(); | |
| setIsPaused(true); | |
| } | |
| }, []); | |
| /** | |
| * Resume recording | |
| */ | |
| const resumeRecording = useCallback(() => { | |
| if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'paused') { | |
| mediaRecorderRef.current.resume(); | |
| setIsPaused(false); | |
| } | |
| }, []); | |
| /** | |
| * Download the recording | |
| */ | |
| const downloadRecording = useCallback((filename = 'interview-recording.webm') => { | |
| if (!recordingBlob) return; | |
| const url = URL.createObjectURL(recordingBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, [recordingBlob]); | |
| /** | |
| * Get recording as File for upload | |
| */ | |
| const getRecordingFile = useCallback((filename = 'interview-recording.webm') => { | |
| if (!recordingBlob) return null; | |
| return new File([recordingBlob], filename, { | |
| type: recordingBlob.type, | |
| lastModified: Date.now(), | |
| }); | |
| }, [recordingBlob]); | |
| /** | |
| * Upload recording to backend | |
| */ | |
| const uploadRecording = useCallback(async (sessionId, apiEndpoint = '/api/interviews/recording') => { | |
| if (!recordingBlob) { | |
| throw new Error('No recording available'); | |
| } | |
| const formData = new FormData(); | |
| formData.append('recording', recordingBlob, `interview-${sessionId}.webm`); | |
| formData.append('session_id', sessionId); | |
| formData.append('duration', recordingDuration.toString()); | |
| try { | |
| const response = await fetch(apiEndpoint, { | |
| method: 'POST', | |
| body: formData, | |
| credentials: 'include', | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Upload failed: ${response.status}`); | |
| } | |
| return await response.json(); | |
| } catch (err) { | |
| console.error('Upload failed:', err); | |
| throw err; | |
| } | |
| }, [recordingBlob, recordingDuration]); | |
| /** | |
| * Clear recording data | |
| */ | |
| const clearRecording = useCallback(() => { | |
| setRecordingBlob(null); | |
| setRecordingDuration(0); | |
| chunksRef.current = []; | |
| }, []); | |
| /** | |
| * Format duration as MM:SS | |
| */ | |
| const formatDuration = useCallback((seconds) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| }, []); | |
| return { | |
| // State | |
| isRecording, | |
| isPaused, | |
| recordingDuration, | |
| recordingBlob, | |
| error, | |
| isSupported, | |
| formattedDuration: formatDuration(recordingDuration), | |
| // Actions | |
| startRecording, | |
| stopRecording, | |
| pauseRecording, | |
| resumeRecording, | |
| downloadRecording, | |
| getRecordingFile, | |
| uploadRecording, | |
| clearRecording, | |
| }; | |
| } | |