diff options
-rw-r--r-- | src/client/util/RecordingApi.ts | 70 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.tsx | 442 |
2 files changed, 248 insertions, 264 deletions
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts index 009652f6e..12c1654a2 100644 --- a/src/client/util/RecordingApi.ts +++ b/src/client/util/RecordingApi.ts @@ -58,13 +58,7 @@ export class RecordingApi { } public start = (meta?: Object) => { - // check if already init a presentation - if (!this.isInitPresenation) { - console.log(this.currentPresentation) - console.trace('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.') - } - - // update the presentation mode + // update the presentation mode Doc.UserDoc().presentationMode = 'recording'; // (1a) get start date for presenation @@ -81,12 +75,10 @@ export class RecordingApi { } /* stops the video and returns the presentatation; if no presentation, returns undefined */ - public* yieldPresentation(clearData: boolean = true): Generator<Presentation | null> { - // TODO: maybe archive the data? - // if (this.tracking) console.warn('[recordingApi.ts] getPresentation() : currently recording presentation.'); + public * yieldPresentation(clearData: boolean = true): Generator<Presentation | null> { + // if no presentation or done tracking, return null + if (!this.isInitPresenation || !this.tracking) return null; - // update the presentation mode - // Doc.UserDoc().presentationMode = 'none'; // set the previus recording view to the play view this.playFFView = this.recordingFFView; @@ -103,9 +95,12 @@ export class RecordingApi { } public clear = (): void => { - // clear the disposeFunc if we are done (not recording) - if (!this.tracking) - this.removeRecordingFFView() + // clear the disposeFunc if we are done (not tracking) + if (!this.tracking) { + this.removeRecordingFFView(); + // update the presentation mode now that we are done tracking + Doc.UserDoc().presentationMode = 'none'; + } // clear presenation data this.currentPresentation = RecordingApi.NULL_PRESENTATION // clear isRecording @@ -114,7 +109,6 @@ export class RecordingApi { this.absoluteStart = -1 } - // call on dispose function to stop tracking movements public removeRecordingFFView = (): void => { this.disposeFunc?.(); @@ -150,7 +144,8 @@ export class RecordingApi { return new Error('[recordingApi.ts] trackMovements(): no presentation') } - console.log('track movment') + // TO FIX: bob + // console.debug('track movment') // get the time const time = new Date().getTime() - this.absoluteStart @@ -262,35 +257,30 @@ export class RecordingApi { }) } - // make a public method that concatenates the movements of the an array of presentations into one array - // TODO: consider the meta data of the presentations + // method that concatenates an array of presentatations into one public concatPresentations = (presentations: Presentation[]): Presentation => { - console.table(presentations); - if (presentations.length === 0) return RecordingApi.NULL_PRESENTATION; - const firstPresentation = presentations[0]; - - let sumTime = firstPresentation.totalTime; - let combinedPresentations = { ...firstPresentation } - presentations.forEach((presentation, i) => { - // already consider the first presentation - if (i === 0) return; - - const { movements, totalTime } = presentation; - if (movements === null) return; - - // add the summed time to the movements - const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } }); - // concat the movements already in the combined presentation with these new ones - const newMovements = [...combinedPresentations.movements || [], ...addedTimeMovements]; - - combinedPresentations = { ...combinedPresentations, movements: newMovements } - + // these three will lead to the combined presentation + let combinedMovements: Movement[] = []; + let sumTime = 0; + let combinedMetas: any[] = []; + + presentations.forEach((presentation) => { + const { movements, totalTime, meta } = presentation; + // update movements if they had one + if (movements) { + // add the summed time to the movements + const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } }); + // concat the movements already in the combined presentation with these new ones + combinedMovements.push(...addedTimeMovements); + } // update the totalTime sumTime += totalTime; + // concatenate the metas + combinedMetas.push(...meta); }); // return the combined presentation with the updated total summed time - return { ...combinedPresentations, totalTime: sumTime }; + return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; } // Unfinished code for tracing multiple free form views diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index be9f342bb..35a6aa07e 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -11,270 +11,264 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Presentation, RecordingApi } from '../../../util/RecordingApi'; export interface MediaSegment { - videoChunks: any[], - endTime: number, + videoChunks: any[], + endTime: number, startTime: number, presentation?: Presentation, } interface IRecordingViewProps { - setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void - setDuration: (seconds: number) => void - id: string + setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void + setDuration: (seconds: number) => void + id: string } const MAXTIME = 100000; export function RecordingView(props: IRecordingViewProps) { - const [recording, setRecording] = useState(false); - const recordingTimerRef = useRef<number>(0); - const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second - const [playing, setPlaying] = useState(false); - const [progress, setProgress] = useState(0); - - // acts as a "refresh state" to tell progressBar when to undo - const [doUndo, setDoUndo] = useState(false); - // whether an undo can occur or not - const [canUndo, setCanUndo] = useState(false); - - const [videos, setVideos] = useState<MediaSegment[]>([]); - const [orderVideos, setOrderVideos] = useState<boolean>(false); - const videoRecorder = useRef<MediaRecorder | null>(null); - const videoElementRef = useRef<HTMLVideoElement | null>(null); - - const [finished, setFinished] = useState<boolean>(false) - const [trackScreen, setTrackScreen] = useState<boolean>(true) - - - - const DEFAULT_MEDIA_CONSTRAINTS = { - video: { - width: 1280, - height: 720, - }, - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 44100 - } - } - - // useEffect(() => console.info('progress', progress), [progress]) + const [recording, setRecording] = useState(false); + const recordingTimerRef = useRef<number>(0); + const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second + const [playing, setPlaying] = useState(false); + const [progress, setProgress] = useState(0); + // acts as a "refresh state" to tell progressBar when to undo + const [doUndo, setDoUndo] = useState(false); + // whether an undo can occur or not + const [canUndo, setCanUndo] = useState(false); - useEffect(() => { - if (finished) { - // make the total presentation that'll match the concatted video - const concatPres = RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); - console.log('concatPres', concatPres); + const [videos, setVideos] = useState<MediaSegment[]>([]); + const [orderVideos, setOrderVideos] = useState<boolean>(false); + const videoRecorder = useRef<MediaRecorder | null>(null); + const videoElementRef = useRef<HTMLVideoElement | null>(null); - // this async function uses the server to create the concatted video and then sets the result to it's accessPaths - (async () => { - const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); + const [finished, setFinished] = useState<boolean>(false) + const [trackScreen, setTrackScreen] = useState<boolean>(true) - // upload the segments to the server and get their server access paths - const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)) - .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server) - // concat the segments together using post call - const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); - !(result instanceof Error) ? props.setResult(result, concatPres) : console.error("video conversion failed"); - })(); - } - }, [videos]) - // this will call upon the progress bar to edit videos to be in the correct order - useEffect(() => { - finished && setOrderVideos(true); - }, [finished]) + const DEFAULT_MEDIA_CONSTRAINTS = { + video: { + width: 1280, + height: 720, + }, + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 44100 + } + } - // check if the browser supports media devices on first load - useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []) + // useEffect(() => console.debug('progress', progress), [progress]) useEffect(() => { - console.log('recording useEffect', recording) - let interval: any = null; - if (recording) { - interval = setInterval(() => { - setRecordingTimer(unit => unit + 1); - }, 10); - } else if (!recording && recordingTimer !== 0) { - clearInterval(interval); - } - return () => clearInterval(interval); - }, [recording]) - - useEffect(() => { - setVideoProgressHelper(recordingTimer) - recordingTimerRef.current = recordingTimer; - }, [recordingTimer]) - - const setVideoProgressHelper = (progress: number) => { - const newProgress = (progress / MAXTIME) * 100; - setProgress(newProgress) + if (finished) { + // make the total presentation that'll match the concatted video + let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); + + // this async function uses the server to create the concatted video and then sets the result to it's accessPaths + (async () => { + const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); + + // upload the segments to the server and get their server access paths + const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)) + .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server) + + // concat the segments together using post call + const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); + !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed"); + })(); } + }, [videos]) - const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { - const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) + // this will call upon the progress bar to edit videos to be in the correct order + useEffect(() => { + finished && setOrderVideos(true); + }, [finished]) - videoElementRef.current!.src = "" - videoElementRef.current!.srcObject = stream - videoElementRef.current!.muted = true + // check if the browser supports media devices on first load + useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []) - return stream + useEffect(() => { + let interval: any = null; + if (recording) { + interval = setInterval(() => { + setRecordingTimer(unit => unit + 1); + }, 10); + } else if (!recording && recordingTimer !== 0) { + clearInterval(interval); } + return () => clearInterval(interval); + }, [recording]) - const record = async () => { - // don't need to start a new stream every time we start recording a new segment - if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream()) - - // temporary chunks of video - let videoChunks: any = [] - - videoRecorder.current.ondataavailable = (event: any) => { - if (event.data.size > 0) videoChunks.push(event.data) - } - - videoRecorder.current.onstart = (event: any) => { - setRecording(true); - // trackScreen && RecordingApi.Instance.start(); - trackScreen && RecordingApi.Instance.start(); - } - - videoRecorder.current.onstop = () => { - // RecordingApi.Instance.stop(); - // if we have a last portion - if (videoChunks.length > 1) { - const presentation = RecordingApi.Instance.yieldPresentation().next().value || undefined - console.log('presenation yield', JSON.parse(JSON.stringify(presentation))) - // append the current portion to the video pieces - setVideos(videos => [...videos, { - videoChunks: videoChunks, - endTime: recordingTimerRef.current, - startTime: videos?.lastElement()?.endTime || 0, - // RecordingApi.stop() will return undefined if no track screen - presentation - }]) - // now that we got the presentation data, we can clear for the next segment to be recorded - // RecordingApi.Instance.clear(); - } - - // reset the temporary chunks - videoChunks = [] - setRecording(false); - } - - videoRecorder.current.start(200) - } + useEffect(() => { + setVideoProgressHelper(recordingTimer) + recordingTimerRef.current = recordingTimer; + }, [recordingTimer]) + const setVideoProgressHelper = (progress: number) => { + const newProgress = (progress / MAXTIME) * 100; + setProgress(newProgress) + } - // if this is called, then we're done recording all the segments - const finish = (e: React.PointerEvent) => { - e.stopPropagation(); + const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { + const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) - // call stop on the video recorder if active - videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop(); + videoElementRef.current!.src = "" + videoElementRef.current!.srcObject = stream + videoElementRef.current!.muted = true - // end the streams (audio/video) to remove recording icon - const stream = videoElementRef.current!.srcObject; - stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); - - // clear the recoringApi - RecordingApi.Instance.clear(); + return stream + } - // this will call upon progessbar to update videos to be in the correct order - setFinished(true); - } + const record = async () => { + // don't need to start a new stream every time we start recording a new segment + if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream()) + + // temporary chunks of video + let videoChunks: any = [] - const pause = (e: React.PointerEvent) => { - e.stopPropagation() - // if recording, then this is just a new segment - videoRecorder.current?.state === "recording" && videoRecorder.current.stop(); + videoRecorder.current.ondataavailable = (event: any) => { + if (event.data.size > 0) videoChunks.push(event.data) } - const start = (e: React.PointerEvent) => { - // the code to start or resume does not get triggered if we start dragging the button - setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { - if (!videoRecorder.current || videoRecorder.current.state === "inactive") { - record(); - // trackScreen && - } - return true; // cancels propagation to documentView to avoid selecting it. - }, false, false); + videoRecorder.current.onstart = (event: any) => { + setRecording(true); + // start the recording api when the video recorder starts + trackScreen && RecordingApi.Instance.start(); } - const undoPrevious = (e: React.PointerEvent) => { - e.stopPropagation(); - setDoUndo(prev => !prev); + videoRecorder.current.onstop = () => { + // RecordingApi.Instance.stop(); + // if we have a last portion + if (videoChunks.length > 1) { + // append the current portion to the video pieces + const nextVideo = { + videoChunks, + endTime: recordingTimerRef.current, + startTime: videos?.lastElement()?.endTime || 0 + }; + // depending on if a presenation exists, add it to the video + const { done: presError, value: presentation } = RecordingApi.Instance.yieldPresentation().next(); + setVideos(videos => [...videos, (!presError && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); + } + + // reset the temporary chunks + videoChunks = [] + setRecording(false); } + videoRecorder.current.start(200) + } + + + // if this is called, then we're done recording all the segments + const finish = (e: React.PointerEvent) => { + e.stopPropagation(); + + // call stop on the video recorder if active + videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop(); + + // end the streams (audio/video) to remove recording icon + const stream = videoElementRef.current!.srcObject; + stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); + + // clear the recoringApi + RecordingApi.Instance.clear(); + + // this will call upon progessbar to update videos to be in the correct order + setFinished(true); + } + + const pause = (e: React.PointerEvent) => { + e.stopPropagation() + // if recording, then this is just a new segment + videoRecorder.current?.state === "recording" && videoRecorder.current.stop(); + } + + const start = (e: React.PointerEvent) => { + // the code to start or resume does not get triggered if we start dragging the button + setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { + if (!videoRecorder.current || videoRecorder.current.state === "inactive") { + record(); + // trackScreen && + } + return true; // cancels propagation to documentView to avoid selecting it. + }, false, false); + } + + const undoPrevious = (e: React.PointerEvent) => { + e.stopPropagation(); + setDoUndo(prev => !prev); + } + const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); }; - const millisecondToMinuteSecond = (milliseconds: number) => { - const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? "0" + digit : digit - } - const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); - return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); + const millisecondToMinuteSecond = (milliseconds: number) => { + const toTwoDigit = (digit: number) => { + return String(digit).length == 1 ? "0" + digit : digit } - - return ( - <div className="recording-container"> - <div className="video-wrapper"> - <video id={`video-${props.id}`} - autoPlay - muted - onTimeUpdate={() => handleOnTimeUpdate()} - ref={videoElementRef} - /> - <div className="recording-sign"> - <span className="dot" /> - <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p> - </div> - <div className="controls"> - - <div className="controls-inner-container"> - <div className="record-button-wrapper"> - {recording ? - <button className="stop-button" onPointerDown={pause} /> : - <button className="record-button" onPointerDown={start} /> - } - </div> - - {!recording && (videos.length > 0 ? - - <div className="options-wrapper video-edit-wrapper"> - <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: {display: canUndo ? 'inherit' : 'none'} }}> - <MdBackspace onPointerDown={undoPrevious} /> - </IconContext.Provider> - <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}> - <FaCheckCircle onPointerDown={finish} /> - </IconContext.Provider> - </div> - - : <div className="options-wrapper track-screen-wrapper"> - <label className="track-screen"> - <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} /> - <span className="checkmark"></span> - Track Screen - </label> - </div>)} - - </div> - - </div> - - <ProgressBar - videos={videos} - setVideos={setVideos} - orderVideos={orderVideos} - progress={progress} - recording={recording} - doUndo={doUndo} - setCanUndo={setCanUndo} - /> + const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); + return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); + } + + return ( + <div className="recording-container"> + <div className="video-wrapper"> + <video id={`video-${props.id}`} + autoPlay + muted + onTimeUpdate={() => handleOnTimeUpdate()} + ref={videoElementRef} + /> + <div className="recording-sign"> + <span className="dot" /> + <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p> + </div> + <div className="controls"> + + <div className="controls-inner-container"> + <div className="record-button-wrapper"> + {recording ? + <button className="stop-button" onPointerDown={pause} /> : + <button className="record-button" onPointerDown={start} /> + } </div> - </div>) + + {!recording && (videos.length > 0 ? + + <div className="options-wrapper video-edit-wrapper"> + <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}> + <MdBackspace onPointerDown={undoPrevious} /> + </IconContext.Provider> + <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}> + <FaCheckCircle onPointerDown={finish} /> + </IconContext.Provider> + </div> + + : <div className="options-wrapper track-screen-wrapper"> + <label className="track-screen"> + <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} /> + <span className="checkmark"></span> + Track Screen + </label> + </div>)} + + </div> + + </div> + + <ProgressBar + videos={videos} + setVideos={setVideos} + orderVideos={orderVideos} + progress={progress} + recording={recording} + doUndo={doUndo} + setCanUndo={setCanUndo} + /> + </div> + </div>) }
\ No newline at end of file |