From 0af393318adafa885d66c0fc43ffbf23f91e3c73 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Thu, 21 Apr 2022 16:48:51 -0400
Subject: Integrate with jenny's videobox api

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 27 ++++------------------
 1 file changed, 5 insertions(+), 22 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 6db2269c4..239aacd4f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -51,6 +51,7 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import e = require("connect-flash");
+import { RecordingApi } from "../../../apis/recording/RecordingApi";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -959,36 +960,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     }
 
-
-    followMovements = (): void => {
-        // need the first for subtraction
-        let first = null;
-
-        this.storedMovements.forEach(movement => {
-            if (first === null) first = movement.time;
-
-            // set the pan to what was stored
-            setTimeout(() => {
-                this.Document._panX = movement.panX;
-                this.Document._panY = movement.panY;
-            }, movement.time - first)
-        })
-
-        // for now, clear the movements
-        this.storedMovements = []
-    }
-
     @action
     setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
         // if not presenting, just retrace the movements
         if (Doc.UserDoc()?.presentationMode === 'watching') {
-            this.followMovements()
-            return;
+            // RecordingApi.followMovements(presentation, this)
+            return
         }
 
         if (Doc.UserDoc()?.presentationMode === 'recording') {
             // store as many movments as possible
-            this.storedMovements.push({time: new Date().getTime(), panX, panY})
+            // this.storedMovements.push({time: new Date().getTime(), panX, panY})
+            const err = RecordingApi.trackMovements(panX, panY)
         }
 
         if (!this.isAnnotationOverlay && clamp) {
-- 
cgit v1.2.3-70-g09d2


From 561890c44833dbd4725cb8d818edb9856a464066 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Thu, 28 Apr 2022 17:10:04 -0400
Subject: Remove demo code from CollectionFFView and implment observer call
 into recordingApi

---
 src/client/apis/recording/recordingApi.ts          | 54 ++++++++++++++++++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 32 -------------
 2 files changed, 45 insertions(+), 41 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/apis/recording/recordingApi.ts b/src/client/apis/recording/recordingApi.ts
index 9ad7b5165..50d6f8038 100644
--- a/src/client/apis/recording/recordingApi.ts
+++ b/src/client/apis/recording/recordingApi.ts
@@ -1,6 +1,7 @@
 import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm";
 import React, { useState } from "react";
-import { observable, observe } from "mobx";
+import { IReactionDisposer, observable, reaction } from "mobx";
+import { NumCast } from "../../../fields/Types";
 
 type Movement = {
     time: number,
@@ -39,6 +40,9 @@ export class RecordingApi {
         this.currentPresentation = RecordingApi.NULL_PRESENTATION
         this.isRecording = false;
         this.absoluteStart = -1;
+
+        // used for tracking movements in the view frame
+        this.disposeFunc = null;
     }
 
     // little helper :)
@@ -79,6 +83,9 @@ export class RecordingApi {
         this.isRecording = false
         // clear absoluteStart
         this.absoluteStart = -1
+
+        // clear the disposeFunc
+        this.disposeFunc = null
     }
 
     public pause = (): Error | undefined => {
@@ -117,7 +124,7 @@ export class RecordingApi {
         return { ...this.currentPresentation }
     }
 
-    public trackMovements = (panX: number, panY: number): Error | undefined => {
+    private trackMovements = (panX: number, panY: number): Error | undefined => {
         // ensure we are recording
         if (!this.isRecording) {
              console.error('[recordingApi.ts] pause() failed: recording is paused()')
@@ -151,13 +158,42 @@ export class RecordingApi {
         })
     }
 
-    // observer that can be updated to track the relevant FreeFormView
-    // public setFreeFormView = (view: CollectionFreeFormView): void => {
-    //     observe(view, 'Document', (change) => {
-    //         if (change.name === '_panX') {
-    //             this.trackMovements(view.Document._panX, view.Document._panY)
-    //         }
-    //     }
+    // 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) 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)
+        )
+    }
+
+    // call on dispose function to stop tracking movements
+    public removeRecordingFFView = (): void => {
+        this.disposeFunc?.();
+        this.disposeFunc = null;
+    }
+
+
+    // 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)
     // }
 
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1db90a65a..100526983 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -118,8 +118,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _keyframeEditing = false;
     @observable ChildDrag: DocumentView | undefined; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
 
-    @observable storedMovements: object[] = []; // stores the movement if in recoding mode
-
     @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
     @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); }
     @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); }
@@ -959,38 +957,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     }
 
-
-    followMovements = (): void => {
-        // need the first for subtraction
-        let first = null;
-
-        this.storedMovements.forEach(movement => {
-            if (first === null) first = movement.time;
-
-            // set the pan to what was stored
-            setTimeout(() => {
-                this.Document._panX = movement.panX;
-                this.Document._panY = movement.panY;
-            }, movement.time - first)
-        })
-
-        // for now, clear the movements
-        this.storedMovements = []
-    }
-
     @action
     setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
-        // if not presenting, just retrace the movements
-        if (Doc.UserDoc()?.presentationMode === 'watching') {
-            this.followMovements()
-            return;
-        }
-
-        if (Doc.UserDoc()?.presentationMode === 'recording') {
-            // store as many movments as possible
-            this.storedMovements.push({time: new Date().getTime(), panX, panY})
-        }
-
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
             const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
-- 
cgit v1.2.3-70-g09d2


From 1eb2c362b020b3cbe446bbc1585108129fda6977 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Wed, 4 May 2022 01:11:54 -0400
Subject: Get storing pres data working, but is choppy due to mobx usage.

---
 src/client/apis/recording/recordingApi.ts          | 300 --------------------
 src/client/util/RecordingApi.ts                    | 303 +++++++++++++++++++++
 src/client/views/Main.tsx                          |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   5 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  14 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  26 +-
 src/client/views/nodes/VideoBox.tsx                |  14 +
 7 files changed, 348 insertions(+), 316 deletions(-)
 delete mode 100644 src/client/apis/recording/recordingApi.ts
 create mode 100644 src/client/util/RecordingApi.ts

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/apis/recording/recordingApi.ts b/src/client/apis/recording/recordingApi.ts
deleted file mode 100644
index b57e675b7..000000000
--- a/src/client/apis/recording/recordingApi.ts
+++ /dev/null
@@ -1,300 +0,0 @@
-import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm";
-import React, { useState } from "react";
-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 = (view: CollectionFreeFormView, 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 | undefined => {
-        // TODO: maybe archive the data?
-        if (this.isRecording) {
-            console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() or finish() first')
-            return new Error('[recordingApi.ts] clear()')
-        }
-
-        // clear presenation data
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        // clear isRecording
-        this.isRecording = false
-        // clear absoluteStart
-        this.absoluteStart = -1
-
-        // clear the disposeFunc
-        this.disposeFunc = null
-    }
-
-    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) 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 => {
-        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
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
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 8560ccb29..517fe097c 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,6 +8,7 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions";
 import { Docs } from "../documents/Documents";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { LinkManager } from "../util/LinkManager";
+import { RecordingApi } from "../util/RecordingApi";
 import { CollectionView } from "./collections/CollectionView";
 import { MainView } from "./MainView";
 
@@ -36,5 +37,6 @@ AssignAllExtensions();
     const expires = "expires=" + d.toUTCString();
     document.cookie = `loadtime=${loading};${expires};path=/`;
     new LinkManager();
+    new RecordingApi;
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 35bd9cf79..f74e526b6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,7 +54,7 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import e = require("connect-flash");
-import { RecordingApi } from "../../../apis/recording/RecordingApi";
+import { RecordingApi } from "../../../util/RecordingApi";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -965,6 +965,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
+        // set the current FFview to the tab being panned.
+        RecordingApi.Instance.setRecordingFFView(this)
+
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
             const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 86358e838..1b17476f7 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,7 +1,7 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
-import { AudioField, VideoField } from "../../../../fields/URLField";
+import { VideoField } from "../../../../fields/URLField";
 import { Upload } from "../../../../server/SharedMediaTypes";
 import { ViewBoxBaseComponent } from "../../DocComponent";
 import { FieldView } from "../FieldView";
@@ -9,6 +9,8 @@ import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 import { DocumentType } from "../../../documents/DocumentTypes";
 
+import { RecordingApi } from "../../../util/RecordingApi";
+
 
 @observer
 export class RecordingBox extends ViewBoxBaseComponent() {
@@ -31,17 +33,19 @@ export class RecordingBox extends ViewBoxBaseComponent() {
 
       @action
       setResult = (info: Upload.FileInformation) => {
-            console.log("Setting result to " + info)
+            // console.log("Setting result to " + info)
             this.result = info
-            console.log(this.result.accessPaths.agnostic.client)
+            // console.log(this.result.accessPaths.agnostic.client)
             this.dataDoc.type = DocumentType.VID;
-            console.log(this.videoDuration)
+            // console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
             this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
             // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
             // this.layoutDoc._fitWidth = undefined;
-            this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+          this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+          // stringify the presenation and store it
+            this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
       }
 
       render() {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index d99492095..870ef87d7 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -8,7 +8,7 @@ import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
 
-import { RecordingApi } from '../../../apis/recording/RecordingApi';
+import { RecordingApi } from '../../../util/RecordingApi';
 
 
 enum RecordingStatus {
@@ -57,11 +57,11 @@ export function RecordingView(props: IRecordingViewProps) {
                   height: 720,
             },
             // audio: true,
-            // audio: {
-            //       echoCancellation: true,
-            //       noiseSuppression: true,
-            //       sampleRate: 44100
-            // }
+            audio: {
+                  echoCancellation: true,
+                  noiseSuppression: true,
+                  sampleRate: 44100
+            }
       }
 
       useEffect(() => {
@@ -159,7 +159,10 @@ export function RecordingView(props: IRecordingViewProps) {
             // }
 
             videoRecorder.current.onstart = (event: any) => {
-                  setRecording(true);
+                setRecording(true);
+                // TODO: update names
+                // RecordingApi.Instance.clear();
+                RecordingApi.Instance.start();
             }
 
             videoRecorder.current.onstop = () => {
@@ -172,7 +175,8 @@ export function RecordingView(props: IRecordingViewProps) {
                   // reset the temporary chunks
                   videoChunks = []
                   setRecording(false);
-                  setFinished(true);
+                setFinished(true);
+                RecordingApi.Instance.pause();
             }
 
             // recording paused
@@ -182,12 +186,14 @@ export function RecordingView(props: IRecordingViewProps) {
 
                   // reset the temporary chunks
                   videoChunks = []
-                  setRecording(false);
+                setRecording(false);
+                RecordingApi.Instance.pause();
             }
 
             videoRecorder.current.onresume = async (event: any) => {
                   await startShowingStream();
-                  setRecording(true);
+                setRecording(true);
+                RecordingApi.Instance.resume();
             }
 
             videoRecorder.current.start(200)
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e57cb1abe..7364a64d9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -28,6 +28,7 @@ import { AnchorMenu } from "../pdf/AnchorMenu";
 import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
+import { RecordingApi } from "../../util/RecordingApi";
 const path = require('path');
 
 
@@ -117,6 +118,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return field?.url.href ?? vfield?.url.href ?? "";
     }
 
+    // returns the presentation data if it exists, null otherwise
+    @computed get presentation() {
+        const data = this.dataDoc[this.fieldKey + '-presentation'];
+        return data ? JSON.parse(data) : null;
+    }
+
 
     @computed private get timeline() { return this._stackedTimeline; }
     private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
@@ -147,6 +154,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // plays video
     @action public Play = (update: boolean = true) => {
+        // if presentation isn't null, call followmovements on the recording api
+        if (this.presentation) {
+            const err = RecordingApi.Instance.followMovements(this.presentation);
+            if (err) console.log(err);
+        }
+
+
         this._playing = true;
         const eleTime = this.player?.currentTime || 0;
         if (this.timeline) {
-- 
cgit v1.2.3-70-g09d2


From 4dc4b0939d4e4afbc9f6db999ff80d434ef4ccc6 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Wed, 4 May 2022 02:00:55 -0400
Subject: Added the presentation mode back in. However, pres isn't being
 recreated smoothly after being stored in the db.

---
 src/client/util/RecordingApi.ts                     | 21 +++++++++++++++++----
 .../collectionFreeForm/CollectionFreeFormView.tsx   |  6 ++++--
 .../views/nodes/RecordingBox/RecordingView.tsx      |  3 ---
 3 files changed, 21 insertions(+), 9 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index ab7642a90..b53cba79d 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -1,6 +1,7 @@
 import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
 import { IReactionDisposer, observable, reaction } from "mobx";
 import { NumCast } from "../../fields/Types";
+import { Doc } from "../../fields/Doc";
 
 type Movement = {
     time: number,
@@ -42,6 +43,7 @@ export class RecordingApi {
 
         // used for tracking movements in the view frame
         this.disposeFunc = null;
+        this.recordingFFView = null;
 
         // for now, set playFFView
         this.playFFView = null;
@@ -60,6 +62,9 @@ export class RecordingApi {
             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
@@ -80,6 +85,9 @@ export class RecordingApi {
             return new Error('[recordingApi.ts] clear()')
         }
 
+        // update the presentation mode
+        Doc.UserDoc().presentationMode = 'none'
+
         const presCopy = { ...this.currentPresentation }
 
         // clear presenation data
@@ -133,8 +141,8 @@ export class RecordingApi {
     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()')
+            console.error('[recordingApi.ts] trackMovements() failed: recording is paused()')
+            return new Error('[recordingApi.ts] trackMovements()')
         }
 
         // get the time
@@ -149,11 +157,12 @@ export class RecordingApi {
 
     // 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 === null || view === this.playFFView) return;
+        if (view === this.recordingFFView || view === null) return;
         
         //this.recordingFFView = view;
         // set the reaction to track the movements
@@ -163,7 +172,7 @@ export class RecordingApi {
         )
 
         // for now, set the most recent recordingFFView to the playFFView
-        this.playFFView = view;
+        this.recordingFFView = view;
     }
 
     // call on dispose function to stop tracking movements
@@ -194,6 +203,10 @@ export class RecordingApi {
     //     })
     // }
 
+    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')
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f74e526b6..8bcf6f46f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -965,8 +965,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
-        // set the current FFview to the tab being panned.
-        RecordingApi.Instance.setRecordingFFView(this)
+        // set the current respective FFview to the tab being panned.
+        RecordingApi.Instance.setRecordingFFView(this);
+        // TODO: make this based off the specific recording FFView
+        Doc.UserDoc()?.presentationMode !== 'recording' && RecordingApi.Instance.setPlayFFView(this);
 
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 51cc44941..f7446b556 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -160,7 +160,6 @@ export function RecordingView(props: IRecordingViewProps) {
 
             videoRecorder.current.onstart = (event: any) => {
                 setRecording(true);
-                // TODO: update names
                 // RecordingApi.Instance.clear();
                 RecordingApi.Instance.start();
             }
@@ -176,7 +175,6 @@ export function RecordingView(props: IRecordingViewProps) {
                   videoChunks = []
                   setRecording(false);
                 setFinished(true);
-                console.log("finished recording")
                 RecordingApi.Instance.pause();
             }
 
@@ -188,7 +186,6 @@ export function RecordingView(props: IRecordingViewProps) {
                   // reset the temporary chunks
                   videoChunks = []
                 setRecording(false);
-                console.log("paused recording")
                 RecordingApi.Instance.pause();
             }
 
-- 
cgit v1.2.3-70-g09d2


From a2173243c7a447527e2e86ec0a00998ed8b9cc40 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Wed, 4 May 2022 03:30:07 -0400
Subject: Get play and pause to work. There is a bug with getting double called
 that was causing choppiness.

---
 src/client/util/RecordingApi.ts                    | 207 ++++++++++++++-------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 src/client/views/nodes/VideoBox.tsx                |  10 +-
 3 files changed, 148 insertions(+), 71 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index b53cba79d..dfe8e98ca 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -183,11 +183,10 @@ export class RecordingApi {
 
     // TODO: extract this into different class with pause and resume recording
     private playFFView: CollectionFreeFormView | null;
-    private timers: Timer[] | null;
+    private timers: NodeJS.Timeout[] | null;
 
-    // public followMovements = (presentation: Presentation): undefined | Error => {
-    //     console.log(presentation)
-    //     if (presentation.startDate === null || this.playFFView === null) {
+    // public loadPresentation = (presentation: Presentation): undefined | Error => {
+    //     if (presentation.startTime === null || this.playFFView === null) {
     //         return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
     //     }
 
@@ -195,48 +194,85 @@ export class RecordingApi {
     //     const { movements } = presentation
     //     this.timers = movements.map(movement => {
     //         const { panX, panY, time } = movement
-    //         return new Timer(() => {
+
+    //         const t = new Timer(() => {
     //             document._panX = panX;
     //             document._panY = panY;
     //             // TODO: consider cleaning this array to null or some state
-    //         }, time)
+    //         }, time - presentation.startTime)
+    //         t.pause()
+    //         return t
     //     })
+    //     console.log(this.timers)
     // }
 
     public setPlayFFView = (view: CollectionFreeFormView): void => {
         this.playFFView = view
     }
 
+    // public pauseMovements = (): undefined | Error => {
+    //     console.log('[recordingApi.ts] pauseMovements()')
+    //     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 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')
+        if (!this._isPlaying) {
+            return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
         }
-        this.timers?.forEach(timer => timer.resume())
+        // TODO: set userdoc presentMode to browsing
+        //console.log('cleared timers', this.timers)
+        console.log(this.timers?.map(timer => clearTimeout(timer)))
+        console.log('[recordingApi.ts] pauseMovements()')
+
+        this._isPlaying = false
     }
 
-    public followMovements = (presentation: Presentation): undefined | Error => {
+    // public resumeMovements = (): undefined | Error => {
+    //     if (this.playFFView === null) {
+    //         return new Error('[recordingApi.ts] resumeMovements() failed: no view')
+    //     }
+    //     console.log('resume')
+    //     this.timers?.forEach(timer => timer.resume())
+    // }
+
+    private _isPlaying = false;
+
+    public playMovements = (presentation: Presentation, timeViewed: number = 0): undefined | Error => {
         if (presentation.startTime === 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')
+        }
+        this._isPlaying = true;
+
+        console.log(timeViewed)
+
         const document = this.playFFView.Document
         const { movements } = presentation
-        movements.forEach(movement => {
+        this.timers = movements.reduce((arr: NodeJS.Timeout[], movement) => {
             const { panX, panY, time } = movement
+            const absoluteTime = time - presentation.startTime - timeViewed*1000
+            if (absoluteTime < 0) return arr;
+
             // set the pan to what was stored
-            setTimeout(() => {
+            arr.push(setTimeout(() => {
                 document._panX = panX;
                 document._panY = panY;
-            }, time - presentation.startTime)
-        })
+            }, absoluteTime))
+            return arr;
+        }, [])
+
+        console.log(this.timers.length)
     }
     // Unfinished code for tracing multiple free form views
     // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
@@ -258,58 +294,95 @@ export class RecordingApi {
 /** 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;
+class Timer {
+    private timerId: NodeJS.Timeout | null;
+    private callback: (...args: any[]) => void
+    private start: number
+    private remaining: number;
+
+    constructor(callback: (...args: any[]) => void, delay: number) {
+        this.callback = callback;
+        this.remaining = delay;
+
+        this.start = Date.now();
+        this.timerId = setTimeout(this.callback, this.remaining);
     }
 
-    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;
-    }
+    public pause = () => {
+        console.log('[timer.ts] pause()')
+        this.timerId !== null && clearTimeout(this.timerId);
+        this.timerId = null;
+        this.remaining -= (Date.now() - this.start);
+    };
 
-    /** resumes the timer */
-    public resume(): Timer {
-        if (!this._durationTimeoutId && !this._done) {
-            this._start = new Date;
-            this._durationTimeoutId = setTimeout(this._callback, this._remaining);
+    public resume = () => {
+        if (this.timerId) {
+            return;
         }
-        return this;
-    }
 
-    /** 
-     * clears the timeout and marks it as done. 
-     * 
-     * After called, the timeout will not resume
-     */
-    public clearTimeout() {
-        this._clearTimeoutRef();
-        this._done = true;
-    }
+        this.start = Date.now();
+        this.timerId = setTimeout(this.callback, this.remaining);
+    };
 
-    private _clearTimeoutRef() {
-        if (this._durationTimeoutId) {
-            clearTimeout(this._durationTimeoutId);
-            this._durationTimeoutId = undefined;
-        }
-    }
-
-}
\ No newline at end of file
+    public clear = () => {
+        console.log('[timer.ts] clear()')
+        this.timerId !== null && clearTimeout(this.timerId);
+        // this.timerId = null;
+        // this.remaining -= (Date.now() - this.start);
+    };
+}
+// 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
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8bcf6f46f..214d4bbdc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -966,7 +966,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
         // set the current respective FFview to the tab being panned.
-        RecordingApi.Instance.setRecordingFFView(this);
+        Doc.UserDoc()?.presentationMode === 'recording' && RecordingApi.Instance.setRecordingFFView(this);
         // TODO: make this based off the specific recording FFView
         Doc.UserDoc()?.presentationMode !== 'recording' && RecordingApi.Instance.setPlayFFView(this);
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 7364a64d9..15a198a00 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -156,11 +156,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @action public Play = (update: boolean = true) => {
         // if presentation isn't null, call followmovements on the recording api
         if (this.presentation) {
-            const err = RecordingApi.Instance.followMovements(this.presentation);
-            if (err) console.log(err);
+            const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0);
+            err && console.log(err)
         }
 
-
         this._playing = true;
         const eleTime = this.player?.currentTime || 0;
         if (this.timeline) {
@@ -198,6 +197,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // pauses video
     @action public Pause = (update: boolean = true) => {
+        if (this.presentation) {
+            const err = RecordingApi.Instance.pauseMovements();
+            err && console.log(err);
+        }
+
         this._playing = false;
         this.removeCurrentlyPlaying();
         try {
-- 
cgit v1.2.3-70-g09d2


From 32f1fb8df81b5dfc12539090c3fe589099bf165b Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Wed, 4 May 2022 04:43:22 -0400
Subject: Pause works with presentation now.

---
 src/client/util/RecordingApi.ts                    | 86 ++++++++++++----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +-
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 3 files changed, 56 insertions(+), 39 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index e779a4ab1..ee427e5df 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -2,6 +2,7 @@ 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,
@@ -10,17 +11,15 @@ type Movement = {
 }
 
 type Presentation = {
-    movements: Array<Movement>
+    movements: Array<Movement> | null
     meta: Object,
-    startTime: number | null,
 }
 
 export class RecordingApi {
 
     private static NULL_PRESENTATION: Presentation = {
-        movements: [],
+        movements: null,
         meta: {},
-        startTime: null,
     }
 
     // instance variables
@@ -52,7 +51,7 @@ export class RecordingApi {
 
     // little helper :)
     private get isInitPresenation(): boolean {
-        return this.currentPresentation.startTime === null
+        return this.currentPresentation.movements === null
     }
 
     public start = (meta?: Object): Error | undefined => {
@@ -68,12 +67,12 @@ export class RecordingApi {
         // (1a) get start date for presenation
         const startDate = new Date()
         // (1b) set start timestamp to absolute timestamp
-        // this.absoluteStart = startDate.getTime()
+        this.absoluteStart = startDate.getTime()
 
         // (2) assign meta content if it exists
         this.currentPresentation.meta = meta || {}
         // (3) assign start date to currentPresenation
-        this.currentPresentation.startTime = startDate.getTime()
+        this.currentPresentation.movements = []
         // (4) set isRecording true to allow trackMovements
         this.isRecording = true
     }
@@ -110,39 +109,35 @@ export class RecordingApi {
         // don't allow track movments
         this.isRecording = false
 
-        // set relativeStart to the pausedTimestamp
+        // set adjust absoluteStart to add the time difference
         const timestamp = new Date().getTime()
-        this.absoluteStart = timestamp
+        this.absoluteStart = timestamp - this.absoluteStart
     }
 
     public resume = () => {
-        const timestamp = new Date().getTime()
-        const startTimestamp = this.currentPresentation.startTime
-        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
+        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) {
-            console.error('[recordingApi.ts] trackMovements() failed: recording is paused()')
             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 timestamp = new Date().getTime()
-
-        // make new movement struct
-        const movement: Movement = { time: timestamp, panX, panY }
+        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.push(movement)
+        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
     }
 
     // instance variable for the FFView
@@ -172,6 +167,7 @@ export class RecordingApi {
     }
 
     // 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;
 
@@ -185,42 +181,58 @@ export class RecordingApi {
         }
 
         if (!this._isPlaying) {
-            return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+            //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+            return
         }
+        this._isPlaying = false
         // TODO: set userdoc presentMode to browsing
-        //console.log('cleared timers', this.timers)
-        console.log(this.timers?.map(timer => clearTimeout(timer)))
-        console.log('[recordingApi.ts] pauseMovements()')
+        this.timers?.map(timer => clearTimeout(timer))
 
-        this._isPlaying = false
+        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): undefined | Error => {
-        if (presentation.startTime === null || this.playFFView === null) {
+    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 new Error('[recordingApi.ts] playMovements() failed: already playing')
+            return
         }
         this._isPlaying = true;
+        // Doc.UserDoc().presentationMode = 'watching';
 
-        console.log(timeViewed)
+        // TODO: consider this bug at the end of the clip on seek
+        console.log(timeViewed, videoBox?.player?.currentTime)
+        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 absoluteTime = time - presentation.startTime - timeViewed*1000
-            if (absoluteTime < 0) return arr;
+
+            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;
-            }, absoluteTime))
+                // if (movement === movements[movements.length - 1]) {
+                //     this._isPlaying = false;
+                // }
+            }, timeDiff))
             return arr;
         }, [])
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 214d4bbdc..9e57bca4f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -968,7 +968,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // set the current respective FFview to the tab being panned.
         Doc.UserDoc()?.presentationMode === 'recording' && RecordingApi.Instance.setRecordingFFView(this);
         // TODO: make this based off the specific recording FFView
-        Doc.UserDoc()?.presentationMode !== 'recording' && RecordingApi.Instance.setPlayFFView(this);
+        Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this);
+        if (Doc.UserDoc()?.presentationMode === 'watching') {
+            Doc.UserDoc().presentationMode = 'none';
+            // RecordingApi.Instance.pauseMovements();
+            RecordingApi.Instance.pauseVideoAndMovements();
+        }
 
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 15a198a00..1ff77e373 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -156,7 +156,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @action public Play = (update: boolean = true) => {
         // if presentation isn't null, call followmovements on the recording api
         if (this.presentation) {
-            const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0);
+            const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
             err && console.log(err)
         }
 
-- 
cgit v1.2.3-70-g09d2


From a9ff0c90656de71e37edafba68e946807d41403f Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Wed, 4 May 2022 13:36:13 -0400
Subject: fix infinite pause/play bug with a stopper, but still very buggy

---
 src/client/util/RecordingApi.ts                      |  3 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx    |  4 ++--
 .../views/nodes/RecordingBox/RecordingView.scss      | 20 ++++++++++++--------
 .../views/nodes/RecordingBox/RecordingView.tsx       |  2 +-
 src/client/views/nodes/VideoBox.tsx                  |  5 +++++
 5 files changed, 22 insertions(+), 12 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index 8fb503b00..fec579486 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -188,7 +188,7 @@ export class RecordingApi {
         // TODO: set userdoc presentMode to browsing
         this.timers?.map(timer => clearTimeout(timer))
 
-        this.videoBox = null;
+        // this.videoBox = null;
     }
     
     private videoBox: VideoBox | null = null;
@@ -236,6 +236,7 @@ export class RecordingApi {
             }, timeDiff))
             return arr;
         }, [])
+
     }
 
     // Unfinished code for tracing multiple free form views
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9e57bca4f..aa2e0c417 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -970,9 +970,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // TODO: make this based off the specific recording FFView
         Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this);
         if (Doc.UserDoc()?.presentationMode === 'watching') {
-            Doc.UserDoc().presentationMode = 'none';
-            // RecordingApi.Instance.pauseMovements();
             RecordingApi.Instance.pauseVideoAndMovements();
+            Doc.UserDoc().presentationMode = 'none';
+            // RecordingApi.Instance.pauseMovements()
         }
 
         if (!this.isAnnotationOverlay && clamp) {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index 1fea231b7..d1a50a82e 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -1,9 +1,11 @@
 video {
-      flex: 100%;
+    // flex: 100%;
       width: 100%;
-      min-height: 400px;
-      height: auto;
-      display: block;
+      // min-height: 400px;
+      //height: auto;
+      height: 100%;
+      //display: block;
+      object-fit: cover;
       background-color: black;
 }
 
@@ -14,18 +16,20 @@ button {
 .recording-container {
       height: 100%;
       width: 100%;
-      display: flex;
+      // display: flex;
       pointer-events: all;
+      background-color: grey;
 }
 
 .video-wrapper {
-      max-width: 600px;
-      max-width: 700px;
+    //   max-width: 600px;
+    //   max-width: 700px;
       position: relative;
       display: flex;
       justify-content: center;
-      overflow: hidden;
+    //   overflow: hidden;
       border-radius: 10px;
+      margin: 0;
 }
 
 .video-wrapper:hover .controls {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index f7446b556..e7e431b49 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -258,7 +258,7 @@ export function RecordingView(props: IRecordingViewProps) {
             return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
       }
 
-      return (
+    return (
             <div className="recording-container">
                   <div className="video-wrapper">
                         <video id="video"
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1ff77e373..226c2b1ce 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -154,6 +154,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // plays video
     @action public Play = (update: boolean = true) => {
+        if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) {
+            console.log('VideoBox : Play : presentation mode', this._playing);
+            return;
+        }
+
         // if presentation isn't null, call followmovements on the recording api
         if (this.presentation) {
             const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
-- 
cgit v1.2.3-70-g09d2

