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 | 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 = 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) // } }