diff options
author | Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu> | 2022-06-13 15:10:22 -0400 |
---|---|---|
committer | Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu> | 2022-06-13 15:10:22 -0400 |
commit | d3bf3086d95ff6f895614d0942b84752662c1373 (patch) | |
tree | a820098bc7c690dd77faf0540f23b5beef0d0824 | |
parent | c7e4bf8c4e19669666cce610020969cf0d865e49 (diff) |
formatting and removing logs
-rw-r--r-- | src/client/util/RecordingApi.ts | 506 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.tsx | 443 |
2 files changed, 472 insertions, 477 deletions
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts index d56093cee..12ca07d67 100644 --- a/src/client/util/RecordingApi.ts +++ b/src/client/util/RecordingApi.ts @@ -3,289 +3,285 @@ import { IReactionDisposer, observable, reaction } from "mobx"; import { NumCast } from "../../fields/Types"; import { Doc } from "../../fields/Doc"; import { VideoBox } from "../views/nodes/VideoBox"; +import { isArray } from "lodash"; type Movement = { - time: number, - panX: number, - panY: number, - scale: number, + time: number, + panX: number, + panY: number, + scale: number, } export type Presentation = { - movements: Movement[] | null, - totalTime: number, - meta: any, + movements: Movement[] | null, + totalTime: number, + meta: Object | Object[], } export class RecordingApi { - private static get NULL_PRESENTATION(): Presentation { - return { movements: null, meta: {}, totalTime: -1, } - } - - // instance variables - private currentPresentation: Presentation; - private tracking: boolean; - private absoluteStart: number; - - - // create static instance and getter for global use - @observable static _instance: RecordingApi; - public static get Instance(): RecordingApi { return RecordingApi._instance } - public constructor() { - // init the global instance - RecordingApi._instance = this; - - // init the instance variables - this.currentPresentation = RecordingApi.NULL_PRESENTATION - this.tracking = false; - this.absoluteStart = -1; - - // used for tracking movements in the view frame - this.disposeFunc = null; - this.recordingFFView = null; - - // for now, set playFFView - this.playFFView = null; - this.timers = null; - - // put a pointerdown event on the doucment to see what the target - } - - // little helper :) - private get nullPresentation(): boolean { - return this.currentPresentation.movements === null - } - - public start = (meta?: Object) => { - // update the presentation mode - Doc.UserDoc().presentationMode = 'recording'; - - // (1a) get start date for presenation - const startDate = new Date(); - // (1b) set start timestamp to absolute timestamp - this.absoluteStart = startDate.getTime(); - - // (2) assign meta content if it exists - this.currentPresentation.meta = meta || {} - // (3) assign start date to currentPresenation - this.currentPresentation.movements = [] - // (4) set tracking true to allow trackMovements - this.tracking = true - } - - /* stops the video and returns the presentatation; if no presentation, returns undefined */ - public yieldPresentation(clearData: boolean = true): Presentation | null { - // if no presentation or done tracking, return null - if (this.nullPresentation || !this.tracking) return null; - - // set the previus recording view to the play view - this.playFFView = this.recordingFFView; - - // ensure we add the endTime now that they are done recording - const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart }; - - // reset the current presentation - clearData && this.clear(); - - return cpy; - } - - public finish = (): void => { - // make is tracking false - this.tracking = false - // reset the RecordingApi instance - this.clear(); - } - - public clear = (): void => { - // 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'; + private static get NULL_PRESENTATION(): Presentation { + return { movements: null, meta: {}, totalTime: -1, } } - // clear presenation data - this.currentPresentation = RecordingApi.NULL_PRESENTATION - // clear isRecording - // this.tracking = false - // clear absoluteStart - this.absoluteStart = -1 - } - - // call on dispose function to stop tracking movements - public removeRecordingFFView = (): void => { - this.disposeFunc?.(); - this.disposeFunc = null; - } - - private trackMovements = (panX: number, panY: number, scale: number = 0) => { - // ensure we are recording - if (!this.tracking) { - console.error('[recordingApi.ts] trackMovements(): tracking is false') - return; + + // instance variables + private currentPresentation: Presentation; + private tracking: boolean; + private absoluteStart: number; + + + // create static instance and getter for global use + @observable static _instance: RecordingApi; + public static get Instance(): RecordingApi { return RecordingApi._instance } + public constructor() { + // init the global instance + RecordingApi._instance = this; + + // init the instance variables + this.currentPresentation = RecordingApi.NULL_PRESENTATION + this.tracking = false; + this.absoluteStart = -1; + + // used for tracking movements in the view frame + this.disposeFunc = null; + this.recordingFFView = null; + + // for now, set playFFView + this.playFFView = null; + this.timers = null; + } + + // little helper :) + private get nullPresentation(): boolean { + return this.currentPresentation.movements === null + } + + public start = (meta?: Object) => { + // update the presentation mode + Doc.UserDoc().presentationMode = 'recording'; + + // (1a) get start date for presenation + const startDate = new Date(); + // (1b) set start timestamp to absolute timestamp + this.absoluteStart = startDate.getTime(); + + // (2) assign meta content if it exists + this.currentPresentation.meta = meta || {} + // (3) assign start date to currentPresenation + this.currentPresentation.movements = [] + // (4) set tracking true to allow trackMovements + this.tracking = true + } + + /* stops the video and returns the presentatation; if no presentation, returns undefined */ + public yieldPresentation(clearData: boolean = true): Presentation | null { + // if no presentation or done tracking, return null + if (this.nullPresentation || !this.tracking) return null; + + // set the previus recording view to the play view + this.playFFView = this.recordingFFView; + + // ensure we add the endTime now that they are done recording + const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart }; + + // reset the current presentation + clearData && this.clear(); + + return cpy; + } + + public finish = (): void => { + // make is tracking false + this.tracking = false + // reset the RecordingApi instance + this.clear(); } - // check to see if the presetation is init - if (this.nullPresentation) { - console.error('[recordingApi.ts] trackMovements(): no presentation') - return; + + public clear = (): void => { + // 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 + // this.tracking = false + // clear absoluteStart + this.absoluteStart = -1 } - // TO FIX: bob - // console.debug('track movment') - - // get the time - const time = new Date().getTime() - this.absoluteStart - // make new movement object - const movement: Movement = { time, panX, panY, scale } - - // add that movement to the current presentation data's movement array - this.currentPresentation.movements && this.currentPresentation.movements.push(movement) - } - - // instance variable for the FFView - private disposeFunc: IReactionDisposer | null; - private recordingFFView: CollectionFreeFormView | null; - - // set the FFView that will be used in a reaction to track the movements - public setRecordingFFView = (view: CollectionFreeFormView): void => { - // set the view to the current view - if (view === this.recordingFFView || view == null) return; - - // this.recordingFFView = view; - // set the reaction to track the movements - this.disposeFunc = reaction( - () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }), - (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale) - ) - - // for now, set the most recent recordingFFView to the playFFView - this.recordingFFView = view; - } - - // TODO: extract this into different class with pause and resume recording - // TODO: store the FFview with the movements - private playFFView: CollectionFreeFormView | null; - private timers: NodeJS.Timeout[] | null; - - public setPlayFFView = (view: CollectionFreeFormView): void => { - this.playFFView = view - } - - // pausing movements will dispose all timers that are planned to replay the movements - // play movemvents will recreate them when the user resumes the presentation - public pauseMovements = (): undefined | Error => { - if (this.playFFView === null) { - return new Error('[recordingApi.ts] pauseMovements() failed: no view') + // call on dispose function to stop tracking movements + public removeRecordingFFView = (): void => { + this.disposeFunc?.(); + this.disposeFunc = null; } - if (!this._isPlaying) { - //return new Error('[recordingApi.ts] pauseMovements() failed: not playing') - return + private trackMovements = (panX: number, panY: number, scale: number = 0) => { + // ensure we are recording + if (!this.tracking) { + console.error('[recordingApi.ts] trackMovements(): tracking is false') + return; + } + // check to see if the presetation is init + if (this.nullPresentation) { + console.error('[recordingApi.ts] trackMovements(): no presentation') + return; + } + + // get the time + const time = new Date().getTime() - this.absoluteStart + // make new movement object + const movement: Movement = { time, panX, panY, scale } + + // add that movement to the current presentation data's movement array + this.currentPresentation.movements && this.currentPresentation.movements.push(movement) } - this._isPlaying = false - // TODO: set userdoc presentMode to browsing - this.timers?.map(timer => clearTimeout(timer)) - // this.videoBox = null; - } + // instance variable for the FFView + private disposeFunc: IReactionDisposer | null; + private recordingFFView: CollectionFreeFormView | null; + + // set the FFView that will be used in a reaction to track the movements + public setRecordingFFView = (view: CollectionFreeFormView): void => { + // set the view to the current view + if (view === this.recordingFFView || view == null) return; - private videoBox: VideoBox | null = null; + // this.recordingFFView = view; + // set the reaction to track the movements + this.disposeFunc = reaction( + () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }), + (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale) + ) - // by calling pause on the VideoBox, the pauseMovements will be called - public pauseVideoAndMovements = (): boolean => { - this.videoBox?.Pause() + // for now, set the most recent recordingFFView to the playFFView + this.recordingFFView = view; + } - this.pauseMovements() - return this.videoBox == null - } + // TODO: extract this into different class with pause and resume recording + // TODO: store the FFview with the movements + private playFFView: CollectionFreeFormView | null; + private timers: NodeJS.Timeout[] | null; - public _isPlaying = false; + public setPlayFFView = (view: CollectionFreeFormView): void => { + this.playFFView = view + } - public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => { - if (presentation.movements === null || this.playFFView === null) { - return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view') + // pausing movements will dispose all timers that are planned to replay the movements + // play movemvents will recreate them when the user resumes the presentation + public pauseMovements = (): undefined | Error => { + if (this.playFFView === null) { + return new Error('[recordingApi.ts] pauseMovements() failed: no view') + } + + if (!this._isPlaying) { + //return new Error('[recordingApi.ts] pauseMovements() failed: not playing') + return + } + this._isPlaying = false + // TODO: set userdoc presentMode to browsing + this.timers?.map(timer => clearTimeout(timer)) + + // this.videoBox = null; } - if (this._isPlaying) return; - this._isPlaying = true; - Doc.UserDoc().presentationMode = 'watching'; + private videoBox: VideoBox | null = null; - // TODO: consider this bug at the end of the clip on seek - this.videoBox = videoBox || null; + // by calling pause on the VideoBox, the pauseMovements will be called + public pauseVideoAndMovements = (): boolean => { + this.videoBox?.Pause() - // only get the movements that are remaining in the video time left - const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000) + this.pauseMovements() + return this.videoBox == null + } - // helper to replay a movement - const document = this.playFFView - let preScale = -1; - const zoomAndPan = (movement: Movement) => { - const { panX, panY, scale } = movement; - (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); - document.Document._panX = panX; - document.Document._panY = panY; + public _isPlaying = false; + + public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => { + if (presentation.movements === null || this.playFFView === null) { + return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view') + } + if (this._isPlaying) return; + + this._isPlaying = true; + Doc.UserDoc().presentationMode = 'watching'; + + // TODO: consider this bug at the end of the clip on seek + this.videoBox = videoBox || null; + + // only get the movements that are remaining in the video time left + const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000) + + // helper to replay a movement + const document = this.playFFView + let preScale = -1; + const zoomAndPan = (movement: Movement) => { + const { panX, panY, scale } = movement; + (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); + document.Document._panX = panX; + document.Document._panY = panY; + + preScale = scale; + } + + // set the first frame to be at the start of the pres + zoomAndPan(filteredMovements[0]); + + // make timers that will execute each movement at the correct replay time + this.timers = filteredMovements.map(movement => { + const timeDiff = movement.time - timeViewed * 1000 + return setTimeout(() => { + // replay the movement + zoomAndPan(movement) + // if last movement, presentation is done -> set the instance var + if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; + }, timeDiff) + }) + } - preScale = scale; + // method that concatenates an array of presentatations into one + public concatPresentations = (presentations: Presentation[]): Presentation => { + // 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 { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; } - // set the first frame to be at the start of the pres - zoomAndPan(filteredMovements[0]); - - // make timers that will execute each movement at the correct replay time - this.timers = filteredMovements.map(movement => { - const timeDiff = movement.time - timeViewed * 1000 - return setTimeout(() => { - // replay the movement - zoomAndPan(movement) - // if last movement, presentation is done -> set the instance var - if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; - }, timeDiff) - }) - } - - // method that concatenates an array of presentatations into one - public concatPresentations = (presentations: Presentation[]): Presentation => { - // 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 { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; - } - - // Unfinished code for tracing multiple free form views - // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map() - - // export function AddRecordingFFView(ffView: CollectionFreeFormView): void { - // pres.set(ffView, - // reaction(() => ({ x: ffView.panX, y: ffView.panY }), - // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y))) - // ) - // } - - // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void { - // const disposer = pres.get(ffView); - // disposer?.(); - // pres.delete(ffView) - // } + // Unfinished code for tracing multiple free form views + // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map() + + // export function AddRecordingFFView(ffView: CollectionFreeFormView): void { + // pres.set(ffView, + // reaction(() => ({ x: ffView.panX, y: ffView.panY }), + // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y))) + // ) + // } + + // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void { + // const disposer = pres.get(ffView); + // disposer?.(); + // pres.delete(ffView) + // } } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 15883632a..ec9838bdd 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -11,262 +11,261 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Presentation, RecordingApi } from '../../../util/RecordingApi'; export interface MediaSegment { - videoChunks: any[], - endTime: number, - startTime: number, - presentation?: Presentation, + 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 [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(() => { + 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]); + + // this will call upon the progress bar to edit videos to be in the correct order + useEffect(() => { + finished && setOrderVideos(true); + }, [finished]); + + // check if the browser supports media devices on first load + useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []); + + 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]); + + useEffect(() => { + setVideoProgressHelper(recordingTimer) + recordingTimerRef.current = recordingTimer; + }, [recordingTimer]); + + const setVideoProgressHelper = (progress: number) => { + const newProgress = (progress / MAXTIME) * 100; + setProgress(newProgress); + } + const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { + const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints); + videoElementRef.current!.src = ""; + videoElementRef.current!.srcObject = stream; + videoElementRef.current!.muted = true; - const DEFAULT_MEDIA_CONSTRAINTS = { - video: { - width: 1280, - height: 720, - }, - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 44100 + return stream; } - } - // useEffect(() => console.debug('progress', progress), [progress]) + 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()); - useEffect(() => { - 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)); + // temporary chunks of video + let videoChunks: any = []; - // 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() })); + videoRecorder.current.ondataavailable = (event: any) => { + if (event.data.size > 0) videoChunks.push(event.data); + }; - // 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) + videoRecorder.current.onstart = (event: any) => { + setRecording(true); + // start the recording api when the video recorder starts + trackScreen && RecordingApi.Instance.start(); + }; - // 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"); - })(); + videoRecorder.current.onstop = () => { + // 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 presentation = RecordingApi.Instance.yieldPresentation(); + setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); + } + + // reset the temporary chunks + videoChunks = []; + setRecording(false); + } + + videoRecorder.current.start(200); } - }, [videos]) - - // this will call upon the progress bar to edit videos to be in the correct order - useEffect(() => { - finished && setOrderVideos(true); - }, [finished]) - - // check if the browser supports media devices on first load - useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []) - - 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]) - - useEffect(() => { - setVideoProgressHelper(recordingTimer) - recordingTimerRef.current = recordingTimer; - }, [recordingTimer]) - - const setVideoProgressHelper = (progress: number) => { - const newProgress = (progress / MAXTIME) * 100; - setProgress(newProgress) - } - const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { - const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) - videoElementRef.current!.src = "" - videoElementRef.current!.srcObject = stream - videoElementRef.current!.muted = true + // if this is called, then we're done recording all the segments + const finish = (e: React.PointerEvent) => { + e.stopPropagation(); - return stream - } + // call stop on the video recorder if active + videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop(); - 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()) + // end the streams (audio/video) to remove recording icon + const stream = videoElementRef.current!.srcObject; + stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); - // temporary chunks of video - let videoChunks: any = [] + // finish/clear the recoringApi + RecordingApi.Instance.finish(); - videoRecorder.current.ondataavailable = (event: any) => { - if (event.data.size > 0) videoChunks.push(event.data) + // this will call upon progessbar to update videos to be in the correct order + setFinished(true); } - videoRecorder.current.onstart = (event: any) => { - setRecording(true); - // start the recording api when the video recorder starts - trackScreen && RecordingApi.Instance.start(); + 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.onstop = () => { - // 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 presentation = RecordingApi.Instance.yieldPresentation(); - setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); - } + const start = (e: React.PointerEvent) => { + setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { + // start recording if not already recording + if (!videoRecorder.current || videoRecorder.current.state === "inactive") record(); - // reset the temporary chunks - videoChunks = [] - setRecording(false); + return true; // cancels propagation to documentView to avoid selecting it. + }, false, 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()); - - // finish/clear the recoringApi - RecordingApi.Instance.finish(); - - // 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) => { - setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { - // start recording if not already recording - if (!videoRecorder.current || videoRecorder.current.state === "inactive") record(); - - return true; // cancels propagation to documentView to avoid selecting it. - }, false, false); - } - - const undoPrevious = (e: React.PointerEvent) => { - e.stopPropagation(); - setDoUndo(prev => !prev); - } + const undoPrevious = (e: React.PointerEvent) => { + e.stopPropagation(); + setDoUndo(prev => !prev); + } - const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); }; + const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); }; - const millisecondToMinuteSecond = (milliseconds: number) => { - const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? "0" + digit : digit + 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 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> - {!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>) + 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} + /> + </div> + </div>) }
\ No newline at end of file |