diff --git a/packages/frontend/src/components/VideoRecord.component.tsx b/packages/frontend/src/components/VideoRecord.component.tsx index d5ccb3e..96129bf 100644 --- a/packages/frontend/src/components/VideoRecord.component.tsx +++ b/packages/frontend/src/components/VideoRecord.component.tsx @@ -1,7 +1,5 @@ import { Button, Grid, Stack, Typography } from '@mui/material'; -import { FC, useEffect, useRef, useState } from 'react'; -import { useReactMediaRecorder } from 'react-media-recorder'; -import { useSnackbar } from '../contexts/Snackbar.context'; +import { useEffect, useRef, useState, useCallback } from 'react'; import { CountDownTimer, CountDownState } from './CountDownTimer.component'; import { ResolvePermissionError } from './ResolvePermissionError.component'; @@ -12,19 +10,16 @@ export interface VideoRecordProps { timeLimit: number; } -export const VideoRecord: FC = (props) => { - const { pushSnackbarMessage } = useSnackbar(); - const recorder = useReactMediaRecorder({ - video: true, - audio: true, - mediaRecorderOptions: { - mimeType: 'video/webm' - }, - onStop: (mediaBlobUrl, blob) => handleCompletion(mediaBlobUrl, blob) - }); - const [countDownState, setCountDownState] = useState('paused'); +export const VideoRecord: React.FC = (props) => { + const videoRef = useRef(null); + const [mediaRecorder, setMediaRecorder] = useState(null); + const [blobs, setBlobs] = useState([]); + const [recording, setRecording] = useState(false); + const stateRef = useRef<{ blobs: Blob[] }>(null); + stateRef.current = { blobs }; const [blobPayload, setBlobPayload] = useState<{ blobURL: string; blob: Blob } | null>(null); - const [issueFound, setIssueFound] = useState(false); + const [countDownState, setCountDownState] = useState('paused'); + const [issueFound, _setIssueFound] = useState(false); const handleCompletion = (blobURL: string, blob: Blob) => { if (props.downloadRecording) { @@ -45,64 +40,85 @@ export const VideoRecord: FC = (props) => { setCountDownState('restart'); }; - const handleSubmit = () => { - if (props.onSubmit && blobPayload) { - props.onSubmit(blobPayload.blobURL, blobPayload.blob); - } - }; + // On data available, store the blob + const handleOnDataAvailable = useCallback( + (event: BlobEvent) => { + const newBlobs = [...stateRef.current!.blobs, event.data]; + setBlobs(newBlobs); + + // If the recording is complete, send the blob to the parent + if (!recording) { + const blob = new Blob(newBlobs, { type: 'video/webm' }); + const blobURL = URL.createObjectURL(blob); + handleCompletion(blobURL, blob); + } + }, + [setBlobs, blobs] + ); - const handleRecordClick = () => { - if (recorder.status == 'recording') { - recorder.stopRecording(); - } else { - recorder.startRecording(); - } - }; + const startRecording = async () => { + // Clear the blobs + setBlobs([]); - const videoRef = useRef(null); + // Create the media recorder + // TODO: In the future have audio be an option + const stream: MediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); - // Handles switching between live preview and video playback - useEffect(() => { - // If in recording mode, show the user the preview - if (videoRef.current && recorder.previewStream && recorder.status == 'recording') { - videoRef.current.srcObject = recorder.previewStream; - } - // Otherwise, show the user the recording video - else if (videoRef.current && recorder.mediaBlobUrl) { - videoRef.current.src = recorder.mediaBlobUrl; - videoRef.current.srcObject = null; + // Setup the preview + videoRef.current!.srcObject = stream; + videoRef.current!.play(); + + // Set the encoding + const options = { mimeType: 'video/webm' }; + + // Create the media recorder + let mediaRecorder = new MediaRecorder(stream, options); + + mediaRecorder.ondataavailable = handleOnDataAvailable; + + // Start recording + mediaRecorder.start(); + setMediaRecorder(mediaRecorder); + + setCountDownState('running'); + setRecording(true); + }; + + const stopRecording = () => { + if (mediaRecorder) { + mediaRecorder.stop(); } - }, [recorder.status, recorder.previewStream, recorder.mediaBlobUrl]); + setRecording(false); + }; - // Handle starting the counter + // Handle changes to the recording status useEffect(() => { - if (recorder.status == 'recording') { - setCountDownState('running'); + if (recording) { setTimeout(() => { - recorder.stopRecording(); + stopRecording(); }, props.timeLimit * 1000); } - }, [recorder.status]); + }, [recording]); - // Error message handling + // Control the display based on if an active blob is present useEffect(() => { - switch (recorder.error) { - case 'permission_denied': - pushSnackbarMessage( - 'You have denied camera or microphone permissions to this site. You must enable permissions to record your video successfully', - 'error' - ); - setIssueFound(true); - break; - case 'media_in_use': - pushSnackbarMessage( - 'Your camera or microphone is already in use. You must close other apps accessing them in order to record your video successfully', - 'error' - ); - setIssueFound(true); - break; + // If there is no active blob, show the video preview + if (!blobPayload) { + videoRef.current!.style.display = 'block'; + videoRef.current!.src = ''; + return; } - }, [recorder.error]); + + // Otherwise show the recording blobl + videoRef.current!.srcObject = null; + videoRef.current!.src = blobPayload.blobURL; + }, [blobPayload]); + + const handleSubmit = () => { + if (props.onSubmit && blobPayload) { + props.onSubmit(blobPayload.blobURL, blobPayload.blob); + } + }; return ( @@ -116,8 +132,8 @@ export const VideoRecord: FC = (props) => { - @@ -128,7 +144,7 @@ export const VideoRecord: FC = (props) => { - {issueFound == false && );