aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/RecordingApi.ts
diff options
context:
space:
mode:
authorMichael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>2022-05-04 01:11:54 -0400
committerMichael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>2022-05-04 01:11:54 -0400
commit1eb2c362b020b3cbe446bbc1585108129fda6977 (patch)
tree92423c99646361c1d450307445ce51ee164f013a /src/client/util/RecordingApi.ts
parentdd5e4a4d5ed3c46c374df4d29a11b0c5e47126b3 (diff)
Get storing pres data working, but is choppy due to mobx usage.
Diffstat (limited to 'src/client/util/RecordingApi.ts')
-rw-r--r--src/client/util/RecordingApi.ts303
1 files changed, 303 insertions, 0 deletions
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
new file mode 100644
index 000000000..c4f76282c
--- /dev/null
+++ b/src/client/util/RecordingApi.ts
@@ -0,0 +1,303 @@
+import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
+import { IReactionDisposer, observable, reaction } from "mobx";
+import { NumCast } from "../../fields/Types";
+
+type Movement = {
+ time: number,
+ panX: number,
+ panY: number,
+}
+
+type Presentation = {
+ movements: Array<Movement>
+ meta: Object,
+ startDate: Date | null,
+}
+
+export class RecordingApi {
+
+ private static NULL_PRESENTATION: Presentation = {
+ movements: [],
+ meta: {},
+ startDate: null,
+ }
+
+ // 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;
+
+ // for now, set playFFView
+ this.playFFView = null;
+ this.timers = null;
+ }
+
+ // little helper :)
+ private get isInitPresenation(): boolean {
+ return this.currentPresentation.startDate === 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()')
+ }
+
+ // (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.startDate = startDate
+ // (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()')
+ }
+
+ 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.disposeFunc = null
+
+ return presCopy;
+ }
+
+ public pause = (): Error | undefined => {
+ if (this.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
+ this.isRecording = false
+
+ // set relativeStart to the pausedTimestamp
+ const timestamp = new Date().getTime()
+ this.absoluteStart = timestamp
+ }
+
+ public resume = () => {
+ const timestamp = new Date().getTime()
+ const startTimestamp = this.currentPresentation.startDate?.getTime()
+ if (!startTimestamp) {
+ console.error('[recordingApi.ts] resume() failed: no presentation data. try calling start() first')
+ return new Error('[recordingApi.ts] pause()')
+ }
+
+ // update absoluteStart to bridge the paused time
+ const absoluteTimePaused = timestamp - this.absoluteStart
+ this.absoluteStart = absoluteTimePaused
+ }
+
+ // public finish = (): Error | Presentation => {
+ // if (this.isInitPresenation) {
+ // console.error('[recordingApi.ts] finish() failed: no presentation data. try calling start() first')
+ // return new Error('[recordingApi.ts] finish()')
+ // }
+
+ // // return a copy of the the presentation data
+ // return { ...this.currentPresentation }
+ // }
+
+ private trackMovements = (panX: number, panY: number): Error | undefined => {
+ // ensure we are recording
+ if (!this.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 - this.absoluteStart
+
+ // make new movement struct
+ const movement: Movement = { time: relativeTime, panX, panY }
+
+ // add that movement to the current presentation data's movement array
+ this.currentPresentation.movements.push(movement)
+ }
+
+ // instance variable for the FFView
+ private disposeFunc: IReactionDisposer | 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 === null || view === this.playFFView) 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.playFFView = 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
+ private playFFView: CollectionFreeFormView | null;
+ private timers: Timer[] | null;
+
+ // public followMovements = (presentation: Presentation): undefined | Error => {
+ // console.log(presentation)
+ // if (presentation.startDate === null || this.playFFView === null) {
+ // return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+ // }
+
+ // const document = this.playFFView.Document
+ // const { movements } = presentation
+ // this.timers = movements.map(movement => {
+ // const { panX, panY, time } = movement
+ // return new Timer(() => {
+ // document._panX = panX;
+ // document._panY = panY;
+ // // TODO: consider cleaning this array to null or some state
+ // }, time)
+ // })
+ // }
+
+ public pauseMovements = (): undefined | Error => {
+ if (this.playFFView === null) {
+ return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+ }
+ // TODO: set userdoc presentMode to browsing
+ this.timers?.forEach(timer => timer.pause())
+ }
+
+ public resumeMovements = (): undefined | Error => {
+ if (this.playFFView === null) {
+ return new Error('[recordingApi.ts] resumeMovements() failed: no view')
+ }
+ this.timers?.forEach(timer => timer.resume())
+ }
+
+ public followMovements = (presentation: Presentation): undefined | Error => {
+ if (presentation.startDate === null || this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+ }
+
+ const document = this.playFFView.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)
+ })
+ }
+ // 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)
+ // }
+}
+
+/** Represents the `setTimeout` with an ability to perform pause/resume actions
+ * citation: https://stackoverflow.com/questions/3969475/javascript-pause-settimeout
+ */
+export class Timer {
+ private _start: Date;
+ private _remaining: number;
+ private _durationTimeoutId?: NodeJS.Timeout;
+ private _callback: (...args: any[]) => void;
+ private _done = false;
+ get done () {
+ return this._done;
+ }
+
+ public constructor(callback: (...args: any[]) => void, ms = 0) {
+ this._callback = () => {
+ callback();
+ this._done = true;
+ };
+ this._remaining = ms;
+ this.resume();
+ }
+
+ /** pauses the timer */
+ public pause(): Timer {
+ if (this._durationTimeoutId && !this._done) {
+ this._clearTimeoutRef();
+ this._remaining -= new Date().getTime() - this._start.getTime();
+ }
+ return this;
+ }
+
+ /** resumes the timer */
+ public resume(): Timer {
+ if (!this._durationTimeoutId && !this._done) {
+ this._start = new Date;
+ this._durationTimeoutId = setTimeout(this._callback, this._remaining);
+ }
+ return this;
+ }
+
+ /**
+ * clears the timeout and marks it as done.
+ *
+ * After called, the timeout will not resume
+ */
+ public clearTimeout() {
+ this._clearTimeoutRef();
+ this._done = true;
+ }
+
+ private _clearTimeoutRef() {
+ if (this._durationTimeoutId) {
+ clearTimeout(this._durationTimeoutId);
+ this._durationTimeoutId = undefined;
+ }
+ }
+
+} \ No newline at end of file