From 84d94b6573e28b74e90ca80c33702909b822701a Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Thu, 29 May 2025 11:48:45 -0400 Subject: [PATCH 1/5] Start working on manually aquiring media --- .../src/components/VideoRecord.component.tsx | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/packages/frontend/src/components/VideoRecord.component.tsx b/packages/frontend/src/components/VideoRecord.component.tsx index d5ccb3e..cb861c3 100644 --- a/packages/frontend/src/components/VideoRecord.component.tsx +++ b/packages/frontend/src/components/VideoRecord.component.tsx @@ -13,6 +13,137 @@ export interface VideoRecordProps { } export const VideoRecord: FC = (props) => { + const videoRef = useRef(null); + const mediaRecorder = useRef(null); + const [stream, setStream] = useState(null); + const [blobs, setVideoBlobs] = useState([]); + const [countDownState, setCountDownState] = useState('paused'); + const [blobPayload, setBlobPayload] = useState<{ blobURL: string; blob: Blob } | null>(null); + const [issueFound, setIssueFound] = useState(false); + const [recording, setRecording] = useState(false); + + const getCameraPermissions = async () => { + if (!('MediaRecorder' in window)) { + // TODO: Push snackbar error + return; + } + try { + const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); + setStream(mediaStream); + } catch (error) { + // TODO: Push snackbar error + setIssueFound(true); + } + }; + + // Try to get permission + useEffect(() => { + getCameraPermissions(); + }, []); + + const startRecording = async () => { + // No stream yet, cannot start + if (!stream) { + return; + } + + // Make the recorder + const media = new MediaRecorder(stream, { mimeType: 'video/webm' }); + mediaRecorder.current = media; + + // Setup capture of blobs + const localBlobs: Blob[] = []; + mediaRecorder.current.ondataavailable = (event) => { + // Ignore empty events + if (!event || event.data.size === 0) { + return; + } + localBlobs.push(event.data); + } + setVideoBlobs(localBlobs); + + // Setup the preview + videoRef.current!.srcObject = stream; + videoRef.current!.play(); + + // Start recording + mediaRecorder.current.start(); + setRecording(true); + }; + + const handleSubmit = () => { + if (props.onSubmit && blobPayload) { + props.onSubmit(blobPayload.blobURL, blobPayload.blob); + } + }; + + const handleCompletion = (blobURL: string, blob: Blob) => { + if (props.downloadRecording) { + const link = document.createElement('a'); + link.href = blobURL; + link.download = 'teacher_tutorial.webm'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + if (props.onRecordingStop) { + props.onRecordingStop(blobURL, blob); + } + + setBlobPayload({ blobURL, blob }); + + setCountDownState('restart'); + }; + + const stopRecording = async () => { + // No media recorder to use + if (!mediaRecorder.current) { + return; + } + + // Setup the stop callback + mediaRecorder.current.onstop = () => { + const videoBlob = new Blob(blobs, { type: 'video/webm' }); + const videoURL = URL.createObjectURL(videoBlob); + handleCompletion(videoURL, videoBlob); + }; + + mediaRecorder.current.stop(); + setRecording(false); + }; + + return ( + + + + Camera Status + + + + + + + + + + + + + + + + {issueFound == false && + ); +}; + +export const VideoRecordOld: FC = (props) => { const { pushSnackbarMessage } = useSnackbar(); const recorder = useReactMediaRecorder({ video: true, From 4113b7f14ea1be53b1abf28589b05c40817632d2 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Thu, 29 May 2025 12:28:06 -0400 Subject: [PATCH 2/5] Switch up again --- .../src/components/VideoRecord.component.tsx | 256 +++++------------- 1 file changed, 73 insertions(+), 183 deletions(-) diff --git a/packages/frontend/src/components/VideoRecord.component.tsx b/packages/frontend/src/components/VideoRecord.component.tsx index cb861c3..ebe6f19 100644 --- a/packages/frontend/src/components/VideoRecord.component.tsx +++ b/packages/frontend/src/components/VideoRecord.component.tsx @@ -1,5 +1,5 @@ import { Button, Grid, Stack, Typography } from '@mui/material'; -import { FC, useEffect, useRef, useState } from 'react'; +import { FC, useEffect, useRef, useState, useCallback } from 'react'; import { useReactMediaRecorder } from 'react-media-recorder'; import { useSnackbar } from '../contexts/Snackbar.context'; import { CountDownTimer, CountDownState } from './CountDownTimer.component'; @@ -12,70 +12,16 @@ export interface VideoRecordProps { timeLimit: number; } -export const VideoRecord: FC = (props) => { +export const VideoRecord: React.FC = (props) => { const videoRef = useRef(null); - const mediaRecorder = useRef(null); - const [stream, setStream] = useState(null); - const [blobs, setVideoBlobs] = useState([]); - const [countDownState, setCountDownState] = useState('paused'); + 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 [countDownState, setCountDownState] = useState('paused'); const [issueFound, setIssueFound] = useState(false); - const [recording, setRecording] = useState(false); - - const getCameraPermissions = async () => { - if (!('MediaRecorder' in window)) { - // TODO: Push snackbar error - return; - } - try { - const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); - setStream(mediaStream); - } catch (error) { - // TODO: Push snackbar error - setIssueFound(true); - } - }; - - // Try to get permission - useEffect(() => { - getCameraPermissions(); - }, []); - - const startRecording = async () => { - // No stream yet, cannot start - if (!stream) { - return; - } - - // Make the recorder - const media = new MediaRecorder(stream, { mimeType: 'video/webm' }); - mediaRecorder.current = media; - - // Setup capture of blobs - const localBlobs: Blob[] = []; - mediaRecorder.current.ondataavailable = (event) => { - // Ignore empty events - if (!event || event.data.size === 0) { - return; - } - localBlobs.push(event.data); - } - setVideoBlobs(localBlobs); - - // Setup the preview - videoRef.current!.srcObject = stream; - videoRef.current!.play(); - - // Start recording - mediaRecorder.current.start(); - setRecording(true); - }; - - const handleSubmit = () => { - if (props.onSubmit && blobPayload) { - props.onSubmit(blobPayload.blobURL, blobPayload.blob); - } - }; const handleCompletion = (blobURL: string, blob: Blob) => { if (props.downloadRecording) { @@ -96,144 +42,85 @@ export const VideoRecord: FC = (props) => { setCountDownState('restart'); }; - const stopRecording = async () => { - // No media recorder to use - if (!mediaRecorder.current) { - return; - } - - // Setup the stop callback - mediaRecorder.current.onstop = () => { - const videoBlob = new Blob(blobs, { type: 'video/webm' }); - const videoURL = URL.createObjectURL(videoBlob); - handleCompletion(videoURL, videoBlob); - }; - - mediaRecorder.current.stop(); - setRecording(false); - }; - - return ( - - - - Camera Status - - - - - - - - - + // 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 startRecording = async () => { + // Clear the blobs + setBlobs([]); - {issueFound == false && - ); -}; + // Create the media recorder + // TODO: In the future have audio be an option + const stream: MediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); -export const VideoRecordOld: 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'); - const [blobPayload, setBlobPayload] = useState<{ blobURL: string; blob: Blob } | null>(null); - const [issueFound, setIssueFound] = useState(false); + // Setup the preview + videoRef.current!.srcObject = stream; + videoRef.current!.play(); - const handleCompletion = (blobURL: string, blob: Blob) => { - if (props.downloadRecording) { - const link = document.createElement('a'); - link.href = blobURL; - link.download = 'teacher_tutorial.webm'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } + // Set the encoding + const options = { mimeType: 'video/webm' }; - if (props.onRecordingStop) { - props.onRecordingStop(blobURL, blob); - } + // Create the media recorder + let mediaRecorder = new MediaRecorder(stream, options); - setBlobPayload({ blobURL, blob }); + mediaRecorder.ondataavailable = handleOnDataAvailable; - setCountDownState('restart'); - }; + // Start recording + mediaRecorder.start(); + setMediaRecorder(mediaRecorder); - const handleSubmit = () => { - if (props.onSubmit && blobPayload) { - props.onSubmit(blobPayload.blobURL, blobPayload.blob); - } + setCountDownState('running'); + setRecording(true); }; - const handleRecordClick = () => { - if (recorder.status == 'recording') { - recorder.stopRecording(); - } else { - recorder.startRecording(); + const stopRecording = () => { + if (mediaRecorder) { + mediaRecorder.stop(); } + setRecording(false); }; - const videoRef = useRef(null); - - // Handles switching between live preview and video playback + // Handle changes to the recording status 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; - } - }, [recorder.status, recorder.previewStream, recorder.mediaBlobUrl]); - - // Handle starting the counter - 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; + } + + // 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); } - }, [recorder.error]); + }; return ( @@ -247,8 +134,11 @@ export const VideoRecordOld: FC = (props) => { - @@ -259,7 +149,7 @@ export const VideoRecordOld: FC = (props) => { - {issueFound == false && ); From fd6c08acc7c2aabf76706c2016fc051b4de34f95 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Thu, 29 May 2025 12:32:03 -0400 Subject: [PATCH 3/5] Fix formatting --- packages/frontend/src/components/VideoRecord.component.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/frontend/src/components/VideoRecord.component.tsx b/packages/frontend/src/components/VideoRecord.component.tsx index ebe6f19..f6a9e18 100644 --- a/packages/frontend/src/components/VideoRecord.component.tsx +++ b/packages/frontend/src/components/VideoRecord.component.tsx @@ -134,10 +134,7 @@ export const VideoRecord: React.FC = (props) => { - From a8ea7d9abc92ce6c1e1fd36b56cc028e81a34bd7 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Thu, 29 May 2025 12:33:45 -0400 Subject: [PATCH 4/5] Fix build --- packages/frontend/src/components/VideoRecord.component.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/frontend/src/components/VideoRecord.component.tsx b/packages/frontend/src/components/VideoRecord.component.tsx index f6a9e18..ef48a5b 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, useCallback } 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'; From 96400d8f5d976141e6ef20abc3a9bb39a6c84858 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Thu, 29 May 2025 12:35:05 -0400 Subject: [PATCH 5/5] Fix error --- packages/frontend/src/components/VideoRecord.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/VideoRecord.component.tsx b/packages/frontend/src/components/VideoRecord.component.tsx index ef48a5b..96129bf 100644 --- a/packages/frontend/src/components/VideoRecord.component.tsx +++ b/packages/frontend/src/components/VideoRecord.component.tsx @@ -19,7 +19,7 @@ export const VideoRecord: React.FC = (props) => { stateRef.current = { blobs }; const [blobPayload, setBlobPayload] = useState<{ blobURL: string; blob: Blob } | null>(null); const [countDownState, setCountDownState] = useState('paused'); - const [issueFound, setIssueFound] = useState(false); + const [issueFound, _setIssueFound] = useState(false); const handleCompletion = (blobURL: string, blob: Blob) => { if (props.downloadRecording) {