aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/RecordingApi.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util/RecordingApi.ts')
-rw-r--r--src/client/util/RecordingApi.ts269
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)
+ // }
+}