diff options
Diffstat (limited to 'src/client/util/RecordingApi.ts')
-rw-r--r-- | src/client/util/RecordingApi.ts | 257 |
1 files changed, 257 insertions, 0 deletions
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) + // } +} |