Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 80 additions & 64 deletions packages/frontend/src/components/VideoRecord.component.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,19 +10,16 @@ export interface VideoRecordProps {
timeLimit: number;
}

export const VideoRecord: FC<VideoRecordProps> = (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<CountDownState>('paused');
export const VideoRecord: React.FC<VideoRecordProps> = (props) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
const [blobs, setBlobs] = useState<Blob[]>([]);
const [recording, setRecording] = useState<boolean>(false);
const stateRef = useRef<{ blobs: Blob[] }>(null);
stateRef.current = { blobs };
const [blobPayload, setBlobPayload] = useState<{ blobURL: string; blob: Blob } | null>(null);
const [issueFound, setIssueFound] = useState<boolean>(false);
const [countDownState, setCountDownState] = useState<CountDownState>('paused');
const [issueFound, _setIssueFound] = useState<boolean>(false);

const handleCompletion = (blobURL: string, blob: Blob) => {
if (props.downloadRecording) {
Expand All @@ -45,64 +40,85 @@ export const VideoRecord: FC<VideoRecordProps> = (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<HTMLVideoElement>(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 (
<Stack padding={3} spacing={3}>
Expand All @@ -116,8 +132,8 @@ export const VideoRecord: FC<VideoRecordProps> = (props) => {
</Grid>

<Grid size={6}>
<Button variant="contained" onClick={handleRecordClick}>
{recorder.status == 'recording' ? 'Stop Recording' : 'Start Recording'}
<Button variant="contained" onClick={() => (recording ? stopRecording() : startRecording())}>
{recording ? 'Stop Recording' : 'Start Recording'}
</Button>
</Grid>

Expand All @@ -128,7 +144,7 @@ export const VideoRecord: FC<VideoRecordProps> = (props) => {
</Grid>
</Grid>

{issueFound == false && <video src={recorder.mediaBlobUrl} controls autoPlay loop ref={videoRef} />}
{issueFound == false && <video controls autoPlay loop ref={videoRef} />}
{issueFound && <ResolvePermissionError />}
</Stack>
);
Expand Down
Loading