From 7c9001006c1c7ffc10ab0bf0fe5a8b4af85f987f Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Tue, 14 Jun 2022 13:06:13 -0400
Subject: got init views based on the current tabs open - able to remove my
 code in collection free form view.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx       | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 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 5534ddd35..33474bc4b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1006,14 +1006,17 @@ 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.
-        (Doc.UserDoc()?.presentationMode === 'recording') && RecordingApi.Instance.setRecordingFFView(this);
+        // if (Doc.UserDoc()?.presentationMode === 'recording') {
+        //     // RecordingApi.Instance.setRecordingFFView(this);
+        //     console.log('setRecordingFFView', this);
+        // } 
         // TODO: make this based off the specific recording FFView
-        (Doc.UserDoc()?.presentationMode === 'none') && RecordingApi.Instance.setPlayFFView(this);
-        if (Doc.UserDoc()?.presentationMode === 'watching') {
-            RecordingApi.Instance.pauseVideoAndMovements();
-            Doc.UserDoc().presentationMode = 'none';
-            // RecordingApi.Instance.pauseMovements()
-        }
+        // (Doc.UserDoc()?.presentationMode === 'none') && RecordingApi.Instance.setPlayFFView(this);
+        // if (Doc.UserDoc()?.presentationMode === 'watching') {
+        //     RecordingApi.Instance.pauseVideoAndMovements();
+        //     Doc.UserDoc().presentationMode = 'none';
+        //     // RecordingApi.Instance.pauseMovements()
+        // }
 
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
-- 
cgit v1.2.3-70-g09d2


From 844b84bc7440e80252b4892138c47192d4cb0cc1 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Tue, 14 Jun 2022 17:03:53 -0400
Subject: got the tab to open, but can't replay the movements on the tab

---
 src/client/util/RecordingApi.ts                    | 64 +++++++++++++++-------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 2 files changed, 44 insertions(+), 22 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 2b8d11818..44dc4593c 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -10,13 +10,15 @@ import { DocumentManager } from "./DocumentManager";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import { Id } from "../../fields/FieldSymbols";
 import { returnAll } from "../../Utils";
+import { ContextExclusionPlugin } from "webpack";
+import { DocServer } from "../DocServer";
 
 type Movement = {
     time: number,
     panX: number,
     panY: number,
     scale: number,
-    docId: String,
+    docId: string,
 }
 
 
@@ -88,20 +90,20 @@ export class RecordingApi {
     }
 
     // in the case where only one tab was changed (updates not across dashboards), set only one to true
-    private updateRecordingFFViewsFromTabs = (docList: Doc[], onlyOne = false) => {
+    private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => {
         if (this.recordingFFViews === null) return;
 
         // so that the size comparisons are correct, we must filter to only the FFViews
         const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform';
-        const tabbedFFViews = new Set<String>();
-        for (const DashDoc of docList) {
+        const tabbedFFViews = new Set<string>();
+        for (const DashDoc of tabbedDocs) {
             if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]);
         }
 
 
         // new tab was added - need to add it
         if (tabbedFFViews.size > this.recordingFFViews.size) {
-            for (const DashDoc of docList) {
+            for (const DashDoc of tabbedDocs) {
                 if (!this.recordingFFViews.has(DashDoc[Id])) {
                     isFFView(DashDoc) && this.addRecordingFFView(DashDoc);
 
@@ -184,10 +186,9 @@ export class RecordingApi {
             // update the presentation mode now that we are done tracking
             Doc.UserDoc().presentationMode = 'none';
         }
+
         // clear presenation data
         this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        // clear isRecording
-        // this.tracking = false
         // clear absoluteStart
         this.absoluteStart = -1
     }
@@ -201,7 +202,7 @@ export class RecordingApi {
         }
     }
 
-    private trackMovements = (panX: number, panY: number, docId: String, scale: number = 0) => {
+    private trackMovements = (panX: number, panY: number, docId: string, scale: number = 0) => {
         // ensure we are recording
         if (!this.tracking) {
             console.error('[recordingApi.ts] trackMovements(): tracking is false')
@@ -281,25 +282,46 @@ export class RecordingApi {
         let preScale = -1;
         const zoomAndPan = (movement: Movement) => {
             const { panX, panY, scale } = movement;
-            (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+            (scale !== 0 && 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)
+        // zoomAndPan(filteredMovements[0]);
+
+        // generate a set of all unique docIds
+        const docIds = new Set(filteredMovements.map(movement => movement.docId))
+        // TODO: optimize only ons-first load
+        // TODO: make async await
+        // TODO: make sure the cahce still hs the id
+        // this will load the cache, so getCachedREfFields won't have to reach server
+        DocServer.GetRefFields([...docIds]).then(refFields => { 
+            console.log('refFields', refFields)
+            const openTab = (docId: string) => {
+                const isInView = DocumentManager.Instance.getDocumentViewById(docId);
+                if (isInView) { return isInView; }
+
+                const doc = DocServer.GetCachedRefField(docId);
+                CollectionDockingView.AddSplit(doc, 'right');
+                return DocumentManager.Instance.getDocumentView(doc);
+            }
+
+            // 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(() => {
+                    // open tab if it is not already open
+                    const view = openTab(movement.docId);
+                    // const ffView = view.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+                    console.log('dview', view, 'ffView', view?.props)
+                    // 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)
+            });
         })
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 33474bc4b..a0e0f4f8d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1011,7 +1011,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         //     console.log('setRecordingFFView', this);
         // } 
         // TODO: make this based off the specific recording FFView
-        // (Doc.UserDoc()?.presentationMode === 'none') && RecordingApi.Instance.setPlayFFView(this);
+        (Doc.UserDoc()?.presentationMode === 'none' || Doc.UserDoc()?.presentationMode === 'watching') && RecordingApi.Instance.setPlayFFView(this);
         // if (Doc.UserDoc()?.presentationMode === 'watching') {
         //     RecordingApi.Instance.pauseVideoAndMovements();
         //     Doc.UserDoc().presentationMode = 'none';
-- 
cgit v1.2.3-70-g09d2


From 83690c334c8d343d6d7560a6aa7f8e7be9cd3639 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Tue, 14 Jun 2022 17:48:45 -0400
Subject: done for day - get the functionality down. will make code more
 readable and fix some small bugs tomorrow

---
 src/client/util/RecordingApi.ts                    | 43 +++++++++++++++-------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 3 files changed, 31 insertions(+), 16 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 99085e658..4bbaa9a79 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -12,6 +12,7 @@ import { Id } from "../../fields/FieldSymbols";
 import { returnAll } from "../../Utils";
 import { ContextExclusionPlugin } from "webpack";
 import { DocServer } from "../DocServer";
+import { DocumentView } from "../views/nodes/DocumentView";
 
 type Movement = {
     time: number,
@@ -105,10 +106,12 @@ export class RecordingApi {
         if (tabbedFFViews.size > this.recordingFFViews.size) {
             for (const DashDoc of tabbedDocs) {
                 if (!this.recordingFFViews.has(DashDoc[Id])) {
-                    isFFView(DashDoc) && this.addRecordingFFView(DashDoc);
+                    if (isFFView(DashDoc)) {
+                        this.addRecordingFFView(DashDoc);
 
-                    // only one max change, so return
-                    if (onlyOne) return;
+                        // only one max change, so return
+                        if (onlyOne) return;
+                    }
                 }
             }
         }
@@ -134,6 +137,7 @@ export class RecordingApi {
             reaction(() => CollectionDockingView.Instance.props.Document.data,
                 (change) => {
                     // TODO: consider changing between dashboards
+                    console.log('change in tabs', change);
                     this.updateRecordingFFViewsFromTabs(DocListCast(change), true);
                 });
 
@@ -197,20 +201,23 @@ export class RecordingApi {
         if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; }
 
         for (const [id, disposeFunc] of this.recordingFFViews) {
+            console.log('calling dispose func : docId', id);
             disposeFunc();
             this.recordingFFViews.delete(id);
         }
     }
 
     private trackMovements = (panX: number, panY: number, docId: string, scale: number = 0) => {
-        // ensure we are recording
+        // ensure we are recording to track
         if (!this.tracking) {
             console.error('[recordingApi.ts] trackMovements(): tracking is false')
             return;
         }
-        // check to see if the presetation is init
+        // check to see if the presetation is init - if not, we are between segments
+        // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres)
+        // bacuse tracking should be false inbetween segments high key 
         if (this.nullPresentation) {
-            console.error('[recordingApi.ts] trackMovements(): no presentation')
+            console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments')
             return;
         }
 
@@ -236,7 +243,7 @@ export class RecordingApi {
     // 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')
+            // return new Error('[recordingApi.ts] pauseMovements() failed: no view')
         }
 
         if (!this._isPlaying) {
@@ -263,7 +270,7 @@ export class RecordingApi {
     public _isPlaying = false;
 
     public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
-        if (presentation.movements === null || this.playFFView === null) {
+        if (presentation.movements === null) {å //|| this.playFFView === null) {
             return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
         }
         if (this._isPlaying) return;
@@ -296,14 +303,20 @@ export class RecordingApi {
         // TODO: optimize only ons-first load
         // TODO: make async await
         // TODO: make sure the cahce still hs the id
+        // TODO: if they are open, set them to their first move
         // this will load the cache, so getCachedREfFields won't have to reach server
         DocServer.GetRefFields([...docIds]).then(refFields => { 
             console.log('refFields', refFields)
-            const openTab = (docId: string) => {
+
+            const openTab = (docId: string) : DocumentView | undefined => {
                 const isInView = DocumentManager.Instance.getDocumentViewById(docId);
                 if (isInView) { return isInView; }
 
-                const doc = DocServer.GetCachedRefField(docId);
+                const doc = DocServer.GetCachedRefField(docId) as Doc;
+                if (doc == undefined) {
+                    console.warn('Doc server cache did not contain docId', docId)
+                    return undefined;
+                }
                 CollectionDockingView.AddSplit(doc, 'right');
                 return DocumentManager.Instance.getDocumentView(doc);
             }
@@ -314,10 +327,12 @@ export class RecordingApi {
                 return setTimeout(() => {
                     // open tab if it is not already open
                     const view = openTab(movement.docId);
-                    // const ffView = view.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
-                    const collectionFFView = view?.ComponentView;
-                    // replay the movement
-                    zoomAndPan(movement, collectionFFView as CollectionFreeFormView);
+                    if (view) { 
+                        const collectionFFView = view.ComponentView;
+                        console.log(collectionFFView instanceof CollectionFreeFormView)
+                        // replay the movement
+                        zoomAndPan(movement, collectionFFView as CollectionFreeFormView);
+                    }
                     // if last movement, presentation is done -> set the instance var
                     if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
                 }, timeDiff)
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a0e0f4f8d..3c5a42b7d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1011,7 +1011,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         //     console.log('setRecordingFFView', this);
         // } 
         // TODO: make this based off the specific recording FFView
-        (Doc.UserDoc()?.presentationMode === 'none' || Doc.UserDoc()?.presentationMode === 'watching') && RecordingApi.Instance.setPlayFFView(this);
+        // (Doc.UserDoc()?.presentationMode === 'none' || Doc.UserDoc()?.presentationMode === 'watching') && RecordingApi.Instance.setPlayFFView(this);
         // if (Doc.UserDoc()?.presentationMode === 'watching') {
         //     RecordingApi.Instance.pauseVideoAndMovements();
         //     Doc.UserDoc().presentationMode = 'none';
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b14a1f0f6..cf3282d2f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -366,7 +366,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                   this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
                         time => {
                               !this._playing && (vref.currentTime = time);
-                              console.log("vref time = " + vref.currentTime)
+                              // console.log("vref time = " + vref.currentTime)
                         }, { fireImmediately: true });
             }
       }
-- 
cgit v1.2.3-70-g09d2


From 5972716e64fd9a006fa2139a40b03c21b503dd04 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Wed, 15 Jun 2022 13:50:33 -0400
Subject: fix small disposer bug and reintroduce pausing on interaction

---
 src/client/util/RecordingApi.ts                    | 26 +++++++++++++---------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +++-
 2 files changed, 18 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 48ea12fd9..ae5431a03 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -57,7 +57,7 @@ export class RecordingApi {
         this.absoluteStart = -1;
 
         // used for tracking movements in the view frame
-        this.recordingFFViews = null;
+        this.recordingFFViews = new Map();
         this.tabChangeDisposeFunc = null;
 
         // for now, set playFFView
@@ -126,9 +126,8 @@ export class RecordingApi {
         }
     }
 
-    public start = (meta?: Object) => {
+    public initTabTracker = () => {
         // init the dispose funcs on the page
-        this.recordingFFViews = new Map();
         const docList = DocListCast(CollectionDockingView.Instance.props.Document.data);
         this.updateRecordingFFViewsFromTabs(docList);
 
@@ -140,7 +139,9 @@ export class RecordingApi {
                     console.log('change in tabs', change);
                     this.updateRecordingFFViewsFromTabs(DocListCast(change), true);
                 });
+    }
 
+    public start = (meta?: Object) => {
         // update the presentation mode
         Doc.UserDoc().presentationMode = 'recording';
 
@@ -238,10 +239,7 @@ export class RecordingApi {
     // play movemvents will recreate them when the user resumes the presentation
     public pauseMovements = (): undefined | Error => {
 
-        if (!this._isPlaying) {
-            //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
-            return
-        }
+        if (!this._isPlaying) { console.warn('[recordingApi.ts] pauseMovements(): already on paused'); return;}
         this._isPlaying = false
         // TODO: set userdoc presentMode to browsing
         this.timers?.map(timer => clearTimeout(timer))
@@ -250,6 +248,7 @@ export class RecordingApi {
     }
 
     private videoBoxDisposeFunc: IReactionDisposer | null = null;
+    private videoBox: VideoBox | null = null;
 
     setVideoBox = (videoBox: VideoBox) => {
         console.log('setVideoBox', videoBox);
@@ -258,20 +257,25 @@ export class RecordingApi {
             reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
                 ({ playing, timeViewed }) => 
                     playing ? this.playMovements(videoBox.presentation, timeViewed) : this.pauseMovements()
-                );
+            );
+        this.videoBox = videoBox;
     }
 
     removeVideoBox = () => {
         if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
         this.videoBoxDisposeFunc();
+
+        this.videoBox = null;
+        this.videoBoxDisposeFunc = null;
     }
 
 
     // by calling pause on the VideoBox, the pauseMovements will be called
-    public pauseVideoAndMovements = () => {
-        // this.videoBox?.Pause()
+    public pauseFromInteraction = () => {
+        Doc.UserDoc().presentationMode = 'none';
+        this.videoBox?.Pause();
 
-        this.pauseMovements()
+        this.pauseMovements();
         // return this.videoBox == null
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3c5a42b7d..7db3b1482 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1009,7 +1009,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // if (Doc.UserDoc()?.presentationMode === 'recording') {
         //     // RecordingApi.Instance.setRecordingFFView(this);
         //     console.log('setRecordingFFView', this);
-        // } 
+        // }
         // TODO: make this based off the specific recording FFView
         // (Doc.UserDoc()?.presentationMode === 'none' || Doc.UserDoc()?.presentationMode === 'watching') && RecordingApi.Instance.setPlayFFView(this);
         // if (Doc.UserDoc()?.presentationMode === 'watching') {
@@ -1017,6 +1017,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         //     Doc.UserDoc().presentationMode = 'none';
         //     // RecordingApi.Instance.pauseMovements()
         // }
+        // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code.
+        if (Doc.UserDoc()?.presentationMode === 'watching') RecordingApi.Instance.pauseFromInteraction();
 
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
-- 
cgit v1.2.3-70-g09d2


From 98fba8bdb0fe81d6f71d0ae6018fcaaf7d8897df Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Wed, 15 Jun 2022 15:39:55 -0400
Subject: Refactor RecordingApi into two main files - TrackMovements and
 ReplayMovements. TrackMovements is for recording presentations and
 ReplayMovments is for replaying them.

---
 src/client/util/RecordingApi.ts                    | 461 ---------------------
 src/client/util/ReplayMovements.ts                 | 204 +++++++++
 src/client/util/TrackMovements.ts                  | 257 ++++++++++++
 src/client/views/Main.tsx                          |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  16 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   2 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  10 +-
 src/client/views/nodes/VideoBox.tsx                |   8 +-
 8 files changed, 477 insertions(+), 487 deletions(-)
 delete mode 100644 src/client/util/RecordingApi.ts
 create mode 100644 src/client/util/ReplayMovements.ts
 create mode 100644 src/client/util/TrackMovements.ts

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
deleted file mode 100644
index 87cb85497..000000000
--- a/src/client/util/RecordingApi.ts
+++ /dev/null
@@ -1,461 +0,0 @@
-import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
-import { IReactionDisposer, observable, observe, reaction } from "mobx";
-import { NumCast } from "../../fields/Types";
-import { Doc, DocListCast } from "../../fields/Doc";
-import { VideoBox } from "../views/nodes/VideoBox";
-import { isArray } from "lodash";
-import { SelectionManager } from "./SelectionManager";
-import { DocumentDecorations } from "../views/DocumentDecorations";
-import { DocumentManager } from "./DocumentManager";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { Id } from "../../fields/FieldSymbols";
-import { returnAll } from "../../Utils";
-import { ContextExclusionPlugin } from "webpack";
-import { DocServer } from "../DocServer";
-import { DocumentView } from "../views/nodes/DocumentView";
-
-type Movement = {
-    time: number,
-    panX: number,
-    panY: number,
-    scale: number,
-    docId: string,
-}
-
-
-export type Presentation = {
-    movements: Movement[] | null,
-    totalTime: number,
-    meta: Object | Object[],
-}
-
-export class RecordingApi {
-
-    private static get NULL_PRESENTATION(): Presentation {
-        return { movements: null, meta: {}, totalTime: -1, }
-    }
-
-    // instance variables
-    private currentPresentation: Presentation;
-    private tracking: boolean;
-    private absoluteStart: number;
-    // instance variable for holding the FFViews and their disposers
-    private recordingFFViews: Map<string, IReactionDisposer> | null;
-    private tabChangeDisposeFunc: IReactionDisposer | null;
-
-
-    // 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.tracking = false;
-        this.absoluteStart = -1;
-
-        // used for tracking movements in the view frame
-        this.recordingFFViews = null;
-        this.tabChangeDisposeFunc = null;
-
-        // for now, set playFFView
-        // this.playFFView = null;
-        this.timers = null;
-    }
-
-    // little helper :)
-    private get nullPresentation(): boolean {
-        return this.currentPresentation.movements === null
-    }
-
-    private addRecordingFFView(doc: Doc, key: string = doc[Id]): void {
-        console.info('adding dispose func : docId', key, 'doc', doc);
-
-        if (this.recordingFFViews === null) { console.warn('addFFView on null RecordingApi'); return; }
-        if (this.recordingFFViews.has(key)) { console.warn('addFFView : key already in map'); return; }
-
-        const disposeFunc = reaction(
-            () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0)}),
-            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, key, res.scale),
-        );
-        this.recordingFFViews?.set(key, disposeFunc);
-    }
-
-    private removeRecordingFFView = (key: string) => {
-        console.info('removing dispose func : docId', key);
-        if (this.recordingFFViews === null) { console.warn('removeFFView on null RecordingApi'); return; }
-        this.recordingFFViews.get(key)?.();
-        this.recordingFFViews.delete(key);
-    }
-
-    // in the case where only one tab was changed (updates not across dashboards), set only one to true
-    private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => {
-        if (this.recordingFFViews === null) return;
-
-        // so that the size comparisons are correct, we must filter to only the FFViews
-        const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform';
-        const tabbedFFViews = new Set<string>();
-        for (const DashDoc of tabbedDocs) {
-            if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]);
-        }
-
-
-        // new tab was added - need to add it
-        if (tabbedFFViews.size > this.recordingFFViews.size) {
-            for (const DashDoc of tabbedDocs) {
-                if (!this.recordingFFViews.has(DashDoc[Id])) {
-                    if (isFFView(DashDoc)) {
-                        this.addRecordingFFView(DashDoc);
-
-                        // only one max change, so return
-                        if (onlyOne) return;
-                    }
-                }
-            }
-        }
-        // tab was removed - need to remove it from recordingFFViews
-        else if (tabbedFFViews.size < this.recordingFFViews.size) {
-            for (const [key] of this.recordingFFViews) {
-                if (!tabbedFFViews.has(key)) { 
-                    this.removeRecordingFFView(key);
-                    if (onlyOne) return;
-                } 
-            }
-        }
-    }
-
-    public initTabTracker = () => {
-        if (this.recordingFFViews === null) {
-            this.recordingFFViews = new Map();
-        }
-
-        // init the dispose funcs on the page
-        const docList = DocListCast(CollectionDockingView.Instance.props.Document.data);
-        this.updateRecordingFFViewsFromTabs(docList);
-
-        // create a reaction to monitor changes in tabs
-        this.tabChangeDisposeFunc =
-            reaction(() => CollectionDockingView.Instance.props.Document.data,
-                (change) => {
-                    // TODO: consider changing between dashboards
-                    console.log('change in tabs', change);
-                    this.updateRecordingFFViewsFromTabs(DocListCast(change), true);
-                });
-    }
-
-    public start = (meta?: Object) => {
-        this.initTabTracker();
-
-        // 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 tracking true to allow trackMovements
-        this.tracking = true
-    }
-
-    /* stops the video and returns the presentatation; if no presentation, returns undefined */
-    public yieldPresentation(clearData: boolean = true): Presentation | null {
-        // if no presentation or done tracking, return null
-        if (this.nullPresentation || !this.tracking) return null;
-
-        // set the previus recording view to the play view
-        // this.playFFView = this.recordingFFView;
-
-        // ensure we add the endTime now that they are done recording    
-        const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
-
-        // reset the current presentation
-        clearData && this.clear();
-
-        console.log('yieldPresentation', cpy);
-        return cpy;
-    }
-
-    public finish = (): void => {
-        // make is tracking false
-        this.tracking = false
-        // reset the RecordingApi instance
-        this.clear();
-    }
-
-    public clear = (): void => {
-        // clear the disposeFunc if we are done (not tracking)
-        if (!this.tracking) {
-            this.removeAllRecordingFFViews();
-            this.tabChangeDisposeFunc?.();
-            // update the presentation mode now that we are done tracking
-            Doc.UserDoc().presentationMode = 'none';
-
-            this.recordingFFViews = null;
-            this.tabChangeDisposeFunc = null;
-        }
-
-        // clear presenation data
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        // clear absoluteStart
-        this.absoluteStart = -1
-    }
-
-    private removeAllRecordingFFViews = () => {
-        if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; }
-
-        for (const [id, disposeFunc] of this.recordingFFViews) {
-            console.log('calling dispose func : docId', id);
-            disposeFunc();
-            this.recordingFFViews.delete(id);
-        }
-    }
-
-    private trackMovements = (panX: number, panY: number, docId: string, scale: number = 0) => {
-        // ensure we are recording to track
-        if (!this.tracking) {
-            console.error('[recordingApi.ts] trackMovements(): tracking is false')
-            return;
-        }
-        // check to see if the presetation is init - if not, we are between segments
-        // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres)
-        // bacuse tracking should be false inbetween segments high key 
-        if (this.nullPresentation) {
-            console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments')
-            return;
-        }
-
-        // get the time
-        const time = new Date().getTime() - this.absoluteStart
-        // make new movement object
-        const movement: Movement = { time, panX, panY, scale, docId }
-
-        // add that movement to the current presentation data's movement array
-        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
-    }
-
-    // TODO: extract this into different class with pause and resume recording
-    // TODO: store the FFview with the movements
-    private timers: NodeJS.Timeout[] | null; 
-
-    // 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._isPlaying) { console.warn('[recordingApi.ts] pauseMovements(): already on paused'); return;}
-        this._isPlaying = false
-        // TODO: set userdoc presentMode to browsing
-        this.timers?.map(timer => clearTimeout(timer))
-
-        // this.videoBox = null;
-    }
-
-    private videoBoxDisposeFunc: IReactionDisposer | null = null;
-    private videoBox: VideoBox | null = null;
-
-    setVideoBox = async (videoBox: VideoBox) => {
-        console.log('setVideoBox', videoBox);
-        if (videoBox !== null) { console.warn('setVideoBox on already videoBox'); }
-        if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); }
-
-
-        const { presentation } = videoBox;
-        if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; }
-        
-        let docIdtoDoc: Map<string, Doc> = new Map();
-        try {
-            docIdtoDoc = await this.loadPresentation(presentation);
-        } catch {
-            console.error('[recordingApi.ts] setVideoBox(): error loading presentation - no replay movements');
-            throw 'error loading docs from server';
-        }
-
-        
-        this.videoBoxDisposeFunc =
-            reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
-                ({ playing, timeViewed }) => 
-                    playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
-            );
-        this.videoBox = videoBox;
-    }
-
-    removeVideoBox = () => {
-        if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
-        this.videoBoxDisposeFunc();
-
-        this.videoBox = null;
-        this.videoBoxDisposeFunc = null;
-    }
-
-
-    // by calling pause on the VideoBox, the pauseMovements will be called
-    public pauseFromInteraction = () => {
-        Doc.UserDoc().presentationMode = 'none';
-        this.videoBox?.Pause();
-
-        this.pauseMovements();
-        // return this.videoBox == null
-    }
-
-
-
-    public _isPlaying = false;
-
-    loadPresentation = async (presentation: Presentation) => {
-        const { movements } = presentation;
-        if (movements === null) {
-            throw '[recordingApi.ts] followMovements() failed: no presentation data';
-        }
-
-        // generate a set of all unique docIds
-        const docIds = new Set<string>();
-        for (const {docId} of movements) {
-            if (!docIds.has(docId)) docIds.add(docId);
-        }
-
-        const docIdtoDoc = new Map<string, Doc>();
-
-        let refFields = await DocServer.GetRefFields([...docIds.keys()]);
-        for (const docId in refFields) {
-            if (!refFields[docId]) {
-                throw `one field was undefined`;
-            }
-            docIdtoDoc.set(docId, refFields[docId] as Doc);
-        }
-        console.log('loadPresentation refFields', refFields, docIdtoDoc);
-
-        return docIdtoDoc;
-    }
-
-    // returns undefined if the docView isn't open on the screen
-    getCollectionFFView = (docId: string) => {
-        const isInView = DocumentManager.Instance.getDocumentViewById(docId);
-        if (isInView) { return isInView.ComponentView as CollectionFreeFormView; }
-    }
-
-    // will open the doc in a tab then return the CollectionFFView that holds it
-    openTab = (docId: string, docIdtoDoc: Map<string, Doc>) => {
-        const doc = docIdtoDoc.get(docId);
-        if (doc == undefined) {
-            console.error(`docIdtoDoc did not contain docId ${docId}`)
-            return undefined;
-        }
-        CollectionDockingView.AddSplit(doc, 'right');
-        const docView = DocumentManager.Instance.getDocumentViewById(docId);
-        return docView?.ComponentView as CollectionFreeFormView;
-    }
-
-    // helper to replay a movement
-    private preScale = -1;
-    zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => {
-        const { panX, panY, scale } = movement;
-        (scale !== 0 && this.preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
-        document.Document._panX = panX;
-        document.Document._panY = panY;
-
-        this.preScale = scale;
-    }
-
-    getFirstMovements = (movements: Movement[], timeViewed: number): Map<string, Movement> => {
-        if (movements === null) return new Map();
-        // generate a set of all unique docIds
-        const docIdtoFirstMove = new Map();
-        for (const move of movements) {
-            const { docId } = move;
-            if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
-        }
-        return docIdtoFirstMove;
-    }
-
-    endPlayingPresentation = () => {
-        this.preScale = -1;
-        RecordingApi.Instance._isPlaying = false;
-    }
-
-    public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
-        console.log('playMovements', presentation, timeViewed, docIdtoDoc);
-
-        if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) {
-            return new Error('[recordingApi.ts] followMovements() failed: no presentation data')
-        }
-        if (this._isPlaying) return;
-
-        this._isPlaying = true;
-        Doc.UserDoc().presentationMode = 'watching';
-
-        // only get the movements that are remaining in the video time left
-        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
-
-        const handleFirstMovements = () => {
-            // if the first movement is a closed tab, open it
-            const firstMovement = filteredMovements[0];
-            const isClosed = this.getCollectionFFView(firstMovement.docId) === undefined;
-            if (isClosed) this.openTab(firstMovement.docId, docIdtoDoc);
-
-            // for the open tabs, set it to the first move
-            const docIdtoFirstMove = this.getFirstMovements(filteredMovements, timeViewed);
-            for (const [docId, firstMove] of docIdtoFirstMove) {
-                const colFFView = this.getCollectionFFView(docId);
-                if (colFFView) this.zoomAndPan(firstMove, colFFView);
-            }
-        }
-        handleFirstMovements();
-        
-
-        // 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(() => {
-                const collectionFFView = this.getCollectionFFView(movement.docId);
-                if (collectionFFView) {
-                    this.zoomAndPan(movement, collectionFFView);
-                } else {
-                    // tab wasn't open - open it and play the movement
-                    const openedColFFView = this.openTab(movement.docId, docIdtoDoc);
-                    openedColFFView && this.zoomAndPan(movement, openedColFFView);
-                }
-
-                // if last movement, presentation is done -> cleanup :)
-                if (movement === filteredMovements[filteredMovements.length - 1]) {
-                    this.endPlayingPresentation();
-                }
-            }, timeDiff);
-        });
-    }
-
-    // method that concatenates an array of presentatations into one
-    public concatPresentations = (presentations: Presentation[]): Presentation => {
-        // these three will lead to the combined presentation
-        let combinedMovements: Movement[] = [];
-        let sumTime = 0;
-        let combinedMetas: any[] = [];
-
-        presentations.forEach((presentation) => {
-            const { movements, totalTime, meta } = presentation;
-
-            // update movements if they had one
-            if (movements) {
-                // add the summed time to the movements
-                const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
-                // concat the movements already in the combined presentation with these new ones
-                combinedMovements.push(...addedTimeMovements);
-            }
-
-            // update the totalTime
-            sumTime += totalTime;
-
-            // concatenate the metas
-            combinedMetas.push(meta);
-        });
-
-        // return the combined presentation with the updated total summed time
-		return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
-    }
-}
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
new file mode 100644
index 000000000..e46810b52
--- /dev/null
+++ b/src/client/util/ReplayMovements.ts
@@ -0,0 +1,204 @@
+import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
+import { IReactionDisposer, observable, observe, reaction } from "mobx";
+import { Doc } from "../../fields/Doc";
+import { VideoBox } from "../views/nodes/VideoBox";
+import { DocumentManager } from "./DocumentManager";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DocServer } from "../DocServer";
+import { Movement, Presentation } from "./TrackMovements";
+
+export class ReplayMovements {
+    private timers: NodeJS.Timeout[] | null; 
+    private videoBoxDisposeFunc: IReactionDisposer | null;
+    private videoBox: VideoBox | null;
+    private isPlaying: boolean;
+
+
+    // create static instance and getter for global use
+    @observable static _instance: ReplayMovements;
+    static get Instance(): ReplayMovements { return ReplayMovements._instance }
+    constructor() {
+        // init the global instance
+        ReplayMovements._instance = this;
+
+        // instance vars for replaying
+        this.timers = null;
+        this.videoBoxDisposeFunc = null;
+        this.videoBox = null;
+        this.isPlaying = false;
+    }
+
+    // pausing movements will dispose all timers that are planned to replay the movements
+    // play movemvents will recreate them when the user resumes the presentation
+    pauseMovements = (): undefined | Error => {
+        if (!this.isPlaying) {
+            console.warn('[recordingApi.ts] pauseMovements(): already on paused');
+            return;
+        }
+
+        this.isPlaying = false
+        // TODO: set userdoc presentMode to browsing
+        this.timers?.map(timer => clearTimeout(timer))
+    }
+
+    setVideoBox = async (videoBox: VideoBox) => {
+        console.log('setVideoBox', videoBox);
+        if (videoBox !== null) { console.warn('setVideoBox on already videoBox'); }
+        if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); }
+
+
+        const { presentation } = videoBox;
+        if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; }
+        
+        let docIdtoDoc: Map<string, Doc> = new Map();
+        try {
+            docIdtoDoc = await this.loadPresentation(presentation);
+        } catch {
+            console.error('[recordingApi.ts] setVideoBox(): error loading presentation - no replay movements');
+            throw 'error loading docs from server';
+        }
+
+        
+        this.videoBoxDisposeFunc =
+            reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+                ({ playing, timeViewed }) => 
+                    playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
+            );
+        this.videoBox = videoBox;
+    }
+
+    removeVideoBox = () => {
+        if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
+        this.videoBoxDisposeFunc();
+
+        this.videoBox = null;
+        this.videoBoxDisposeFunc = null;
+    }
+
+    // should be called from interacting with the screen
+    pauseFromInteraction = () => {
+        Doc.UserDoc().presentationMode = 'none';
+        this.videoBox?.Pause();
+
+        this.pauseMovements();
+    }
+
+    loadPresentation = async (presentation: Presentation) => {
+        const { movements } = presentation;
+        if (movements === null) {
+            throw '[recordingApi.ts] followMovements() failed: no presentation data';
+        }
+
+        // generate a set of all unique docIds
+        const docIds = new Set<string>();
+        for (const {docId} of movements) {
+            if (!docIds.has(docId)) docIds.add(docId);
+        }
+
+        const docIdtoDoc = new Map<string, Doc>();
+
+        let refFields = await DocServer.GetRefFields([...docIds.keys()]);
+        for (const docId in refFields) {
+            if (!refFields[docId]) {
+                throw `one field was undefined`;
+            }
+            docIdtoDoc.set(docId, refFields[docId] as Doc);
+        }
+        console.log('loadPresentation refFields', refFields, docIdtoDoc);
+
+        return docIdtoDoc;
+    }
+
+    // returns undefined if the docView isn't open on the screen
+    getCollectionFFView = (docId: string) => {
+        const isInView = DocumentManager.Instance.getDocumentViewById(docId);
+        if (isInView) { return isInView.ComponentView as CollectionFreeFormView; }
+    }
+
+    // will open the doc in a tab then return the CollectionFFView that holds it
+    openTab = (docId: string, docIdtoDoc: Map<string, Doc>) => {
+        const doc = docIdtoDoc.get(docId);
+        if (doc == undefined) {
+            console.error(`docIdtoDoc did not contain docId ${docId}`)
+            return undefined;
+        }
+        CollectionDockingView.AddSplit(doc, 'right');
+        const docView = DocumentManager.Instance.getDocumentViewById(docId);
+        return docView?.ComponentView as CollectionFreeFormView;
+    }
+
+    // helper to replay a movement
+    zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => {
+        const { panX, panY, scale } = movement;
+        scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+        document.Document._panX = panX;
+        document.Document._panY = panY;
+    }
+
+    getFirstMovements = (movements: Movement[], timeViewed: number): Map<string, Movement> => {
+        if (movements === null) return new Map();
+        // generate a set of all unique docIds
+        const docIdtoFirstMove = new Map();
+        for (const move of movements) {
+            const { docId } = move;
+            if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
+        }
+        return docIdtoFirstMove;
+    }
+
+    endPlayingPresentation = () => {
+        this.isPlaying = false;
+    }
+
+    public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
+        console.log('playMovements', presentation, timeViewed, docIdtoDoc);
+
+        if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) {
+            return new Error('[recordingApi.ts] followMovements() failed: no presentation data')
+        }
+        if (this.isPlaying) return;
+
+        this.isPlaying = true;
+        Doc.UserDoc().presentationMode = 'watching';
+
+        // only get the movements that are remaining in the video time left
+        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+        const handleFirstMovements = () => {
+            // if the first movement is a closed tab, open it
+            const firstMovement = filteredMovements[0];
+            const isClosed = this.getCollectionFFView(firstMovement.docId) === undefined;
+            if (isClosed) this.openTab(firstMovement.docId, docIdtoDoc);
+
+            // for the open tabs, set it to the first move
+            const docIdtoFirstMove = this.getFirstMovements(filteredMovements, timeViewed);
+            for (const [docId, firstMove] of docIdtoFirstMove) {
+                const colFFView = this.getCollectionFFView(docId);
+                if (colFFView) this.zoomAndPan(firstMove, colFFView);
+            }
+        }
+        handleFirstMovements();
+        
+
+        // 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(() => {
+                const collectionFFView = this.getCollectionFFView(movement.docId);
+                if (collectionFFView) {
+                    this.zoomAndPan(movement, collectionFFView);
+                } else {
+                    // tab wasn't open - open it and play the movement
+                    const openedColFFView = this.openTab(movement.docId, docIdtoDoc);
+                    openedColFFView && this.zoomAndPan(movement, openedColFFView);
+                }
+
+                // if last movement, presentation is done -> cleanup :)
+                if (movement === filteredMovements[filteredMovements.length - 1]) {
+                    this.endPlayingPresentation();
+                }
+            }, timeDiff);
+        });
+    }
+}
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
new file mode 100644
index 000000000..342bb440e
--- /dev/null
+++ b/src/client/util/TrackMovements.ts
@@ -0,0 +1,257 @@
+import { IReactionDisposer, observable, observe, reaction } from "mobx";
+import { NumCast } from "../../fields/Types";
+import { Doc, DocListCast } from "../../fields/Doc";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { Id } from "../../fields/FieldSymbols";
+
+export type Movement = {
+    time: number,
+    panX: number,
+    panY: number,
+    scale: number,
+    docId: string,
+}
+
+export type Presentation = {
+    movements: Movement[] | null,
+    totalTime: number,
+    meta: Object | Object[],
+}
+
+export class TrackMovements {
+
+    private static get NULL_PRESENTATION(): Presentation {
+        return { movements: null, meta: {}, totalTime: -1, }
+    }
+
+    // instance variables
+    private currentPresentation: Presentation;
+    private tracking: boolean;
+    private absoluteStart: number;
+    // instance variable for holding the FFViews and their disposers
+    private recordingFFViews: Map<string, IReactionDisposer> | null;
+    private tabChangeDisposeFunc: IReactionDisposer | null;
+
+
+    // create static instance and getter for global use
+    @observable static _instance: TrackMovements;
+    static get Instance(): TrackMovements { return TrackMovements._instance }
+    constructor() {
+        // init the global instance
+        TrackMovements._instance = this;
+
+        // init the instance variables
+        this.currentPresentation = TrackMovements.NULL_PRESENTATION
+        this.tracking = false;
+        this.absoluteStart = -1;
+
+        // used for tracking movements in the view frame
+        this.recordingFFViews = null;
+        this.tabChangeDisposeFunc = null;
+    }
+
+    // little helper :)
+    private get nullPresentation(): boolean {
+        return this.currentPresentation.movements === null
+    }
+
+    private addRecordingFFView(doc: Doc, key: string = doc[Id]): void {
+        console.info('adding dispose func : docId', key, 'doc', doc);
+
+        if (this.recordingFFViews === null) { console.warn('addFFView on null RecordingApi'); return; }
+        if (this.recordingFFViews.has(key)) { console.warn('addFFView : key already in map'); return; }
+
+        const disposeFunc = reaction(
+            () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0)}),
+            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovement(res.x, res.y, key, res.scale),
+        );
+        this.recordingFFViews?.set(key, disposeFunc);
+    }
+
+    private removeRecordingFFView = (key: string) => {
+        console.info('removing dispose func : docId', key);
+        if (this.recordingFFViews === null) { console.warn('removeFFView on null RecordingApi'); return; }
+        this.recordingFFViews.get(key)?.();
+        this.recordingFFViews.delete(key);
+    }
+
+    // in the case where only one tab was changed (updates not across dashboards), set only one to true
+    private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => {
+        if (this.recordingFFViews === null) return;
+
+        // so that the size comparisons are correct, we must filter to only the FFViews
+        const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform';
+        const tabbedFFViews = new Set<string>();
+        for (const DashDoc of tabbedDocs) {
+            if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]);
+        }
+
+
+        // new tab was added - need to add it
+        if (tabbedFFViews.size > this.recordingFFViews.size) {
+            for (const DashDoc of tabbedDocs) {
+                if (!this.recordingFFViews.has(DashDoc[Id])) {
+                    if (isFFView(DashDoc)) {
+                        this.addRecordingFFView(DashDoc);
+
+                        // only one max change, so return
+                        if (onlyOne) return;
+                    }
+                }
+            }
+        }
+        // tab was removed - need to remove it from recordingFFViews
+        else if (tabbedFFViews.size < this.recordingFFViews.size) {
+            for (const [key] of this.recordingFFViews) {
+                if (!tabbedFFViews.has(key)) { 
+                    this.removeRecordingFFView(key);
+                    if (onlyOne) return;
+                } 
+            }
+        }
+    }
+
+    private initTabTracker = () => {
+        if (this.recordingFFViews === null) {
+            this.recordingFFViews = new Map();
+        }
+
+        // init the dispose funcs on the page
+        const docList = DocListCast(CollectionDockingView.Instance.props.Document.data);
+        this.updateRecordingFFViewsFromTabs(docList);
+
+        // create a reaction to monitor changes in tabs
+        this.tabChangeDisposeFunc =
+            reaction(() => CollectionDockingView.Instance.props.Document.data,
+                (change) => {
+                    // TODO: consider changing between dashboards
+                    console.log('change in tabs', change);
+                    this.updateRecordingFFViewsFromTabs(DocListCast(change), true);
+                });
+    }
+
+    start = (meta?: Object) => {
+        this.initTabTracker();
+
+        // 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 tracking true to allow trackMovements
+        this.tracking = true
+    }
+
+    /* stops the video and returns the presentatation; if no presentation, returns undefined */
+    yieldPresentation(clearData: boolean = true): Presentation | null {
+        // if no presentation or done tracking, return null
+        if (this.nullPresentation || !this.tracking) return null;
+
+        // set the previus recording view to the play view
+        // this.playFFView = this.recordingFFView;
+
+        // ensure we add the endTime now that they are done recording    
+        const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+
+        // reset the current presentation
+        clearData && this.clear();
+
+        console.log('yieldPresentation', cpy);
+        return cpy;
+    }
+
+    finish = (): void => {
+        // make is tracking false
+        this.tracking = false
+        // reset the RecordingApi instance
+        this.clear();
+    }
+
+    private clear = (): void => {
+        // clear the disposeFunc if we are done (not tracking)
+        if (!this.tracking) {
+            this.removeAllRecordingFFViews();
+            this.tabChangeDisposeFunc?.();
+            // update the presentation mode now that we are done tracking
+            Doc.UserDoc().presentationMode = 'none';
+
+            this.recordingFFViews = null;
+            this.tabChangeDisposeFunc = null;
+        }
+
+        // clear presenation data
+        this.currentPresentation = TrackMovements.NULL_PRESENTATION
+        // clear absoluteStart
+        this.absoluteStart = -1
+    }
+
+    removeAllRecordingFFViews = () => {
+        if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; }
+
+        for (const [id, disposeFunc] of this.recordingFFViews) {
+            console.log('calling dispose func : docId', id);
+            disposeFunc();
+            this.recordingFFViews.delete(id);
+        }
+    }
+
+    private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => {
+        // ensure we are recording to track
+        if (!this.tracking) {
+            console.error('[recordingApi.ts] trackMovements(): tracking is false')
+            return;
+        }
+        // check to see if the presetation is init - if not, we are between segments
+        // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres)
+        // bacuse tracking should be false inbetween segments high key 
+        if (this.nullPresentation) {
+            console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments')
+            return;
+        }
+
+        // get the time
+        const time = new Date().getTime() - this.absoluteStart
+        // make new movement object
+        const movement: Movement = { time, panX, panY, scale, docId }
+
+        // add that movement to the current presentation data's movement array
+        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
+    }
+
+
+    // method that concatenates an array of presentatations into one
+    public concatPresentations = (presentations: Presentation[]): Presentation => {
+        // these three will lead to the combined presentation
+        let combinedMovements: Movement[] = [];
+        let sumTime = 0;
+        let combinedMetas: any[] = [];
+
+        presentations.forEach((presentation) => {
+            const { movements, totalTime, meta } = presentation;
+
+            // update movements if they had one
+            if (movements) {
+                // add the summed time to the movements
+                const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
+                // concat the movements already in the combined presentation with these new ones
+                combinedMovements.push(...addedTimeMovements);
+            }
+
+            // update the totalTime
+            sumTime += totalTime;
+
+            // concatenate the metas
+            combinedMetas.push(meta);
+        });
+
+        // return the combined presentation with the updated total summed time
+		return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
+    }
+}
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 517fe097c..c1b67ba19 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,7 +8,8 @@ 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 { ReplayMovements } from '../util/ReplayMovements';
+import { TrackMovements } from "../util/TrackMovements";
 import { CollectionView } from "./collections/CollectionView";
 import { MainView } from "./MainView";
 
@@ -37,6 +38,7 @@ AssignAllExtensions();
     const expires = "expires=" + d.toUTCString();
     document.cookie = `loadtime=${loading};${expires};path=/`;
     new LinkManager();
-    new RecordingApi;
+    new TrackMovements();
+    new ReplayMovements();
     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 7db3b1482..a661cebb8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -58,7 +58,7 @@ import React = require("react");
 import { FieldView, FieldViewProps } from "../../nodes/FieldView";
 import { InkTranscription } from "../../InkTranscription";
 import e = require("connect-flash");
-import { RecordingApi } from "../../../util/RecordingApi";
+import { ReplayMovements } from "../../../util/ReplayMovements";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -1005,20 +1005,8 @@ 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.
-        // if (Doc.UserDoc()?.presentationMode === 'recording') {
-        //     // RecordingApi.Instance.setRecordingFFView(this);
-        //     console.log('setRecordingFFView', this);
-        // }
-        // TODO: make this based off the specific recording FFView
-        // (Doc.UserDoc()?.presentationMode === 'none' || Doc.UserDoc()?.presentationMode === 'watching') && RecordingApi.Instance.setPlayFFView(this);
-        // if (Doc.UserDoc()?.presentationMode === 'watching') {
-        //     RecordingApi.Instance.pauseVideoAndMovements();
-        //     Doc.UserDoc().presentationMode = 'none';
-        //     // RecordingApi.Instance.pauseMovements()
-        // }
         // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code.
-        if (Doc.UserDoc()?.presentationMode === 'watching') RecordingApi.Instance.pauseFromInteraction();
+        if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction();
 
         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/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index a28677525..0ff7c4292 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -8,7 +8,7 @@ import { FieldView } from "../FieldView";
 import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 import { DocumentType } from "../../../documents/DocumentTypes";
-import { Presentation } from "../../../util/RecordingApi";
+import { Presentation } from "../../../util/TrackMovements";
 import { Doc } from "../../../../fields/Doc";
 import { Id } from "../../../../fields/FieldSymbols";
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index ec9838bdd..83ed6914e 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 { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-import { Presentation, RecordingApi } from '../../../util/RecordingApi';
+import { Presentation, TrackMovements } from '../../../util/TrackMovements';
 
 export interface MediaSegment {
     videoChunks: any[],
@@ -64,7 +64,7 @@ export function RecordingView(props: IRecordingViewProps) {
     useEffect(() => {
         if (finished) {
             // make the total presentation that'll match the concatted video
-            let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+            let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
 
             // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
             (async () => {
@@ -135,7 +135,7 @@ export function RecordingView(props: IRecordingViewProps) {
         videoRecorder.current.onstart = (event: any) => {
             setRecording(true);
             // start the recording api when the video recorder starts
-            trackScreen && RecordingApi.Instance.start();
+            trackScreen && TrackMovements.Instance.start();
         };
 
         videoRecorder.current.onstop = () => {
@@ -149,7 +149,7 @@ export function RecordingView(props: IRecordingViewProps) {
                 };
 
                 // depending on if a presenation exists, add it to the video
-                const presentation = RecordingApi.Instance.yieldPresentation();
+                const presentation = TrackMovements.Instance.yieldPresentation();
                 setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
             }
 
@@ -174,7 +174,7 @@ export function RecordingView(props: IRecordingViewProps) {
         stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
 
         // finish/clear the recoringApi
-        RecordingApi.Instance.finish();
+        TrackMovements.Instance.finish();
 
         // this will call upon progessbar to update videos to be in the correct order
         setFinished(true);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 34df03954..5a221fea4 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -28,9 +28,9 @@ import { AnchorMenu } from "../pdf/AnchorMenu";
 import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
-import { Presentation, RecordingApi } from "../../util/RecordingApi";
-import { List } from "../../../fields/List";
+import { Presentation } from "../../util/TrackMovements";
 import { RecordingBox } from "./RecordingBox";
+import { ReplayMovements } from "../../util/ReplayMovements";
 const path = require('path');
 
 
@@ -148,7 +148,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
           
           // if presentation data exists, pass it to the recordingPi
           if (this.presentation != null) {
-            RecordingApi.Instance.setVideoBox(this);  
+            ReplayMovements.Instance.setVideoBox(this);  
           }
       }
 
@@ -159,7 +159,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
           
           // dispose the recordingApi's observer
           if (this.presentation != null) {
-            RecordingApi.Instance.removeVideoBox();  
+            ReplayMovements.Instance.removeVideoBox();  
           }
       }
 
-- 
cgit v1.2.3-70-g09d2

