diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/recording/recordingApi.tsx | 151 | ||||
-rw-r--r-- | src/client/util/RecordingApi.ts | 257 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 37 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingBox.tsx | 14 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.scss | 18 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.tsx | 41 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 23 |
8 files changed, 336 insertions, 207 deletions
diff --git a/src/client/apis/recording/recordingApi.tsx b/src/client/apis/recording/recordingApi.tsx deleted file mode 100644 index 55714f03b..000000000 --- a/src/client/apis/recording/recordingApi.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm"; -import React, { useState } from "react"; - -export function RecordingApi() { - - type Movement = { - time: number, - panX: number, - panY: number, - } - - type Presentation = { - movements: Array<Movement> - meta: Object, - startDate: Date | null, - } - - const NULL_PRESENTATION = { - movements: [], - meta: {}, - startDate: null, - } - - const [currentPresentation, setCurrentPresenation] = useState<Presentation>(NULL_PRESENTATION) - const [isRecording, setIsRecording] = useState(false) - const [absoluteStart, setAbsoluteStart] = useState<number>(-1) - - const initAndStart = (view: CollectionFreeFormView, meta?: Object): Error | undefined => { - // check if already init a presentation - if (currentPresentation.startDate !== null) { - console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.') - return new Error('[recordingApi.ts] start()') - } - - // (1a) get start date for presenation - const startDate = new Date() - // (1b) set start timestamp to absolute timestamp - setAbsoluteStart(startDate.getTime()) - - // TODO: (2) assign meta content - - // (3) assign init values to currentPresenation - setCurrentPresenation({ ...currentPresentation, startDate }) - - // (4) set isRecording true to allow trackMovements - setIsRecording(true) - } - - const clear = (): Error | undefined => { - // TODO: maybe archive the data? - if (isRecording) { - console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() or finish() first') - return new Error('[recordingApi.ts] clear()') - } - // clear presenation data - setCurrentPresenation(NULL_PRESENTATION) - - // clear isRecording - setIsRecording(false) - - // clear absoluteStart - setAbsoluteStart(-1) - } - - const pause = (): Error | undefined => { - if (currentPresentation.startDate === null) { - console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first') - return new Error('[recordingApi.ts] pause(): no presenation') - } - // don't allow track movments - setIsRecording(false) - - // set relativeStart to the pausedTimestamp - const timestamp = new Date().getTime() - setAbsoluteStart(timestamp) - } - - const resume = () => { - if (currentPresentation.startDate === null) { - console.error('[recordingApi.ts] resume() failed: no presentation started. try calling init() first') - return new Error('[recordingApi.ts] resume()') - } - - const timestamp = new Date().getTime() - const startTimestamp = currentPresentation.startDate?.getTime() - if (!startTimestamp) { - console.error('[recordingApi.ts] resume() failed: no presentation data. try calling init() first') - return new Error('[recordingApi.ts] pause()') - } - - setAbsoluteStart(prevTime => { - // const relativeUnpause = timestamp - absoluteStart - // const timePaused = relativeUnpause - prevTime - // return timePaused + absoluteStart - const absoluteTimePaused = timestamp - prevTime - return absoluteTimePaused - }) - } - - const finish = (): Error | Presentation => { - if (currentPresentation.movements === null) { - console.error('[recordingApi.ts] finish() failed: no presentation data. try calling init() first') - return new Error('[recordingApi.ts] finish()') - } - - // make copy and clear this class's data - const returnCopy = { ...currentPresentation } - clear() - - // return the copy - return returnCopy - } - - const trackMovements = (panX: number, panY: number): Error | undefined => { - // ensure we are recording - if (!isRecording) { - console.error('[recordingApi.ts] pause() failed: recording is paused()') - return new Error('[recordingApi.ts] pause()') - } - - // get the relative time - const timestamp = new Date().getTime() - const relativeTime = timestamp - absoluteStart - - // make new movement struct - const movement: Movement = { time: relativeTime, panX, panY } - - // add that movement struct to the current presentation data - setCurrentPresenation(prevPres => { - const movements = [...prevPres.movements, movement] - return {...prevPres, movements} - }) - } - - // TOOD: need to pause all intervals if possible lol - // TODO: extract this into different class with pause and resume recording - const followMovements = (presentation: Presentation, docView: CollectionFreeFormView): void => { - const document = docView.Document - - const { movements } = presentation - movements.forEach(movement => { - const { panX, panY, time } = movement - // set the pan to what was stored - setTimeout(() => { - document._panX = panX; - document._panY = panY; - }, time) - }) - } - -}
\ No newline at end of file diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts new file mode 100644 index 000000000..fec579486 --- /dev/null +++ b/src/client/util/RecordingApi.ts @@ -0,0 +1,257 @@ +import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; +import { IReactionDisposer, observable, reaction } from "mobx"; +import { NumCast } from "../../fields/Types"; +import { Doc } from "../../fields/Doc"; +import { VideoBox } from "../views/nodes/VideoBox"; + +type Movement = { + time: number, + panX: number, + panY: number, +} + +type Presentation = { + movements: Array<Movement> | null + meta: Object, +} + +export class RecordingApi { + + private static NULL_PRESENTATION: Presentation = { + movements: null, + meta: {}, + } + + // instance variables + private currentPresentation: Presentation; + private isRecording: 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.isRecording = 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 isInitPresenation(): boolean { + return this.currentPresentation.movements === null + } + + public start = (meta?: Object): Error | undefined => { + // check if already init a presentation + if (!this.isInitPresenation) { + console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.') + return new Error('[recordingApi.ts] start()') + } + + // 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 isRecording true to allow trackMovements + this.isRecording = true + } + + public clear = (): Error | Presentation => { + // TODO: maybe archive the data? + if (this.isRecording) { + console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first') + return new Error('[recordingApi.ts] clear()') + } + + // update the presentation mode + Doc.UserDoc().presentationMode = 'none' + + const presCopy = { ...this.currentPresentation } + + // clear presenation data + this.currentPresentation = RecordingApi.NULL_PRESENTATION + // clear isRecording + this.isRecording = false + // clear absoluteStart + this.absoluteStart = -1 + // clear the disposeFunc + this.removeRecordingFFView() + + return presCopy; + } + + public pause = (): Error | undefined => { + if (this.isInitPresenation) { + console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first') + return new Error('[recordingApi.ts] pause(): no presentation') + } + // don't allow track movments + this.isRecording = false + + // set adjust absoluteStart to add the time difference + const timestamp = new Date().getTime() + this.absoluteStart = timestamp - this.absoluteStart + } + + public resume = () => { + this.isRecording = true + // set absoluteStart to the pausedTimestamp + this.absoluteStart = new Date().getTime() - this.absoluteStart + } + + private trackMovements = (panX: number, panY: number): Error | undefined => { + // ensure we are recording + if (!this.isRecording) { + return new Error('[recordingApi.ts] trackMovements()') + } + // check to see if the presetation is init + if (this.isInitPresenation) { + return new Error('[recordingApi.ts] trackMovements(): no presentation') + } + + // get the time + const time = new Date().getTime() - this.absoluteStart + // make new movement object + console.log(time) + const movement: Movement = { time, panX, panY } + + // 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) }), + (res) => (res.x !== -1 && res.y !== -1) && this.trackMovements(res.x, res.y) + ) + + // for now, set the most recent recordingFFView to the playFFView + this.recordingFFView = view; + } + + // call on dispose function to stop tracking movements + public removeRecordingFFView = (): void => { + this.disposeFunc?.(); + this.disposeFunc = 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 setPlayFFView = (view: CollectionFreeFormView): void => { + this.playFFView = view + } + + 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; + } + + private videoBox: VideoBox | null = null; + + // by calling pause on the VideoBox, the pauseMovements will be called + public pauseVideoAndMovements = (): boolean => { + this.videoBox?.Pause() + return this.videoBox === null + } + + private _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 new Error('[recordingApi.ts] playMovements() failed: already playing') + return + } + this._isPlaying = true; + Doc.UserDoc().presentationMode = 'watching'; + + // TODO: consider this bug at the end of the clip on seek + // console.log(timeViewed) + this.videoBox = videoBox || null; + + const document = this.playFFView.Document + const { movements } = presentation + this.timers = movements.reduce((arr: NodeJS.Timeout[], movement) => { + const { panX, panY, time } = movement + + const timeDiff = time - timeViewed*1000 + if (timeDiff < 0) return arr; + + // set the pan to what was stored + arr.push(setTimeout(() => { + document._panX = panX; + document._panY = panY; + if (movement === movements[movements.length - 1]) { + this._isPlaying = false; + Doc.UserDoc().presentationMode = 'none'; + } + }, timeDiff)) + return arr; + }, []) + + } + + // 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/Main.tsx b/src/client/views/Main.tsx index 8560ccb29..517fe097c 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -8,6 +8,7 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Docs } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { LinkManager } from "../util/LinkManager"; +import { RecordingApi } from "../util/RecordingApi"; import { CollectionView } from "./collections/CollectionView"; import { MainView } from "./MainView"; @@ -36,5 +37,6 @@ AssignAllExtensions(); const expires = "expires=" + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; new LinkManager(); + new RecordingApi; ReactDOM.render(<MainView />, document.getElementById('root')); })();
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index edc872a40..aa2e0c417 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -54,6 +54,7 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import e = require("connect-flash"); +import { RecordingApi } from "../../../util/RecordingApi"; export const panZoomSchema = createSchema({ _panX: "number", @@ -121,8 +122,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. - @observable storedMovements: object[] = []; // stores the movement if in recoding mode - @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); } @@ -964,36 +963,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } - - followMovements = (): void => { - // need the first for subtraction - let first = null; - - this.storedMovements.forEach(movement => { - if (first === null) first = movement.time; - - // set the pan to what was stored - setTimeout(() => { - this.Document._panX = movement.panX; - this.Document._panY = movement.panY; - }, movement.time - first) - }) - - // for now, clear the movements - this.storedMovements = [] - } - @action setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { - // if not presenting, just retrace the movements + // set the current respective FFview to the tab being panned. + Doc.UserDoc()?.presentationMode === 'recording' && RecordingApi.Instance.setRecordingFFView(this); + // TODO: make this based off the specific recording FFView + Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this); if (Doc.UserDoc()?.presentationMode === 'watching') { - this.followMovements() - return; - } - - if (Doc.UserDoc()?.presentationMode === 'recording') { - // store as many movments as possible - this.storedMovements.push({time: new Date().getTime(), panX, panY}) + RecordingApi.Instance.pauseVideoAndMovements(); + Doc.UserDoc().presentationMode = 'none'; + // RecordingApi.Instance.pauseMovements() } if (!this.isAnnotationOverlay && clamp) { diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index a49cbbea5..d00f05759 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -1,7 +1,7 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { AudioField, VideoField } from "../../../../fields/URLField"; +import { VideoField } from "../../../../fields/URLField"; import { Upload } from "../../../../server/SharedMediaTypes"; import { ViewBoxBaseComponent } from "../../DocComponent"; import { FieldView } from "../FieldView"; @@ -9,6 +9,8 @@ import { VideoBox } from "../VideoBox"; import { RecordingView } from './RecordingView'; import { DocumentType } from "../../../documents/DocumentTypes"; +import { RecordingApi } from "../../../util/RecordingApi"; + @observer export class RecordingBox extends ViewBoxBaseComponent() { @@ -31,18 +33,20 @@ export class RecordingBox extends ViewBoxBaseComponent() { @action setResult = (info: Upload.FileInformation) => { - console.log("Setting result to " + info) + // console.log("Setting result to " + info) this.result = info - console.log(this.result.accessPaths.agnostic.client) + // console.log(this.result.accessPaths.agnostic.client) this.dataDoc.type = DocumentType.VID; - console.log(this.videoDuration) + // console.log(this.videoDuration) this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration; // this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; // this.layoutDoc._fitWidth = undefined; - this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); + this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); + // stringify the presenation and store it + this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear()); } render() { diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss index 0c153c9c8..c55af5952 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.scss +++ b/src/client/views/nodes/RecordingBox/RecordingView.scss @@ -1,9 +1,11 @@ video { - flex: 100%; + // flex: 100%; width: 100%; // min-height: 400px; - height: auto; - display: block; + //height: auto; + height: 100%; + //display: block; + object-fit: cover; background-color: black; } @@ -14,18 +16,20 @@ button { .recording-container { height: 100%; width: 100%; - display: flex; + // display: flex; pointer-events: all; + background-color: grey; } .video-wrapper { - max-width: 600px; - max-width: 700px; + // max-width: 600px; + // max-width: 700px; position: relative; display: flex; justify-content: center; - overflow: hidden; + // overflow: hidden; border-radius: 10px; + margin: 0; } .video-wrapper:hover .controls { diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index e5f6deca2..0a8294dcf 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -8,6 +8,8 @@ import { IconContext } from "react-icons"; import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; +import { RecordingApi } from '../../../util/RecordingApi'; + enum RecordingStatus { Recording, @@ -43,18 +45,23 @@ export function RecordingView(props: IRecordingViewProps) { const audioRecorder = useRef<MediaRecorder | null>(null); const videoElementRef = useRef<HTMLVideoElement | null>(null); - const [finished, setFinished] = useState<Boolean>(false) + const [finished, setFinished] = useState<Boolean>(false) const DEFAULT_MEDIA_CONSTRAINTS = { - video: true, - audio: true, - // audio: { - // echoCancellation: true, - // noiseSuppression: true, - // sampleRate: 44100 - // } + // video: true, + // audio: true + video: { + width: 1280, + height: 720, + }, + // audio: true, + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 44100 + } } useEffect(() => { @@ -150,7 +157,9 @@ export function RecordingView(props: IRecordingViewProps) { // } videoRecorder.current.onstart = (event: any) => { - setRecording(true); + setRecording(true); + // RecordingApi.Instance.clear(); + RecordingApi.Instance.start(); } videoRecorder.current.onstop = () => { @@ -163,7 +172,8 @@ export function RecordingView(props: IRecordingViewProps) { // reset the temporary chunks videoChunks = [] setRecording(false); - setFinished(true); + setFinished(true); + RecordingApi.Instance.pause(); } // recording paused @@ -173,12 +183,14 @@ export function RecordingView(props: IRecordingViewProps) { // reset the temporary chunks videoChunks = [] - setRecording(false); + setRecording(false); + RecordingApi.Instance.pause(); } videoRecorder.current.onresume = async (event: any) => { await startShowingStream(); - setRecording(true); + setRecording(true); + RecordingApi.Instance.resume(); } videoRecorder.current.start(200) @@ -244,7 +256,7 @@ export function RecordingView(props: IRecordingViewProps) { return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); } - return ( + return ( <div className="recording-container"> <div className="video-wrapper"> <video id="video" @@ -293,8 +305,7 @@ export function RecordingView(props: IRecordingViewProps) { <i className="bx bxs-volume-mute"></i> )} </button> */} - </div> - + </div> </div> </div>) }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b424225e7..8aecc2483 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -28,6 +28,7 @@ import { AnchorMenu } from "../pdf/AnchorMenu"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; +import { RecordingApi } from "../../util/RecordingApi"; const path = require('path'); @@ -117,6 +118,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return field?.url.href ?? vfield?.url.href ?? ""; } + // returns the presentation data if it exists, null otherwise + @computed get presentation() { + const data = this.dataDoc[this.fieldKey + '-presentation']; + return data ? JSON.parse(data) : null; + } + @computed private get timeline() { return this._stackedTimeline; } private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline @@ -147,6 +154,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // plays video @action public Play = (update: boolean = true) => { + if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) { + console.log('VideoBox : Play : presentation mode', this._playing); + return; + } + + // if presentation isn't null, call followmovements on the recording api + if (this.presentation) { + const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this); + err && console.log(err) + } + this._playing = true; const eleTime = this.player?.currentTime || 0; if (this.timeline) { @@ -184,6 +202,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // pauses video @action public Pause = (update: boolean = true) => { + if (this.presentation) { + const err = RecordingApi.Instance.pauseMovements(); + err && console.log(err); + } + this._playing = false; this.removeCurrentlyPlaying(); try { |