aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/util/RecordingApi.ts70
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx442
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