From e75384e9702f0075849d1ea552fca1b9c5cabdcc Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 23 Jan 2024 10:33:01 -0500 Subject: [PATCH 01/11] Adding in custom renderer for video recording --- .../contribute/TagForm.component.tsx | 8 +++++++ .../tag/VideoRecordField.component.tsx | 22 +++++++++++++++++++ .../src/pages/contribute/TaggingInterface.tsx | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/client/src/components/tag/VideoRecordField.component.tsx diff --git a/packages/client/src/components/contribute/TagForm.component.tsx b/packages/client/src/components/contribute/TagForm.component.tsx index 0361f8da..578149fd 100644 --- a/packages/client/src/components/contribute/TagForm.component.tsx +++ b/packages/client/src/components/contribute/TagForm.component.tsx @@ -6,6 +6,8 @@ import { Box, Stack, Button } from '@mui/material'; import { ErrorObject } from 'ajv'; import AslLexSearchControl from '../../jsonForms/customRenderes/AslLexSearchControl'; import AslLexSearchControlTester from '../../jsonForms/customRenderes/aslLexSearchControlTester'; +import VideoRecordField, { videoFieldTester } from '../tag/VideoRecordField.component'; +import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; export interface TagFormProps { study: Study; @@ -44,6 +46,12 @@ export const TagForm: React.FC = (props) => { setData({}); }; + const renderers: JsonFormsRendererRegistryEntry[] = [ + ...materialRenderers, + { tester: videoFieldTester, renderer: VideoRecordField }, + { tester: AslLexSearchControlTester, renderer: AslLexSearchControl } + ]; + return ( diff --git a/packages/client/src/components/tag/VideoRecordField.component.tsx b/packages/client/src/components/tag/VideoRecordField.component.tsx new file mode 100644 index 00000000..5ea06797 --- /dev/null +++ b/packages/client/src/components/tag/VideoRecordField.component.tsx @@ -0,0 +1,22 @@ +import { ControlProps, RankedTester, rankWith } from '@jsonforms/core'; +import { ExpandMore } from '@mui/icons-material'; +import { Accordion, AccordionSummary, Typography } from '@mui/material'; +import { withJsonFormsControlProps } from '@jsonforms/react'; + +const VideoRecordField: React.FC = (props) => { +console.log(props.label); + return ( + + }> + {props.label} + {props.description} + + + ); +}; + +export const videoFieldTester: RankedTester = rankWith(10, (uischema, _schema, _rootSchema) => { + return uischema.options != undefined && uischema.options && uischema.options.customType === 'video'; +}); + +export default withJsonFormsControlProps(VideoRecordField); diff --git a/packages/client/src/pages/contribute/TaggingInterface.tsx b/packages/client/src/pages/contribute/TaggingInterface.tsx index 81d0766f..3eebe837 100644 --- a/packages/client/src/pages/contribute/TaggingInterface.tsx +++ b/packages/client/src/pages/contribute/TaggingInterface.tsx @@ -78,7 +78,7 @@ interface MainViewProps { const MainView: React.FC = (props) => { return ( - + Date: Tue, 23 Jan 2024 10:56:18 -0500 Subject: [PATCH 02/11] Make status circles --- .../contribute/TagForm.component.tsx | 4 +- .../videorecord/StatusCircles.component.tsx | 54 +++++++++++++++++++ .../VideoRecordField.component.tsx | 9 +++- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 packages/client/src/components/tag/videorecord/StatusCircles.component.tsx rename packages/client/src/components/tag/{ => videorecord}/VideoRecordField.component.tsx (65%) diff --git a/packages/client/src/components/contribute/TagForm.component.tsx b/packages/client/src/components/contribute/TagForm.component.tsx index 578149fd..78598b9c 100644 --- a/packages/client/src/components/contribute/TagForm.component.tsx +++ b/packages/client/src/components/contribute/TagForm.component.tsx @@ -6,7 +6,7 @@ import { Box, Stack, Button } from '@mui/material'; import { ErrorObject } from 'ajv'; import AslLexSearchControl from '../../jsonForms/customRenderes/AslLexSearchControl'; import AslLexSearchControlTester from '../../jsonForms/customRenderes/aslLexSearchControlTester'; -import VideoRecordField, { videoFieldTester } from '../tag/VideoRecordField.component'; +import VideoRecordField, { videoFieldTester } from '../tag/videorecord/VideoRecordField.component'; import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; export interface TagFormProps { @@ -14,8 +14,6 @@ export interface TagFormProps { setTagData: Dispatch>; } -const renderers = [...materialRenderers, { tester: AslLexSearchControlTester, renderer: AslLexSearchControl }]; - export const TagForm: React.FC = (props) => { const [data, setData] = useState(); const [dataValid, setDataValid] = useState(false); diff --git a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx new file mode 100644 index 00000000..09a9070c --- /dev/null +++ b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx @@ -0,0 +1,54 @@ +import { Box } from '@mui/material'; + +export interface StatusProcessCirclesProps { + /** List of statuses to display */ + isComplete: boolean[]; + /** Handle when a user clicks a circle */ + setState: (index: number) => void; + /** The active index */ + activeIndex: number; +}; + +export const StatusProcessCircles: React.FC = (props) => { + return ( + + {props.isComplete.map((isComplete, index) => ( + props.setState(index)} + activeIndex={props.activeIndex} + index={index} + /> + ))} + + ); +}; + +interface StatusProcessCircleProps { + /** Whether the circle is complete */ + isComplete: boolean; + /** Handle when a user clicks a circle */ + onClick: () => void; + /** The active index */ + activeIndex: number; + /** The index of the circle */ + index: number; +} + +const StatusProcessCircle: React.FC = (props) => { + return ( + + ); +} diff --git a/packages/client/src/components/tag/VideoRecordField.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx similarity index 65% rename from packages/client/src/components/tag/VideoRecordField.component.tsx rename to packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx index 5ea06797..a381811d 100644 --- a/packages/client/src/components/tag/VideoRecordField.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx @@ -1,16 +1,21 @@ import { ControlProps, RankedTester, rankWith } from '@jsonforms/core'; import { ExpandMore } from '@mui/icons-material'; -import { Accordion, AccordionSummary, Typography } from '@mui/material'; +import { Accordion, AccordionDetails, AccordionSummary, Typography, Stack } from '@mui/material'; import { withJsonFormsControlProps } from '@jsonforms/react'; +import { StatusProcessCircles } from './StatusCircles.component'; const VideoRecordField: React.FC = (props) => { -console.log(props.label); return ( }> {props.label} {props.description} + + + {}} activeIndex={2} /> + + ); }; From 016882b14ec92b96664a4e6cc8d94facf32bb01c Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 23 Jan 2024 13:50:52 -0500 Subject: [PATCH 03/11] UI elements present for video recording --- .../videorecord/StatusCircles.component.tsx | 6 ++- .../VideoRecordField.component.tsx | 54 +++++++++++++++++-- .../VideoRecordInterface.component.tsx | 22 ++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx diff --git a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx index 09a9070c..fc35cd03 100644 --- a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx +++ b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx @@ -37,11 +37,13 @@ interface StatusProcessCircleProps { } const StatusProcessCircle: React.FC = (props) => { + const circleSize = '50px'; + return ( = (props) => { + const [maxVideos, setMaxVideos] = useState(0); + const [minimumVideos, setMinimumVideos] = useState(0); + const [validVideos, setValidVideos] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + const [blobs, setBlobs] = useState<(Blob | null)[]>([]); + + useEffect(() => { + if (!props.uischema.options?.minimumRequired) { + console.error('Minimum number of videos required not specified'); + return; + } + const minimumVideos = props.uischema.options.minimumRequired; + + let maxVideos = minimumVideos; + if (props.uischema.options?.maximumOptional) { + maxVideos = props.uischema.options.maximumOptional; + } + + setValidVideos(Array.from({ length: maxVideos }, (_, _i) => false)); + setMinimumVideos(minimumVideos); + setMaxVideos(maxVideos); + setBlobs(Array.from({ length: maxVideos }, (_, _i) => null)); + }, [props.uischema]); + + const handleVideoRecord = (video: Blob) => { + blobs[activeIndex] = video; + setBlobs(blobs); + + validVideos[activeIndex] = true; + setValidVideos(validVideos); + }; + return ( }> @@ -12,8 +46,20 @@ const VideoRecordField: React.FC = (props) => { {props.description} - - {}} activeIndex={2} /> + + Required: {minimumVideos}, Optional Max: {maxVideos} + {}} activeIndex={activeIndex} /> + + + {/* Left navigation button */} + + + + + {/* Right navigation button */} + + + diff --git a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx new file mode 100644 index 00000000..3773ba94 --- /dev/null +++ b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx @@ -0,0 +1,22 @@ +import { useEffect, useRef } from 'react'; + +export const VideoRecordInterface: React.FC = () => { + const videoRef = useRef(null); + + useEffect(() => { + if (!videoRef.current) { + return; + } + + navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => { + videoRef.current!.srcObject = stream; + videoRef.current!.play(); + }); + }, []); + + return ( + <> + - + diff --git a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx index 3773ba94..cec48087 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx @@ -1,18 +1,82 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; -export const VideoRecordInterface: React.FC = () => { +export interface VideoRecordInterfaceProps { + activeBlob: Blob | null; + recordVideo: (blob: Blob | null) => void; + recording: boolean; +} + +export const VideoRecordInterface: React.FC = (props) => { const videoRef = useRef(null); + const [mediaRecorder, setMediaRecorder] = useState(null); + const [blobs, setBlobs] = useState([]); + + // On data available, store the blob + const handleOnDataAvailable = useCallback((event: BlobEvent) => { + console.log('New Blob', event.data); + blobs.push(event.data); + setBlobs(blobs); + if (!props.recording) { + props.recordVideo(new Blob(blobs, { type: 'video/webm' })); + } + }, [setBlobs]); + + const startRecording = async () => { + // Clear the blobs + setBlobs([]); + + // Create the media recorder + // TODO: In the future have audio be an option + const stream: MediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); + + // Setup the preview + videoRef.current!.srcObject = stream; + videoRef.current!.play(); + + // Set the encoding + const options = { mimeType: 'video/webm; codecs=vp9' }; + + // Create the media recorder + let mediaRecorder = new MediaRecorder(stream, options); + + mediaRecorder.ondataavailable = handleOnDataAvailable; + + // Start recording + mediaRecorder.start(); + setMediaRecorder(mediaRecorder); + }; + + const stopRecording = () => { + if (mediaRecorder) { + mediaRecorder.stop(); + } + }; + + // Handle changes to the recording status + useEffect(() => { + console.log('Recording status changed to: ' + props.recording); + if (props.recording) { + startRecording(); + } else { + stopRecording(); + } + }, [props.recording]); + // Control the display based on if an active blob is present useEffect(() => { - if (!videoRef.current) { + console.log('Active blob changed to: ', props.activeBlob); + // If there is no active blob, show the video preview + if (!props.activeBlob) { + videoRef.current!.style.display = 'block'; + videoRef.current!.src = ''; return; } - navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => { - videoRef.current!.srcObject = stream; - videoRef.current!.play(); - }); - }, []); + // Otherwise show the recording blobl + const blobUrl = URL.createObjectURL(props.activeBlob); + videoRef.current!.srcObject = null; + videoRef.current!.src = blobUrl; + }, [props]); return ( <> From 8ac64413acf9a813342a3095c57a3fb848661b21 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 24 Jan 2024 12:24:56 -0500 Subject: [PATCH 05/11] Ability to re-record videos --- .../VideoRecordField.component.tsx | 33 ++++++++++++++----- .../VideoRecordInterface.component.tsx | 19 ++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx index 97100708..949c5139 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx @@ -4,7 +4,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Typography, Stack, Butto import { withJsonFormsControlProps } from '@jsonforms/react'; import { StatusProcessCircles } from './StatusCircles.component'; import { VideoRecordInterface } from './VideoRecordInterface.component'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; const VideoRecordField: React.FC = (props) => { const [maxVideos, setMaxVideos] = useState(0); @@ -13,6 +13,8 @@ const VideoRecordField: React.FC = (props) => { const [activeIndex, setActiveIndex] = useState(0); const [blobs, setBlobs] = useState<(Blob | null)[]>([]); const [recording, setRecording] = useState(false); + const stateRef = useRef<{ validVideos: boolean[], blobs: (Blob | null)[]}>(); + stateRef.current = { validVideos, blobs }; useEffect(() => { if (!props.uischema.options?.minimumRequired) { @@ -26,19 +28,34 @@ const VideoRecordField: React.FC = (props) => { maxVideos = props.uischema.options.maximumOptional; } + console.log('Minimum videos', minimumVideos, maxVideos); + setValidVideos(Array.from({ length: maxVideos }, (_, _i) => false)); setMinimumVideos(minimumVideos); setMaxVideos(maxVideos); setBlobs(Array.from({ length: maxVideos }, (_, _i) => null)); }, [props.uischema]); - const handleVideoRecord = (video: Blob | null) => { - blobs[activeIndex] = video; - validVideos[activeIndex] = !!video; + const handleVideoRecord = (video: Blob | null, blobs: (Blob | null)[], validVideos: boolean[]) => { + console.log('Video', video); + console.log('Original, blobs', stateRef.current!.blobs); + const updatedBlobs = stateRef.current!.blobs.map((blob, index) => { + if (index === activeIndex) { + return video; + } + return blob; + }); + const updateValidVideos = stateRef.current!.validVideos.map((valid, index) => { + if (index === activeIndex) { + return video !== null; + } + return valid; + }); + + console.log('Updated blobs', updatedBlobs); - setBlobs(blobs); - setValidVideos(validVideos); - console.log(blobs); + setBlobs(updatedBlobs); + setValidVideos(updateValidVideos); }; return ( @@ -56,7 +73,7 @@ const VideoRecordField: React.FC = (props) => { {/* Left navigation button */} - + handleVideoRecord(blob, blobs, validVideos)} recording={recording} /> {/* Right navigation button */} diff --git a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx index cec48087..051742d9 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx @@ -10,16 +10,20 @@ export const VideoRecordInterface: React.FC = (props) const videoRef = useRef(null); const [mediaRecorder, setMediaRecorder] = useState(null); const [blobs, setBlobs] = useState([]); + const stateRef = useRef<{ blobs: Blob[] }>(); + stateRef.current = { blobs }; // On data available, store the blob const handleOnDataAvailable = useCallback((event: BlobEvent) => { - console.log('New Blob', event.data); - blobs.push(event.data); - setBlobs(blobs); + const newBlobs = [...stateRef.current!.blobs, event.data]; + setBlobs(newBlobs); + + // If the recording is complete, send the blob to the parent if (!props.recording) { - props.recordVideo(new Blob(blobs, { type: 'video/webm' })); + console.log('Recording complete'); + props.recordVideo(new Blob(newBlobs, { type: 'video/webm' })); } - }, [setBlobs]); + }, [setBlobs, blobs]); const startRecording = async () => { // Clear the blobs @@ -54,7 +58,6 @@ export const VideoRecordInterface: React.FC = (props) // Handle changes to the recording status useEffect(() => { - console.log('Recording status changed to: ' + props.recording); if (props.recording) { startRecording(); } else { @@ -64,7 +67,7 @@ export const VideoRecordInterface: React.FC = (props) // Control the display based on if an active blob is present useEffect(() => { - console.log('Active blob changed to: ', props.activeBlob); + console.log('Blob Changed', props.activeBlob); // If there is no active blob, show the video preview if (!props.activeBlob) { videoRef.current!.style.display = 'block'; @@ -76,7 +79,7 @@ export const VideoRecordInterface: React.FC = (props) const blobUrl = URL.createObjectURL(props.activeBlob); videoRef.current!.srcObject = null; videoRef.current!.src = blobUrl; - }, [props]); + }, [props.activeBlob]); return ( <> From d983bc185eab2551e8ccc6b67dc4ae03deba82ec Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 24 Jan 2024 12:26:11 -0500 Subject: [PATCH 06/11] Code cleanup --- .../tag/videorecord/VideoRecordField.component.tsx | 11 +++-------- .../videorecord/VideoRecordInterface.component.tsx | 2 -- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx index 949c5139..c1231372 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx @@ -4,7 +4,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Typography, Stack, Butto import { withJsonFormsControlProps } from '@jsonforms/react'; import { StatusProcessCircles } from './StatusCircles.component'; import { VideoRecordInterface } from './VideoRecordInterface.component'; -import { useEffect, useState, useCallback, useRef } from 'react'; +import { useEffect, useState, useRef } from 'react'; const VideoRecordField: React.FC = (props) => { const [maxVideos, setMaxVideos] = useState(0); @@ -28,7 +28,6 @@ const VideoRecordField: React.FC = (props) => { maxVideos = props.uischema.options.maximumOptional; } - console.log('Minimum videos', minimumVideos, maxVideos); setValidVideos(Array.from({ length: maxVideos }, (_, _i) => false)); setMinimumVideos(minimumVideos); @@ -36,9 +35,7 @@ const VideoRecordField: React.FC = (props) => { setBlobs(Array.from({ length: maxVideos }, (_, _i) => null)); }, [props.uischema]); - const handleVideoRecord = (video: Blob | null, blobs: (Blob | null)[], validVideos: boolean[]) => { - console.log('Video', video); - console.log('Original, blobs', stateRef.current!.blobs); + const handleVideoRecord = (video: Blob | null) => { const updatedBlobs = stateRef.current!.blobs.map((blob, index) => { if (index === activeIndex) { return video; @@ -52,8 +49,6 @@ const VideoRecordField: React.FC = (props) => { return valid; }); - console.log('Updated blobs', updatedBlobs); - setBlobs(updatedBlobs); setValidVideos(updateValidVideos); }; @@ -73,7 +68,7 @@ const VideoRecordField: React.FC = (props) => { {/* Left navigation button */} - handleVideoRecord(blob, blobs, validVideos)} recording={recording} /> + handleVideoRecord(blob)} recording={recording} /> {/* Right navigation button */} diff --git a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx index 051742d9..e65e7647 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx @@ -20,7 +20,6 @@ export const VideoRecordInterface: React.FC = (props) // If the recording is complete, send the blob to the parent if (!props.recording) { - console.log('Recording complete'); props.recordVideo(new Blob(newBlobs, { type: 'video/webm' })); } }, [setBlobs, blobs]); @@ -67,7 +66,6 @@ export const VideoRecordInterface: React.FC = (props) // Control the display based on if an active blob is present useEffect(() => { - console.log('Blob Changed', props.activeBlob); // If there is no active blob, show the video preview if (!props.activeBlob) { videoRef.current!.style.display = 'block'; From 8c55e31c48edd140095df4837cc42593f4480fc4 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 24 Jan 2024 12:40:17 -0500 Subject: [PATCH 07/11] Working ability to move between videos --- .../videorecord/StatusCircles.component.tsx | 1 + .../VideoRecordField.component.tsx | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx index fc35cd03..3874f767 100644 --- a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx +++ b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx @@ -10,6 +10,7 @@ export interface StatusProcessCirclesProps { }; export const StatusProcessCircles: React.FC = (props) => { + console.log(props.isComplete); return ( {props.isComplete.map((isComplete, index) => ( diff --git a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx index c1231372..33c9cb6a 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx @@ -13,8 +13,8 @@ const VideoRecordField: React.FC = (props) => { const [activeIndex, setActiveIndex] = useState(0); const [blobs, setBlobs] = useState<(Blob | null)[]>([]); const [recording, setRecording] = useState(false); - const stateRef = useRef<{ validVideos: boolean[], blobs: (Blob | null)[]}>(); - stateRef.current = { validVideos, blobs }; + const stateRef = useRef<{ validVideos: boolean[], blobs: (Blob | null)[], activeIndex: number}>(); + stateRef.current = { validVideos, blobs, activeIndex }; useEffect(() => { if (!props.uischema.options?.minimumRequired) { @@ -37,13 +37,13 @@ const VideoRecordField: React.FC = (props) => { const handleVideoRecord = (video: Blob | null) => { const updatedBlobs = stateRef.current!.blobs.map((blob, index) => { - if (index === activeIndex) { + if (index === stateRef.current!.activeIndex) { return video; } return blob; }); const updateValidVideos = stateRef.current!.validVideos.map((valid, index) => { - if (index === activeIndex) { + if (index === stateRef.current!.activeIndex) { return video !== null; } return valid; @@ -66,12 +66,20 @@ const VideoRecordField: React.FC = (props) => { {/* Left navigation button */} - + setActiveIndex(activeIndex - 1)} + > handleVideoRecord(blob)} recording={recording} /> {/* Right navigation button */} - + setActiveIndex(activeIndex + 1)} + > From fa63d4829a5771a780559fe8e8ebd174748983b7 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 24 Jan 2024 12:41:59 -0500 Subject: [PATCH 08/11] Limit users ability to go next before completing previous video --- .../components/tag/videorecord/VideoRecordField.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx index 33c9cb6a..3de3e6c5 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx @@ -77,7 +77,7 @@ const VideoRecordField: React.FC = (props) => { {/* Right navigation button */} setActiveIndex(activeIndex + 1)} > From ab013ed904c0dd529651b8ba7c49322cfdb2ffa0 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 24 Jan 2024 12:57:27 -0500 Subject: [PATCH 09/11] Fix formatting --- .../videorecord/StatusCircles.component.tsx | 10 +++--- .../VideoRecordField.component.tsx | 33 +++++++++++-------- .../VideoRecordInterface.component.tsx | 23 +++++++------ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx index 3874f767..0f94e8d9 100644 --- a/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx +++ b/packages/client/src/components/tag/videorecord/StatusCircles.component.tsx @@ -1,4 +1,4 @@ -import { Box } from '@mui/material'; +import { Box, Stack } from '@mui/material'; export interface StatusProcessCirclesProps { /** List of statuses to display */ @@ -7,12 +7,12 @@ export interface StatusProcessCirclesProps { setState: (index: number) => void; /** The active index */ activeIndex: number; -}; +} export const StatusProcessCircles: React.FC = (props) => { console.log(props.isComplete); return ( - + {props.isComplete.map((isComplete, index) => ( = (props) index={index} /> ))} - + ); }; @@ -54,4 +54,4 @@ const StatusProcessCircle: React.FC = (props) => { onClick={props.onClick} /> ); -} +}; diff --git a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx index 3de3e6c5..95bab254 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordField.component.tsx @@ -13,7 +13,7 @@ const VideoRecordField: React.FC = (props) => { const [activeIndex, setActiveIndex] = useState(0); const [blobs, setBlobs] = useState<(Blob | null)[]>([]); const [recording, setRecording] = useState(false); - const stateRef = useRef<{ validVideos: boolean[], blobs: (Blob | null)[], activeIndex: number}>(); + const stateRef = useRef<{ validVideos: boolean[]; blobs: (Blob | null)[]; activeIndex: number }>(); stateRef.current = { validVideos, blobs, activeIndex }; useEffect(() => { @@ -28,7 +28,6 @@ const VideoRecordField: React.FC = (props) => { maxVideos = props.uischema.options.maximumOptional; } - setValidVideos(Array.from({ length: maxVideos }, (_, _i) => false)); setMinimumVideos(minimumVideos); setMaxVideos(maxVideos); @@ -61,27 +60,35 @@ const VideoRecordField: React.FC = (props) => { - Required: {minimumVideos}, Optional Max: {maxVideos} + + Required: {minimumVideos}, Optional Max: {maxVideos} + {}} activeIndex={activeIndex} /> - + {/* Left navigation button */} - setActiveIndex(activeIndex - 1)} - > + setActiveIndex(activeIndex - 1)}> + + - handleVideoRecord(blob)} recording={recording} /> + handleVideoRecord(blob)} + recording={recording} + /> {/* Right navigation button */} setActiveIndex(activeIndex + 1)} - > + > + + - + diff --git a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx index e65e7647..26a4a6f3 100644 --- a/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx +++ b/packages/client/src/components/tag/videorecord/VideoRecordInterface.component.tsx @@ -14,15 +14,18 @@ export const VideoRecordInterface: React.FC = (props) stateRef.current = { blobs }; // 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 (!props.recording) { - props.recordVideo(new Blob(newBlobs, { type: 'video/webm' })); - } - }, [setBlobs, blobs]); + 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 (!props.recording) { + props.recordVideo(new Blob(newBlobs, { type: 'video/webm' })); + } + }, + [setBlobs, blobs] + ); const startRecording = async () => { // Clear the blobs @@ -81,7 +84,7 @@ export const VideoRecordInterface: React.FC = (props) return ( <> -