ai-interviewer-version-2 / frontend /src /hooks /useInterviewRecorder.js
chowdary1's picture
Deploy AI Interviewer Pro Max v1.1 - Clean build with psutil fix
86d71bd
Raw History Blame Contribute Delete
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,
};
}