diff options
Diffstat (limited to 'src/client/util/RecordingApi.ts')
-rw-r--r-- | src/client/util/RecordingApi.ts | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts new file mode 100644 index 000000000..7bffb0379 --- /dev/null +++ b/src/client/util/RecordingApi.ts @@ -0,0 +1,269 @@ +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'; +import { scaleDiverging } from 'd3-scale'; +import { Transform } from './Transform'; + +type Movement = { + time: number; + panX: number; + panY: number; + scale: 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'; + // set the previus recording view to the play view + this.playFFView = this.recordingFFView; + + 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 difference in time + this.absoluteStart = new Date().getTime() - this.absoluteStart; + }; + + private trackMovements = (panX: number, panY: number, scale: number = 0): 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 + 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.isRecording && this.trackMovements(res.x, res.y, res.scale) + ); + + // 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; + }; + + // 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; + }; + + private videoBox: VideoBox | null = null; + + // by calling pause on the VideoBox, the pauseMovements will be called + public pauseVideoAndMovements = (): boolean => { + this.videoBox?.Pause(); + + this.pauseMovements(); + return this.videoBox == null; + }; + + 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); + }); + }; + + // 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) + // } +} |