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/views/nodes/VideoBox.tsx | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

(limited to 'src/client/views/nodes/VideoBox.tsx')

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 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/nodes/VideoBox.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/nodes/VideoBox.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 51b4d65fe05939ea27639bc1b5cc66ec99fa54e5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 4 May 2022 11:30:32 -0400
Subject: fixes to make recordingBox appear on slides after reopening.

---
 src/client/views/OverlayView.scss                |  71 +-
 src/client/views/OverlayView.tsx                 | 390 +++++------
 src/client/views/nodes/VideoBox.tsx              |   2 +-
 src/client/views/nodes/trails/PresElementBox.tsx | 836 +++++++++++------------
 4 files changed, 650 insertions(+), 649 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index d6a6e4301..42efecb98 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,59 +1,60 @@
 .overlayView {
-      position: absolute;
-      pointer-events: none;
-      top: 0;
-      width: 100vw;
-      height: 100vh;
-      /* background-color: pink; */
-      z-index: 100000;
+    position: absolute;
+    pointer-events: none;
+    top: 0;
+    width: 100vw;
+    height: 100vh;
+    /* background-color: pink; */
+    z-index: 100000;
 }
 
 .overlayWindow-outerDiv {
-      border-radius: 5px;
-      overflow: hidden;
-      display: flex;
-      flex-direction: column;
-      top: 0;
-      left: 0;
+    border-radius: 5px;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    top: 0;
+    left: 0;
 }
 
 .overlayWindow-outerDiv,
 .overlayView-wrapperDiv {
-      position: absolute;
-      z-index: 1;
+    position: absolute;
+    z-index: 1;
 }
 
 .overlayWindow-titleBar {
-      flex: 0 1 30px;
-      background: darkslategray;
-      color: whitesmoke;
-      text-align: center;
-      cursor: move;
+    flex: 0 1 30px;
+    background: darkslategray;
+    color: whitesmoke;
+    text-align: center;
+    cursor: move;
 }
 
 .overlayWindow-content {
-      flex: 1 1 auto;
-      display: flex;
-      flex-direction: column;
+    flex: 1 1 auto;
+    display: flex;
+    flex-direction: column;
 }
 
 .overlayWindow-closeButton {
-      float: right;
-      height: 30px;
-      width: 30px;
+    float: right;
+    height: 30px;
+    width: 30px;
 }
 
 .overlayWindow-resizeDragger {
-      background-color: rgb(0, 0, 0);
-      position: absolute;
-      right: 0px;
-      bottom: 0px;
-      width: 10px;
-      height: 10px;
-      cursor: nwse-resize;
+    background-color: rgb(0, 0, 0);
+    position: absolute;
+    right: 0px;
+    bottom: 0px;
+    width: 10px;
+    height: 10px;
+    cursor: nwse-resize;
 }
 
 .overlayView-doc {
-      z-index: 9002; //so that it appears above chroma 
-      position: absolute;
+    z-index: 9002; //so that it appears above chroma 
+    position: absolute;
+    pointer-events: all;
 }
\ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index f86406997..4b8e87b0f 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -2,7 +2,7 @@ import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
 import ReactLoading from 'react-loading';
-import { Doc } from "../../fields/Doc";
+import { Doc, WidthSym, HeightSym } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
 import { Cast, NumCast } from "../../fields/Types";
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, setupMoveUpEvents, Utils } from "../../Utils";
@@ -20,213 +20,213 @@ import { DefaultStyleProvider } from "./StyleProvider";
 export type OverlayDisposer = () => void;
 
 export type OverlayElementOptions = {
-      x: number;
-      y: number;
-      width?: number;
-      height?: number;
-      title?: string;
+    x: number;
+    y: number;
+    width?: number;
+    height?: number;
+    title?: string;
 };
 
 export interface OverlayWindowProps {
-      children: JSX.Element;
-      overlayOptions: OverlayElementOptions;
-      onClick: () => void;
+    children: JSX.Element;
+    overlayOptions: OverlayElementOptions;
+    onClick: () => void;
 }
 
 @observer
 export class OverlayWindow extends React.Component<OverlayWindowProps> {
-      @observable x: number;
-      @observable y: number;
-      @observable width: number;
-      @observable height: number;
-      constructor(props: OverlayWindowProps) {
-            super(props);
-
-            const opts = props.overlayOptions;
-            this.x = opts.x;
-            this.y = opts.y;
-            this.width = opts.width || 200;
-            this.height = opts.height || 200;
-      }
-
-      onPointerDown = (_: React.PointerEvent) => {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.removeEventListener("pointerup", this.onPointerUp);
-            document.addEventListener("pointermove", this.onPointerMove);
-            document.addEventListener("pointerup", this.onPointerUp);
-      }
-
-      onResizerPointerDown = (_: React.PointerEvent) => {
-            document.removeEventListener("pointermove", this.onResizerPointerMove);
-            document.removeEventListener("pointerup", this.onResizerPointerUp);
-            document.addEventListener("pointermove", this.onResizerPointerMove);
-            document.addEventListener("pointerup", this.onResizerPointerUp);
-      }
-
-      @action
-      onPointerMove = (e: PointerEvent) => {
-            this.x += e.movementX;
-            this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
-            this.y += e.movementY;
-            this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
-      }
-
-      @action
-      onResizerPointerMove = (e: PointerEvent) => {
-            this.width += e.movementX;
-            this.width = Math.max(this.width, 30);
-            this.height += e.movementY;
-            this.height = Math.max(this.height, 30);
-      }
-
-      onPointerUp = (e: PointerEvent) => {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.removeEventListener("pointerup", this.onPointerUp);
-      }
-
-      onResizerPointerUp = (e: PointerEvent) => {
-            document.removeEventListener("pointermove", this.onResizerPointerMove);
-            document.removeEventListener("pointerup", this.onResizerPointerUp);
-      }
-
-      render() {
-            return (
-                  <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
-                        <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
-                              {this.props.overlayOptions.title || "Untitled"}
-                              <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
-                        </div>
-                        <div className="overlayWindow-content">
-                              {this.props.children}
-                        </div>
-                        <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
-                  </div>
-            );
-      }
+    @observable x: number;
+    @observable y: number;
+    @observable width: number;
+    @observable height: number;
+    constructor(props: OverlayWindowProps) {
+        super(props);
+
+        const opts = props.overlayOptions;
+        this.x = opts.x;
+        this.y = opts.y;
+        this.width = opts.width || 200;
+        this.height = opts.height || 200;
+    }
+
+    onPointerDown = (_: React.PointerEvent) => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        document.addEventListener("pointermove", this.onPointerMove);
+        document.addEventListener("pointerup", this.onPointerUp);
+    }
+
+    onResizerPointerDown = (_: React.PointerEvent) => {
+        document.removeEventListener("pointermove", this.onResizerPointerMove);
+        document.removeEventListener("pointerup", this.onResizerPointerUp);
+        document.addEventListener("pointermove", this.onResizerPointerMove);
+        document.addEventListener("pointerup", this.onResizerPointerUp);
+    }
+
+    @action
+    onPointerMove = (e: PointerEvent) => {
+        this.x += e.movementX;
+        this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
+        this.y += e.movementY;
+        this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
+    }
+
+    @action
+    onResizerPointerMove = (e: PointerEvent) => {
+        this.width += e.movementX;
+        this.width = Math.max(this.width, 30);
+        this.height += e.movementY;
+        this.height = Math.max(this.height, 30);
+    }
+
+    onPointerUp = (e: PointerEvent) => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+    }
+
+    onResizerPointerUp = (e: PointerEvent) => {
+        document.removeEventListener("pointermove", this.onResizerPointerMove);
+        document.removeEventListener("pointerup", this.onResizerPointerUp);
+    }
+
+    render() {
+        return (
+            <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
+                <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
+                    {this.props.overlayOptions.title || "Untitled"}
+                    <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
+                </div>
+                <div className="overlayWindow-content">
+                    {this.props.children}
+                </div>
+                <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
+            </div>
+        );
+    }
 }
 
 @observer
 export class OverlayView extends React.Component {
-      public static Instance: OverlayView;
-      @observable.shallow
-      private _elements: JSX.Element[] = [];
-
-      constructor(props: any) {
-            super(props);
-            if (!OverlayView.Instance) {
-                  OverlayView.Instance = this;
-            }
-      }
-
-      @action
-      addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
-            const remove = action(() => {
-                  const index = this._elements.indexOf(ele);
-                  if (index !== -1) this._elements.splice(index, 1);
+    public static Instance: OverlayView;
+    @observable.shallow
+    private _elements: JSX.Element[] = [];
+
+    constructor(props: any) {
+        super(props);
+        if (!OverlayView.Instance) {
+            OverlayView.Instance = this;
+        }
+    }
+
+    @action
+    addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+        const remove = action(() => {
+            const index = this._elements.indexOf(ele);
+            if (index !== -1) this._elements.splice(index, 1);
+        });
+        ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
+            transform: `translate(${options.x}px, ${options.y}px)`,
+            width: options.width,
+            height: options.height,
+            top: 0,
+            left: 0
+        }}>{ele}</div>;
+        this._elements.push(ele);
+        return remove;
+    }
+
+    @action
+    addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+        const remove = action(() => {
+            const index = this._elements.indexOf(contents);
+            if (index !== -1) this._elements.splice(index, 1);
+        });
+        contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
+        this._elements.push(contents);
+        return remove;
+    }
+
+
+    @computed get overlayDocs() {
+        return CurrentUserUtils.OverlayDocs?.map(d => {
+            let offsetx = 0, offsety = 0;
+            const dref = React.createRef<HTMLDivElement>();
+            const onPointerMove = action((e: PointerEvent, down: number[]) => {
+                if (e.buttons === 1) {
+                    d.x = e.clientX + offsetx;
+                    d.y = e.clientY + offsety;
+                }
+                if (e.metaKey) {
+                    const dragData = new DragManager.DocumentDragData([d]);
+                    dragData.offset = [-offsetx, -offsety];
+                    dragData.dropAction = "move";
+                    dragData.removeDocument = (doc: Doc | Doc[]) => {
+                        const docs = (doc instanceof Doc) ? [doc] : doc;
+                        docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", d));
+                        return true;
+                    };
+                    dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
+                        return dragData.removeDocument!(doc) ? addDocument(doc) : false;
+                    };
+                    DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
+                    return true;
+                }
+                return false;
             });
-            ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
-                  transform: `translate(${options.x}px, ${options.y}px)`,
-                  width: options.width,
-                  height: options.height,
-                  top: 0,
-                  left: 0
-            }}>{ele}</div>;
-            this._elements.push(ele);
-            return remove;
-      }
-
-      @action
-      addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
-            const remove = action(() => {
-                  const index = this._elements.indexOf(contents);
-                  if (index !== -1) this._elements.splice(index, 1);
-            });
-            contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
-            this._elements.push(contents);
-            return remove;
-      }
-
-
-      @computed get overlayDocs() {
-            return CurrentUserUtils.OverlayDocs?.map(d => {
-                  let offsetx = 0, offsety = 0;
-                  const dref = React.createRef<HTMLDivElement>();
-                  const onPointerMove = action((e: PointerEvent, down: number[]) => {
-                        if (e.buttons === 1) {
-                              d.x = e.clientX + offsetx;
-                              d.y = e.clientY + offsety;
-                        }
-                        if (e.metaKey) {
-                              const dragData = new DragManager.DocumentDragData([d]);
-                              dragData.offset = [-offsetx, -offsety];
-                              dragData.dropAction = "move";
-                              dragData.removeDocument = (doc: Doc | Doc[]) => {
-                                    const docs = (doc instanceof Doc) ? [doc] : doc;
-                                    docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", d));
-                                    return true;
-                              };
-                              dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
-                                    return dragData.removeDocument!(doc) ? addDocument(doc) : false;
-                              };
-                              DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
-                              return true;
-                        }
-                        return false;
-                  });
-
-                  const onPointerDown = (e: React.PointerEvent) => {
-                        setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
-                        offsetx = NumCast(d.x) - e.clientX;
-                        offsety = NumCast(d.y) - e.clientY;
-                  };
-
-                  return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
-                        <DocumentView
-                              Document={d}
-                              rootSelected={returnTrue}
-                              bringToFront={emptyFunction}
-                              addDocument={undefined}
-                              removeDocument={undefined}
-                              PanelWidth={() => NumCast(d._width)}
-                              PanelHeight={() => NumCast(d._height)}
-                              ScreenToLocalTransform={Transform.Identity}
-                              renderDepth={1}
-                              isDocumentActive={returnTrue}
-                              isContentActive={emptyFunction}
-                              whenChildContentsActiveChanged={emptyFunction}
-                              focus={DocUtils.DefaultFocus}
-                              styleProvider={DefaultStyleProvider}
-                              layerProvider={undefined}
-                              docViewPath={returnEmptyDoclist}
-                              addDocTab={returnFalse}
-                              pinToPres={emptyFunction}
-                              docFilters={returnEmptyFilter}
-                              docRangeFilters={returnEmptyFilter}
-                              searchFilterDocs={returnEmptyDoclist}
-                              ContainingCollectionView={undefined}
-                              ContainingCollectionDoc={undefined} />
-                  </div>;
-            });
-      }
-
-      public static ShowSpinner() {
-            return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 });
-      }
-
-
-      render() {
-            return (
-                  <div className="overlayView" id="overlayView">
-                        <div>
-                              {this._elements}
-                        </div>
-                        <CollectionFreeFormLinksView key="freeformLinks" />
-                        {this.overlayDocs}
-                  </div>
-            );
-      }
+
+            const onPointerDown = (e: React.PointerEvent) => {
+                setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
+                offsetx = NumCast(d.x) - e.clientX;
+                offsety = NumCast(d.y) - e.clientY;
+            };
+
+            return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
+                <DocumentView
+                    Document={d}
+                    rootSelected={returnTrue}
+                    bringToFront={emptyFunction}
+                    addDocument={undefined}
+                    removeDocument={doc => (doc instanceof Doc ? [doc] : doc).map(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), "data", doc)).length ? true : false}
+                    PanelWidth={d[WidthSym]}
+                    PanelHeight={d[HeightSym]}
+                    ScreenToLocalTransform={() => new Transform(-NumCast(d.x), -NumCast(d.y), 1)}
+                    renderDepth={1}
+                    isDocumentActive={returnTrue}
+                    isContentActive={emptyFunction}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    focus={DocUtils.DefaultFocus}
+                    styleProvider={DefaultStyleProvider}
+                    layerProvider={undefined}
+                    docViewPath={returnEmptyDoclist}
+                    addDocTab={returnFalse}
+                    pinToPres={emptyFunction}
+                    docFilters={returnEmptyFilter}
+                    docRangeFilters={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                    ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined} />
+            </div>;
+        });
+    }
+
+    public static ShowSpinner() {
+        return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 });
+    }
+
+
+    render() {
+        return (
+            <div className="overlayView" id="overlayView">
+                <div>
+                    {this._elements}
+                </div>
+                <CollectionFreeFormLinksView key="freeformLinks" />
+                {this.overlayDocs}
+            </div>
+        );
+    }
 }
 // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error
 ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) {
-      OverlayView.Instance.addWindow(<ScriptingRepl />, options);
+    OverlayView.Instance.addWindow(<ScriptingRepl />, options);
 });
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e57cb1abe..b424225e7 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -305,7 +305,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
         if (Number.isFinite(this.player!.duration)) {
             this.rawDuration = this.player!.duration;
-        }
+        } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
     });
 
 
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index c13f469cc..cd7b09017 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -31,429 +31,429 @@ import { List } from "../../../../fields/List";
  */
 @observer
 export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
-      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
-      _heightDisposer: IReactionDisposer | undefined;
-
-      @observable _dragging = false;
-      // these fields are conditionally computed fields on the layout document that take this document as a parameter
-      @computed get indexInPres() { return Number(this.lookupField("indexInPres")); }  // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
-      @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox.  We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
-      @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
-      @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
-      @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
-      @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
-
-
-      @observable isShowingVideo = false;
-      @action setIsShowingVideo(shown: boolean) {
-            this.isShowingVideo = shown
-      }
-
-      componentDidMount() {
-            this.layoutDoc.hideLinkButton = true;
-            this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
-                  params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
-      }
-      componentWillUnmount() {
-            this._heightDisposer?.();
-      }
-
-      /**
-       * Returns a local transformed coordinate array for given coordinates.
-       */
-      ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
-
-      embedHeight = (): number => 97;
-      // embedWidth = () => this.props.PanelWidth();
-      // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
-      embedWidth = (): number => this.props.PanelWidth() - 35;
-      styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
-            if (property === StyleProp.Opacity) return 1;
-            return this.props.styleProvider?.(doc, props, property);
-      }
-      /**
-       * The function that is responsible for rendering a preview or not for this
-       * presentation element.
-       */
-      @computed get renderEmbeddedInline() {
-            return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
-                  <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
-                        <DocumentView
-                              Document={this.targetDoc}
-                              DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
-                              styleProvider={this.styleProvider}
-                              layerProvider={this.props.layerProvider}
-                              docViewPath={returnEmptyDoclist}
-                              rootSelected={returnTrue}
-                              addDocument={returnFalse}
-                              removeDocument={returnFalse}
-                              isContentActive={this.props.isContentActive}
-                              addDocTab={returnFalse}
-                              pinToPres={returnFalse}
-                              fitContentsToDoc={returnTrue}
-                              PanelWidth={this.embedWidth}
-                              PanelHeight={this.embedHeight}
-                              ScreenToLocalTransform={Transform.Identity}
-                              moveDocument={this.props.moveDocument!}
-                              renderDepth={this.props.renderDepth + 1}
-                              focus={DocUtils.DefaultFocus}
-                              whenChildContentsActiveChanged={returnFalse}
-                              bringToFront={returnFalse}
-                              docFilters={this.props.docFilters}
-                              docRangeFilters={this.props.docRangeFilters}
-                              searchFilterDocs={this.props.searchFilterDocs}
-                              ContainingCollectionView={undefined}
-                              ContainingCollectionDoc={undefined}
-                              hideLinkButton={true}
-                        />
-                        <div className="presItem-embeddedMask" />
-                  </div>;
-      }
-      @computed get duration() {
-            let durationInS: number;
-            if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; }
-            else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
-            else durationInS = 2;
-            return "D: " + durationInS + "s";
-      }
-
-      @computed get transition() {
-            let transitionInS: number;
-            if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
-            else transitionInS = 0.5;
-            return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
-      }
-
-      private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
-      private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
-      private _titleRef: React.RefObject<EditableView> = React.createRef();
-
-
-      @action
-      headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
-            const element = e.target as any;
-            e.stopPropagation();
-            e.preventDefault();
-            if (element && !(e.ctrlKey || e.metaKey)) {
-                  if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
-                        PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
-                        setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
-                  } else {
-                        setupMoveUpEvents(this, e, ((e: PointerEvent) => {
-                              PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
-                              return this.startDrag(e);
-                        }), emptyFunction, emptyFunction);
-                  }
-            }
-      }
-
-      headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
-            e.stopPropagation();
-            e.preventDefault();
-      }
-
-      /**
-       * Function to drag and drop the pres element to a diferent location
-       */
-      startDrag = (e: PointerEvent) => {
-            const miniView: boolean = this.toolbarWidth <= 100;
-            const activeItem = this.rootDoc;
-            const dragArray = PresBox.Instance._dragArray;
-            const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
-            if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
-            dragData.dropAction = "move";
-            dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc;
-            dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument;
-            const dragItem: HTMLElement[] = [];
-            if (dragArray.length === 1) {
-                  const doc = dragArray[0];
-                  doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
-                  dragItem.push(doc);
-            } else if (dragArray.length >= 1) {
-                  const doc = document.createElement('div');
-                  doc.className = "presItem-multiDrag";
-                  doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
-                  doc.style.position = 'absolute';
-                  doc.style.top = (e.clientY) + 'px';
-                  doc.style.left = (e.clientX - 50) + 'px';
-                  dragItem.push(doc);
-            }
-
-            // const dropEvent = () => runInAction(() => this._dragging = false);
-            if (activeItem) {
-                  DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
-                  // runInAction(() => this._dragging = true);
-                  return true;
-            }
-            return false;
-      }
-
-      onPointerOver = (e: any) => {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.addEventListener("pointermove", this.onPointerMove);
-      }
-
-      onPointerMove = (e: PointerEvent) => {
-            const slide = this._itemRef.current!;
-            let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
-            for (const doc of DragManager.docsBeingDragged) {
-                  if (!doc.presentationTargetDoc) dragIsPresItem = false;
-            }
-            if (slide && dragIsPresItem) {
-                  const rect = slide.getBoundingClientRect();
-                  const y = e.clientY - rect.top;  //y position within the element.
-                  const height = slide.clientHeight;
-                  const halfLine = height / 2;
-                  if (y <= halfLine) {
-                        slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
-                        slide.style.borderBottom = "0px";
-                  } else if (y > halfLine) {
-                        slide.style.borderTop = "0px";
-                        slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
-                  }
-            }
-            document.removeEventListener("pointermove", this.onPointerMove);
-      }
-
-      onPointerLeave = (e: any) => {
-            this._itemRef.current!.style.borderTop = "0px";
-            this._itemRef.current!.style.borderBottom = "0px";
-            document.removeEventListener("pointermove", this.onPointerMove);
-      }
-
-      @action
-      toggleProperties = () => {
-            if (CurrentUserUtils.propertiesWidth < 5) {
-                  action(() => (CurrentUserUtils.propertiesWidth = 250));
-            }
-      }
-
-      @undoBatch
-      removeItem = action((e: React.MouseEvent) => {
-            this.props.removeDocument?.(this.rootDoc);
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
+    _heightDisposer: IReactionDisposer | undefined;
+
+    @observable _dragging = false;
+    // these fields are conditionally computed fields on the layout document that take this document as a parameter
+    @computed get indexInPres() { return Number(this.lookupField("indexInPres")); }  // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
+    @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox.  We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
+    @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
+    @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
+    @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
+    @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
+
+
+    @observable isShowingVideo = false;
+    @action setIsShowingVideo(shown: boolean) {
+        this.isShowingVideo = shown
+    }
+
+    componentDidMount() {
+        this.layoutDoc.hideLinkButton = true;
+        this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
+            params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+    }
+    componentWillUnmount() {
+        this._heightDisposer?.();
+    }
+
+    /**
+     * Returns a local transformed coordinate array for given coordinates.
+     */
+    ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
+
+    embedHeight = (): number => 97;
+    // embedWidth = () => this.props.PanelWidth();
+    // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
+    embedWidth = (): number => this.props.PanelWidth() - 35;
+    styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+        if (property === StyleProp.Opacity) return 1;
+        return this.props.styleProvider?.(doc, props, property);
+    }
+    /**
+     * The function that is responsible for rendering a preview or not for this
+     * presentation element.
+     */
+    @computed get renderEmbeddedInline() {
+        return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
+            <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
+                <DocumentView
+                    Document={this.targetDoc}
+                    DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+                    styleProvider={this.styleProvider}
+                    layerProvider={this.props.layerProvider}
+                    docViewPath={returnEmptyDoclist}
+                    rootSelected={returnTrue}
+                    addDocument={returnFalse}
+                    removeDocument={returnFalse}
+                    isContentActive={this.props.isContentActive}
+                    addDocTab={returnFalse}
+                    pinToPres={returnFalse}
+                    fitContentsToDoc={returnTrue}
+                    PanelWidth={this.embedWidth}
+                    PanelHeight={this.embedHeight}
+                    ScreenToLocalTransform={Transform.Identity}
+                    moveDocument={this.props.moveDocument!}
+                    renderDepth={this.props.renderDepth + 1}
+                    focus={DocUtils.DefaultFocus}
+                    whenChildContentsActiveChanged={returnFalse}
+                    bringToFront={returnFalse}
+                    docFilters={this.props.docFilters}
+                    docRangeFilters={this.props.docRangeFilters}
+                    searchFilterDocs={this.props.searchFilterDocs}
+                    ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
+                    hideLinkButton={true}
+                />
+                <div className="presItem-embeddedMask" />
+            </div>;
+    }
+    @computed get duration() {
+        let durationInS: number;
+        if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; }
+        else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
+        else durationInS = 2;
+        return "D: " + durationInS + "s";
+    }
+
+    @computed get transition() {
+        let transitionInS: number;
+        if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
+        else transitionInS = 0.5;
+        return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
+    }
+
+    private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
+    private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+    private _titleRef: React.RefObject<EditableView> = React.createRef();
+
+
+    @action
+    headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+        const element = e.target as any;
+        e.stopPropagation();
+        e.preventDefault();
+        if (element && !(e.ctrlKey || e.metaKey)) {
             if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
-                  PresBox.Instance._selectedArray.delete(this.rootDoc);
+                PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+                setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
+            } else {
+                setupMoveUpEvents(this, e, ((e: PointerEvent) => {
+                    PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+                    return this.startDrag(e);
+                }), emptyFunction, emptyFunction);
             }
-            this.hideRecording();
-            e.stopPropagation();
-      });
-
-      // set the value/title of the individual pres element
-      @undoBatch
-      @action
-      onSetValue = (value: string) => {
-            this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
+        }
+    }
+
+    headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    /**
+     * Function to drag and drop the pres element to a diferent location
+     */
+    startDrag = (e: PointerEvent) => {
+        const miniView: boolean = this.toolbarWidth <= 100;
+        const activeItem = this.rootDoc;
+        const dragArray = PresBox.Instance._dragArray;
+        const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
+        if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
+        dragData.dropAction = "move";
+        dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc;
+        dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument;
+        const dragItem: HTMLElement[] = [];
+        if (dragArray.length === 1) {
+            const doc = dragArray[0];
+            doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
+            dragItem.push(doc);
+        } else if (dragArray.length >= 1) {
+            const doc = document.createElement('div');
+            doc.className = "presItem-multiDrag";
+            doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
+            doc.style.position = 'absolute';
+            doc.style.top = (e.clientY) + 'px';
+            doc.style.left = (e.clientX - 50) + 'px';
+            dragItem.push(doc);
+        }
+
+        // const dropEvent = () => runInAction(() => this._dragging = false);
+        if (activeItem) {
+            DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
+            // runInAction(() => this._dragging = true);
             return true;
-      }
-
-      /**
-       * Method called for updating the view of the currently selected document
-       * 
-       * @param targetDoc 
-       * @param activeItem 
-       */
-      @undoBatch
-      @action
-      updateView = (targetDoc: Doc, activeItem: Doc) => {
-            if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
-                  const scroll = targetDoc._scrollTop;
-                  activeItem.presPinViewScroll = scroll;
-            } else if (targetDoc.type === DocumentType.VID) {
-                  activeItem.presPinTimecode = targetDoc._currentTimecode;
-            } else if (targetDoc.type === DocumentType.COMPARISON) {
-                  const clipWidth = targetDoc._clipWidth;
-                  activeItem.presPinClipWidth = clipWidth;
-            } else {
-                  const x = targetDoc._panX;
-                  const y = targetDoc._panY;
-                  const scale = targetDoc._viewScale;
-                  activeItem.presPinViewX = x;
-                  activeItem.presPinViewY = y;
-                  activeItem.presPinViewScale = scale;
+        }
+        return false;
+    }
+
+    onPointerOver = (e: any) => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.addEventListener("pointermove", this.onPointerMove);
+    }
+
+    onPointerMove = (e: PointerEvent) => {
+        const slide = this._itemRef.current!;
+        let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
+        for (const doc of DragManager.docsBeingDragged) {
+            if (!doc.presentationTargetDoc) dragIsPresItem = false;
+        }
+        if (slide && dragIsPresItem) {
+            const rect = slide.getBoundingClientRect();
+            const y = e.clientY - rect.top;  //y position within the element.
+            const height = slide.clientHeight;
+            const halfLine = height / 2;
+            if (y <= halfLine) {
+                slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
+                slide.style.borderBottom = "0px";
+            } else if (y > halfLine) {
+                slide.style.borderTop = "0px";
+                slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
             }
-      }
-
-      @computed get recordingIsInOverlay() {
-            let isInOverlay = false
-            DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
-                  if (doc.slides === this.rootDoc) {
-                        isInOverlay = true
-                        return
-                  }
-            })
-            return isInOverlay
-      }
-
-      @undoBatch
-      @action
-      hideRecording = () => {
-            console.log("hide recording")
-            DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
-                  if (doc.slides === this.rootDoc) {
-                        Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
-                  }
-            })
-      }
-
-      @undoBatch
-      @action
-      startRecording = (targetDoc: Doc, activeItem: Doc) => {
-            // Remove every recording that already exists in overlay view
-            DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
-                  if (doc.slides !== null) {
-                        Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
-                  }
-            })
-
-            if (activeItem.recording) {
-                  // if we already have an existing recording
-                  console.log("recording exists")
-                  console.log(activeItem.recording)
-                  console.log(Cast(activeItem.recording, Doc, null))
-                  Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
-
-            } else {
-                  console.log("creating new recording")
-                  // if we dont have any recording
-                  const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
-
-                  // attach the recording to the slide, and attach the slide to the recording
-                  recording.slides = activeItem
-                  activeItem.recording = recording
-
-                  // TODO: Figure out exactly where we want the video to appear
-                  const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-                  recording.x = pt[0];
-                  recording.y = pt[1];
-                  console.log(pt)
-                  Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
+        }
+        document.removeEventListener("pointermove", this.onPointerMove);
+    }
+
+    onPointerLeave = (e: any) => {
+        this._itemRef.current!.style.borderTop = "0px";
+        this._itemRef.current!.style.borderBottom = "0px";
+        document.removeEventListener("pointermove", this.onPointerMove);
+    }
+
+    @action
+    toggleProperties = () => {
+        if (CurrentUserUtils.propertiesWidth < 5) {
+            action(() => (CurrentUserUtils.propertiesWidth = 250));
+        }
+    }
+
+    @undoBatch
+    removeItem = action((e: React.MouseEvent) => {
+        this.props.removeDocument?.(this.rootDoc);
+        if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
+            PresBox.Instance._selectedArray.delete(this.rootDoc);
+        }
+        this.hideRecording();
+        e.stopPropagation();
+    });
+
+    // set the value/title of the individual pres element
+    @undoBatch
+    @action
+    onSetValue = (value: string) => {
+        this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
+        return true;
+    }
+
+    /**
+     * Method called for updating the view of the currently selected document
+     * 
+     * @param targetDoc 
+     * @param activeItem 
+     */
+    @undoBatch
+    @action
+    updateView = (targetDoc: Doc, activeItem: Doc) => {
+        if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
+            const scroll = targetDoc._scrollTop;
+            activeItem.presPinViewScroll = scroll;
+        } else if (targetDoc.type === DocumentType.VID) {
+            activeItem.presPinTimecode = targetDoc._currentTimecode;
+        } else if (targetDoc.type === DocumentType.COMPARISON) {
+            const clipWidth = targetDoc._clipWidth;
+            activeItem.presPinClipWidth = clipWidth;
+        } else {
+            const x = targetDoc._panX;
+            const y = targetDoc._panY;
+            const scale = targetDoc._viewScale;
+            activeItem.presPinViewX = x;
+            activeItem.presPinViewY = y;
+            activeItem.presPinViewScale = scale;
+        }
+    }
+
+    @computed get recordingIsInOverlay() {
+        let isInOverlay = false
+        DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+            if (doc.slides === this.rootDoc) {
+                isInOverlay = true
+                return
+            }
+        })
+        return isInOverlay
+    }
+
+    @undoBatch
+    @action
+    hideRecording = () => {
+        console.log("hide recording")
+        DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+            if (doc.slides === this.rootDoc) {
+                Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
             }
-      }
-
-      @computed
-      get toolbarWidth(): number {
-            const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
-            let width: number = NumCast(this.presBox._width);
-            if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
-            if (width === 0) width = 300;
-            return width;
-      }
-
-      @computed get mainItem() {
-            const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc);
-            const toolbarWidth: number = this.toolbarWidth;
-            const showMore: boolean = this.toolbarWidth >= 300;
-            const miniView: boolean = this.toolbarWidth <= 110;
-            const presBox: Doc = this.presBox; //presBox
-            const presBoxColor: string = StrCast(presBox._backgroundColor);
-            const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
-            const targetDoc: Doc = this.targetDoc;
-            const activeItem: Doc = this.rootDoc;
-            return (
-                  <div className={`presItem-container`}
-                        key={this.props.Document[Id] + this.indexInPres}
-                        ref={this._itemRef}
-                        style={{
-                              backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
-                              opacity: this._dragging ? 0.3 : 1
-                        }}
-                        onClick={e => {
-                              e.stopPropagation();
-                              e.preventDefault();
-                              PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
-                        }}
-                        onDoubleClick={action(e => {
-                              this.toggleProperties();
-                              PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
-                        })}
-                        onPointerOver={this.onPointerOver}
-                        onPointerLeave={this.onPointerLeave}
-                        onPointerDown={this.headerDown}
-                        onPointerUp={this.headerUp}
-                  >
-                        {miniView ?
-                              // when width is LESS than 110 px 
-                              <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
-                                    {`${this.indexInPres + 1}.`}
-                              </div>
-                              :
-                              // when width is MORE than 110 px 
-                              <div className="presItem-number">
-                                    {`${this.indexInPres + 1}.`}
-                              </div>}
-                        {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
-                              style={{
-                                    backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
-                                    boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
-                              }}>
-                              <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
-                                    <EditableView
-                                          ref={this._titleRef}
-                                          editing={!isSelected ? false : undefined}
-                                          contents={activeItem.title}
-                                          overflow={'ellipsis'}
-                                          GetValue={() => StrCast(activeItem.title)}
-                                          SetValue={this.onSetValue}
-                                    />
-                              </div>
-                              <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
-                              <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
-                              <div className={"presItem-slideButtons"}>
-                                    <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
-                                          <div className="slideButton"
-                                                onClick={() => this.updateView(targetDoc, activeItem)}
-                                                style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
-                                    </Tooltip>
-
-                                    {this.recordingIsInOverlay ?
-                                          <Tooltip title={<><div className="dash-tooltip">{"Hide Recording"}</div></>}>
-                                                <div className="slideButton"
-                                                      onClick={this.hideRecording}
-                                                      style={{ fontWeight: 700 }}>
-                                                      <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} />
-                                                </div>
-                                          </Tooltip> :
-                                          <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
-                                                <div className="slideButton"
-                                                      onClick={() => this.startRecording(targetDoc, activeItem)}
-                                                      style={{ fontWeight: 700 }}>
-                                                      <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} />
-                                                </div>
-                                          </Tooltip>
-                                    }
-
-
-                                    {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
-                                          <div className="slideButton"
-                                                onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
-                                                style={{
-                                                      zIndex: 1000 - this.indexInPres,
-                                                      fontWeight: 700,
-                                                      backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
-                                                      height: activeItem.groupWithUp ? 53 : 18,
-                                                      transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
-                                                }}>
-                                                <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
-                                                      <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
-                                                </div>
-                                          </div>
-                                    </Tooltip>}
-                                    {/* <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
+        })
+    }
+
+    @undoBatch
+    @action
+    startRecording = (targetDoc: Doc, activeItem: Doc) => {
+        // Remove every recording that already exists in overlay view
+        DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+            if (doc.slides !== null) {
+                Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
+            }
+        })
+
+        if (activeItem.recording) {
+            // if we already have an existing recording
+            console.log("recording exists")
+            console.log(activeItem.recording)
+            console.log(Cast(activeItem.recording, Doc, null))
+            Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
+
+        } else {
+            console.log("creating new recording")
+            // if we dont have any recording
+            const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", cloneFieldFilter: new List<string>(["system"]) });
+
+            // attach the recording to the slide, and attach the slide to the recording
+            recording.slides = activeItem
+            activeItem.recording = recording
+
+            // TODO: Figure out exactly where we want the video to appear
+            const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+            recording.x = pt[0];
+            recording.y = pt[1];
+            console.log(pt)
+            Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
+        }
+    }
+
+    @computed
+    get toolbarWidth(): number {
+        const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
+        let width: number = NumCast(this.presBox._width);
+        if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
+        if (width === 0) width = 300;
+        return width;
+    }
+
+    @computed get mainItem() {
+        const isSelected: boolean = PresBox.Instance?._selectedArray.has(this.rootDoc);
+        const toolbarWidth: number = this.toolbarWidth;
+        const showMore: boolean = this.toolbarWidth >= 300;
+        const miniView: boolean = this.toolbarWidth <= 110;
+        const presBox: Doc = this.presBox; //presBox
+        const presBoxColor: string = StrCast(presBox._backgroundColor);
+        const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
+        const targetDoc: Doc = this.targetDoc;
+        const activeItem: Doc = this.rootDoc;
+        return (
+            <div className={`presItem-container`}
+                key={this.props.Document[Id] + this.indexInPres}
+                ref={this._itemRef}
+                style={{
+                    backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
+                    opacity: this._dragging ? 0.3 : 1
+                }}
+                onClick={e => {
+                    e.stopPropagation();
+                    e.preventDefault();
+                    PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+                }}
+                onDoubleClick={action(e => {
+                    this.toggleProperties();
+                    PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+                })}
+                onPointerOver={this.onPointerOver}
+                onPointerLeave={this.onPointerLeave}
+                onPointerDown={this.headerDown}
+                onPointerUp={this.headerUp}
+            >
+                {miniView ?
+                    // when width is LESS than 110 px 
+                    <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
+                        {`${this.indexInPres + 1}.`}
+                    </div>
+                    :
+                    // when width is MORE than 110 px 
+                    <div className="presItem-number">
+                        {`${this.indexInPres + 1}.`}
+                    </div>}
+                {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
+                    style={{
+                        backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+                        boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
+                    }}>
+                    <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
+                        <EditableView
+                            ref={this._titleRef}
+                            editing={!isSelected ? false : undefined}
+                            contents={activeItem.title}
+                            overflow={'ellipsis'}
+                            GetValue={() => StrCast(activeItem.title)}
+                            SetValue={this.onSetValue}
+                        />
+                    </div>
+                    <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
+                    <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
+                    <div className={"presItem-slideButtons"}>
+                        <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
+                            <div className="slideButton"
+                                onClick={() => this.updateView(targetDoc, activeItem)}
+                                style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
+                        </Tooltip>
+
+                        {this.recordingIsInOverlay ?
+                            <Tooltip title={<><div className="dash-tooltip">{"Hide Recording"}</div></>}>
+                                <div className="slideButton"
+                                    onClick={this.hideRecording}
+                                    style={{ fontWeight: 700 }}>
+                                    <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} />
+                                </div>
+                            </Tooltip> :
+                            <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
+                                <div className="slideButton"
+                                    onClick={() => this.startRecording(targetDoc, activeItem)}
+                                    style={{ fontWeight: 700 }}>
+                                    <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} />
+                                </div>
+                            </Tooltip>
+                        }
+
+
+                        {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
+                            <div className="slideButton"
+                                onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
+                                style={{
+                                    zIndex: 1000 - this.indexInPres,
+                                    fontWeight: 700,
+                                    backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
+                                    height: activeItem.groupWithUp ? 53 : 18,
+                                    transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
+                                }}>
+                                <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
+                                    <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
+                                </div>
+                            </div>
+                        </Tooltip>}
+                        {/* <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
                                           <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
                                     </div></Tooltip> */}
-                                    <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
-                                          className={"slideButton"}
-                                          onClick={this.removeItem}>
-                                          <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
-                                    </div></Tooltip>
-                              </div>
-                              <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
-                              {this.renderEmbeddedInline}
-                        </div>}
-                  </div >);
-      }
-
-      render() {
-            return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
-      }
+                        <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
+                            className={"slideButton"}
+                            onClick={this.removeItem}>
+                            <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
+                        </div></Tooltip>
+                    </div>
+                    <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
+                    {this.renderEmbeddedInline}
+                </div>}
+            </div >);
+    }
+
+    render() {
+        return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
+    }
 }
\ No newline at end of file
-- 
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/nodes/VideoBox.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


From d0aa8e81cc98a4cb58a8e7a8c431aa51f8ef2361 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Thu, 5 May 2022 05:51:30 -0400
Subject: fix some bugs and start position. ready for demo :)

---
 src/client/util/RecordingApi.ts     | 44 +++++++++++++++++++++++++++----------
 src/client/views/nodes/VideoBox.tsx | 37 +++++++++++++++++++++++--------
 2 files changed, 60 insertions(+), 21 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index fec579486..7592199eb 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -199,18 +199,19 @@ export class RecordingApi {
         return this.videoBox === null
     }
 
-    private _isPlaying = false;
+    public _isPlaying = false;
 
     public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
         if (presentation.movements === null || this.playFFView === null) {
             return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
         }
-
+        
         if (this._isPlaying) {
             //return new Error('[recordingApi.ts] playMovements() failed: already playing')
             return
         }
         this._isPlaying = true;
+        console.log('setWatching lol')
         Doc.UserDoc().presentationMode = 'watching';
 
         // TODO: consider this bug at the end of the clip on seek
@@ -219,24 +220,43 @@ export class RecordingApi {
 
         const document = this.playFFView.Document
         const { movements } = presentation
-        this.timers = movements.reduce((arr: NodeJS.Timeout[], movement) => {
-            const { panX, panY, time } = movement
 
+        const filteredMovements = movements.filter(movement => movement.time > timeViewed * 1000)
+        
+        document._panX = filteredMovements[0].panX;
+        document._panY = filteredMovements[0].panY;
+        
+        this.timers = filteredMovements.map(movement => {
+            const { panX, panY, time } = movement
             const timeDiff = time - timeViewed*1000
-            if (timeDiff < 0) return arr;
-
-            // set the pan to what was stored
-            arr.push(setTimeout(() => {
+            return setTimeout(() => {
                 document._panX = panX;
                 document._panY = panY;
                 if (movement === movements[movements.length - 1]) {
-                    this._isPlaying = false;
-                    Doc.UserDoc().presentationMode = 'none';
+                    // stuff for recording API
+                    RecordingApi.Instance._isPlaying = false;
                 }
-            }, timeDiff))
-            return arr;
+            }, timeDiff)
         }, [])
 
+        // this.timers = movements.reduce((arr: NodeJS.Timeout[], movement) => {
+        //     const { panX, panY, time } = movement
+
+        //     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;
+        //         if (movement === movements[movements.length - 1]) {
+        //             // stuff for recording API
+        //             RecordingApi.Instance._isPlaying = false;
+        //         }
+        //     }, timeDiff))
+        //     return arr;
+        // }, [])
+
     }
 
     // Unfinished code for tracing multiple free form views
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8aecc2483..f449ae429 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -154,10 +154,10 @@ 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 (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) {
@@ -198,11 +198,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
         this.player && (this.player.currentTime = time);
         this._audioPlayer && (this._audioPlayer.currentTime = time);
+        // TODO: revisit this and clean it
+        if ((this.player?.currentTime || -1) < this.rawDuration) {
+            this._finished = false;
+        }
     }
 
     // pauses video
     @action public Pause = (update: boolean = true) => {
         if (this.presentation) {
+            console.log('VideoBox : Pause');
             const err = RecordingApi.Instance.pauseMovements();
             err && console.log(err);
         }
@@ -347,6 +352,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // sets video element ref
     @action
     setVideoRef = (vref: HTMLVideoElement | null) => {
+        console.log("SEt video ref " +  vref)
         this._videoRef = vref;
         if (vref) {
             this._videoRef!.ontimeupdate = this.updateTimecode;
@@ -354,7 +360,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
             this._disposers.reactionDisposer?.();
             this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
-                time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
+                time => {
+                    !this._playing && (vref.currentTime = time);
+                    console.log("vref time = " + vref.currentTime)
+                }, { fireImmediately: true });
         }
     }
 
@@ -410,9 +419,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
                         onCanPlay={this.videoLoad}
                         controls={VideoBox._nativeControls}
-                        onPlay={() => this.Play()}
+                        onPlay={() => {
+                            // console.log("PLAY from CONTENT")
+                            //this.Play()
+                        }}
                         onSeeked={this.updateTimecode}
-                        onPause={() => this.Pause()}
+                        // onPause={() => this.Pause() }
                         onClick={e => e.preventDefault()}>
                         <source src={field.url.href} type="video/mp4" />
                         Not supported.
@@ -472,7 +484,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
 
     // for play button
-    onPlayDown = () => this._playing ? this.Pause() : this.Play();
+
+    onPlayDown = () => {
+        console.log("PLAY DOWN");
+        this._playing ? this.Pause() : this.Play();
+    }
 
     // for fullscreen button
     onFullDown = (e: React.PointerEvent) => {
@@ -577,7 +593,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this._playRegionTimer = setTimeout(
                     () => {
                         // need to keep track of if end of clip is reached so on next play, clip restarts
-                        if (fullPlay) this._finished = true;
+                        if (fullPlay) {
+                            Doc.UserDoc().presentationMode = 'none';
+                            this._finished = true;
+                         }
                         // removes from currently playing if playback has reached end of range marker
                         else this.removeCurrentlyPlaying();
                         this.Pause();
-- 
cgit v1.2.3-70-g09d2


From b47ff4a335d15a259a6db436e8f2a1beb3180f6d Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 12 May 2022 01:13:34 -0400
Subject: feat: option to recreate recording

---
 .../views/nodes/RecordingBox/RecordingBox.tsx      |    5 +-
 src/client/views/nodes/VideoBox.tsx                | 1686 ++++++++++----------
 2 files changed, 853 insertions(+), 838 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index a7524df37..807624394 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -24,6 +24,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       componentDidMount() {
+            console.log("set native width and height")
             Doc.SetNativeWidth(this.dataDoc, 1280);
             Doc.SetNativeHeight(this.dataDoc, 720);
       }
@@ -45,11 +46,9 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             // console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
-            // this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc.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.fieldKey + "-recorded"] = true;
             // stringify the presenation and store it
             this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
       }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index f449ae429..58e9f390c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -7,7 +7,7 @@ import * as rp from 'request-promise';
 import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc";
 import { InkTool } from "../../../fields/InkField";
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, ImageField, VideoField } from "../../../fields/URLField";
+import { AudioField, ImageField, RecordingField, VideoField } from "../../../fields/URLField";
 import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DocumentType } from "../../documents/DocumentTypes";
@@ -29,6 +29,8 @@ import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 import { RecordingApi } from "../../util/RecordingApi";
+import { List } from "../../../fields/List";
+import { RecordingBox } from "./RecordingBox";
 const path = require('path');
 
 
@@ -46,857 +48,871 @@ const path = require('path');
 
 @observer
 export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
-    /**
-     * Uploads an image buffer to the server and stores with specified filename.  by default the image
-     * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
-     * @param imageUri the bytes of the image
-     * @param returnedFilename the base filename to store the image on the server
-     * @param nosuffix optionally suppress creating multiple resolution images
-     */
-    public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
-        try {
-            const posting = Utils.prepend("/uploadURI");
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename
-                },
-                json: true,
-            });
-            return returnedUri;
-
-        } catch (e) {
-            console.log("VideoBox :" + e);
-        }
-    }
-
-    static _youtubeIframeCounter: number = 0;
-    static heightPercent = 80; // height of video relative to videoBox when timeline is open
-    private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _youtubePlayer: YT.Player | undefined = undefined;
-    private _videoRef: HTMLVideoElement | null = null; // <video> ref
-    private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
-    private _youtubeIframeId: number = -1;
-    private _youtubeContentCreated = false;
-    private _audioPlayer: HTMLAudioElement | null = null;
-    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
-    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
-    private _playRegionTimer: any = null; // timeout for playback
-    @observable _stackedTimeline: any; // CollectionStackedTimeline ref
-    @observable static _nativeControls: boolean; // default html controls
-    @observable _marqueeing: number[] | undefined; // coords for marquee selection
-    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
-    @observable _screenCapture = false;
-    @observable _clicking = false; // used for transition between showing/hiding timeline
-    @observable _forceCreateYouTubeIFrame = false;
-    @observable _playTimer?: NodeJS.Timeout = undefined;
-    @observable _fullScreen = false;
-    @observable _playing = false;
-    @observable _finished: boolean = false; // has playback reached end of clip
-    @observable _volume: number = 1;
-    @observable _muted: boolean = false;
-
-    @computed get links() { return DocListCast(this.dataDoc.links); }
-    @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
-    // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
-    @observable rawDuration: number = 0;
-
-
-    @computed get youtubeVideoId() {
-        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
-        return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
-    }
-
-
-    // returns the path of the audio file
-    @computed get audiopath() {
-        const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
-        const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
-        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
-    public get player(): HTMLVideoElement | null { return this._videoRef; }
-
-
-    componentDidMount() {
-        this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
-        if (this.youtubeVideoId) {
-            const youtubeaspect = 400 / 315;
-            const nativeWidth = Doc.NativeWidth(this.layoutDoc);
-            const nativeHeight = Doc.NativeHeight(this.layoutDoc);
-            if (!nativeWidth || !nativeHeight) {
-                if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
-                Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
-                this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+      /**
+       * Uploads an image buffer to the server and stores with specified filename.  by default the image
+       * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+       * @param imageUri the bytes of the image
+       * @param returnedFilename the base filename to store the image on the server
+       * @param nosuffix optionally suppress creating multiple resolution images
+       */
+      public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
+            try {
+                  const posting = Utils.prepend("/uploadURI");
+                  const returnedUri = await rp.post(posting, {
+                        body: {
+                              uri: imageUri,
+                              name: returnedFilename,
+                              nosuffix,
+                              replaceRootFilename
+                        },
+                        json: true,
+                  });
+                  return returnedUri;
+
+            } catch (e) {
+                  console.log("VideoBox :" + e);
             }
-        }
-        this.player && this.setPlayheadTime(0);
-    }
-
-    componentWillUnmount() {
-        this.removeCurrentlyPlaying();
-        this.Pause();
-        Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
-    }
-
-
-    // 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);
-            err && console.log(err)
-        }
-
-        this._playing = true;
-        const eleTime = this.player?.currentTime || 0;
-        if (this.timeline) {
-            let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
-
-            if (this._finished) {
-                // restarts video if reached end on previous play
-                this._finished = false;
-                start = this.timeline.trimStart;
+      }
+
+      static _youtubeIframeCounter: number = 0;
+      static heightPercent = 80; // height of video relative to videoBox when timeline is open
+      private _disposers: { [name: string]: IReactionDisposer } = {};
+      private _youtubePlayer: YT.Player | undefined = undefined;
+      private _videoRef: HTMLVideoElement | null = null; // <video> ref
+      private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
+      private _youtubeIframeId: number = -1;
+      private _youtubeContentCreated = false;
+      private _audioPlayer: HTMLAudioElement | null = null;
+      private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
+      private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+      private _playRegionTimer: any = null; // timeout for playback
+      @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+      @observable static _nativeControls: boolean; // default html controls
+      @observable _marqueeing: number[] | undefined; // coords for marquee selection
+      @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+      @observable _screenCapture = false;
+      @observable _clicking = false; // used for transition between showing/hiding timeline
+      @observable _forceCreateYouTubeIFrame = false;
+      @observable _playTimer?: NodeJS.Timeout = undefined;
+      @observable _fullScreen = false;
+      @observable _playing = false;
+      @observable _finished: boolean = false; // has playback reached end of clip
+      @observable _volume: number = 1;
+      @observable _muted: boolean = false;
+
+      @computed get links() { return DocListCast(this.dataDoc.links); }
+      @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
+      // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+      @observable rawDuration: number = 0;
+
+
+      @computed get youtubeVideoId() {
+            const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+            return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+      }
+
+
+      // returns the path of the audio file
+      @computed get audiopath() {
+            const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
+            const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
+            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
+      public get player(): HTMLVideoElement | null { return this._videoRef; }
+
+
+      componentDidMount() {
+            this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
+            if (this.youtubeVideoId) {
+                  const youtubeaspect = 400 / 315;
+                  const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+                  const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+                  if (!nativeWidth || !nativeHeight) {
+                        if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
+                        Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
+                        this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+                  }
             }
+            this.player && this.setPlayheadTime(0);
+      }
+
+      componentWillUnmount() {
+            this.removeCurrentlyPlaying();
+            this.Pause();
+            Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+      }
+
+
+      // 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);
+                  err && console.log(err)
+            }
+
+            this._playing = true;
+            const eleTime = this.player?.currentTime || 0;
+            if (this.timeline) {
+                  let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
+                  if (this._finished) {
+                        // restarts video if reached end on previous play
+                        this._finished = false;
+                        start = this.timeline.trimStart;
+                  }
+
+                  try {
+                        this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
+                        update && this.player && this.playFrom(start, undefined, true);
+                        update && this._audioPlayer?.play();
+                        update && this._youtubePlayer?.playVideo();
+                        this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+                  } catch (e) {
+                        console.log("Video Play Exception:", e);
+                  }
+            }
+            this.updateTimecode();
+      }
 
+      // goes to time
+      @action public Seek(time: number) {
             try {
-                this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
-                update && this.player && this.playFrom(start, undefined, true);
-                update && this._audioPlayer?.play();
-                update && this._youtubePlayer?.playVideo();
-                this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+                  this._youtubePlayer?.seekTo(Math.round(time), true);
             } catch (e) {
-                console.log("Video Play Exception:", e);
+                  console.log("Video Seek Exception:", e);
             }
-        }
-        this.updateTimecode();
-    }
-
-    // goes to time
-    @action public Seek(time: number) {
-        try {
-            this._youtubePlayer?.seekTo(Math.round(time), true);
-        } catch (e) {
-            console.log("Video Seek Exception:", e);
-        }
-        this.player && (this.player.currentTime = time);
-        this._audioPlayer && (this._audioPlayer.currentTime = time);
-        // TODO: revisit this and clean it
-        if ((this.player?.currentTime || -1) < this.rawDuration) {
-            this._finished = false;
-        }
-    }
-
-    // pauses video
-    @action public Pause = (update: boolean = true) => {
-        if (this.presentation) {
-            console.log('VideoBox : Pause');
-            const err = RecordingApi.Instance.pauseMovements();
-            err && console.log(err);
-        }
-
-        this._playing = false;
-        this.removeCurrentlyPlaying();
-        try {
-            update && this.player?.pause();
-            update && this._audioPlayer?.pause();
-            update && this._youtubePlayer?.pauseVideo();
-            this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
-            this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
-        } catch (e) {
-            console.log("Video Pause Exception:", e);
-        }
-        this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
-        this._playTimer = undefined;
-        this.updateTimecode();
-        if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
-    }
-
-    // toggles video full screen
-    @action public FullScreen = () => {
-        if (document.fullscreenElement === this._contentRef) {
-            this._fullScreen = false;
-            this.player && this._contentRef && document.exitFullscreen();
-        }
-        else {
-            this._fullScreen = true;
-            this.player && this._contentRef && this._contentRef.requestFullscreen();
-
-        }
-        try {
-            this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
-        } catch (e) {
-            console.log("Video FullScreen Exception:", e);
-        }
-    }
-
-
-    // creates and links snapshot photo of current video frame
-    @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
-        const width = NumCast(this.layoutDoc._width);
-        const canvas = document.createElement('canvas');
-        canvas.width = 640;
-        canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
-        const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
-        if (ctx) {
-            this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
-        }
-
-        if (!this._videoRef) {
-            const b = Docs.Create.LabelDocument({
-                x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
-                _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
-                _isLinkButton: true
-            });
-            this.props.addDocument?.(b);
-            DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
-            Networking.PostToServer("/youtubeScreenshot", {
-                id: this.youtubeVideoId,
-                timecode: this.layoutDoc._currentTimecode
-            }).then(response => {
-                const resolved = response?.accessPaths?.agnostic?.client;
-                if (resolved) {
-                    this.props.removeDocument?.(b);
-                    this.createRealSummaryLink(resolved);
-                }
-            });
-        } else {
-            //convert to desired file format
-            const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
-            // if you want to preview the captured image,
-            const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, "");
-            const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
-            const filename = basename(encodedFilename);
-            VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
-                returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
-        }
-    }
-
-    updateIcon = () => {
-        const makeIcon = (returnedfilename: string) => {
-            this.dataDoc.icon = new ImageField(returnedfilename);
-            this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
-            this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
-        };
-        this.Snapshot(undefined, undefined, makeIcon);
-    }
-
-    // creates link for snapshot
-    createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
-        const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
-        const width = NumCast(this.layoutDoc._width) || 1;
-        const height = NumCast(this.layoutDoc._height);
-        const imageSummary = Docs.Create.ImageDocument(url, {
-            _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
-            x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
-            _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
-        });
-        Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
-        Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
-        this.props.addDocument?.(imageSummary);
-        const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
-        link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
-        setTimeout(() =>
-            (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
-    }
-
-
-    getAnchor = () => {
-        const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
-        const marquee = AnchorMenu.Instance.GetAnchor?.();
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
-    }
-
-
-    // sets video info on load
-    videoLoad = action(() => {
-        const aspect = this.player!.videoWidth / this.player!.videoHeight;
-        Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
-        Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
-        this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
-        if (Number.isFinite(this.player!.duration)) {
-            this.rawDuration = this.player!.duration;
-        } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
-    });
-
-
-    // updates video time
-    @action
-    updateTimecode = () => {
-        this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
-        try {
-            this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
-        } catch (e) {
-            console.log("Video Timecode Exception:", e);
-        }
-    }
-
-
-    // sets video element ref
-    @action
-    setVideoRef = (vref: HTMLVideoElement | null) => {
-        console.log("SEt video ref " +  vref)
-        this._videoRef = vref;
-        if (vref) {
-            this._videoRef!.ontimeupdate = this.updateTimecode;
-            // @ts-ignore
-            // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
-            this._disposers.reactionDisposer?.();
-            this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
-                time => {
-                    !this._playing && (vref.currentTime = time);
-                    console.log("vref time = " + vref.currentTime)
-                }, { fireImmediately: true });
-        }
-    }
-
-    // set ref for div that wraps video and controls for fullscreen
-    @action
-    setContentRef = (cref: HTMLDivElement | null) => {
-        this._contentRef = cref;
-        if (cref) {
-            cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref));
-        }
-    }
-
-
-    // context menu
-    specificContextMenu = (e: React.MouseEvent): void => {
-        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
-        if (field) {
-            const url = field.url.href;
-            const subitems: ContextMenuProps[] = [];
-            subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
-            subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
-            this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
-                description: "Screen Capture", event: (async () => {
-                    runInAction(() => this._screenCapture = !this._screenCapture);
-                    this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
-                }), icon: "expand-arrows-alt"
+            this.player && (this.player.currentTime = time);
+            this._audioPlayer && (this._audioPlayer.currentTime = time);
+            // TODO: revisit this and clean it
+            if ((this.player?.currentTime || -1) < this.rawDuration) {
+                  this._finished = false;
+            }
+      }
+
+      // pauses video
+      @action public Pause = (update: boolean = true) => {
+            if (this.presentation) {
+                  console.log('VideoBox : Pause');
+                  const err = RecordingApi.Instance.pauseMovements();
+                  err && console.log(err);
+            }
+
+            this._playing = false;
+            this.removeCurrentlyPlaying();
+            try {
+                  update && this.player?.pause();
+                  update && this._audioPlayer?.pause();
+                  update && this._youtubePlayer?.pauseVideo();
+                  this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
+                  this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
+            } catch (e) {
+                  console.log("Video Pause Exception:", e);
+            }
+            this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
+            this._playTimer = undefined;
+            this.updateTimecode();
+            if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
+      }
+
+      // toggles video full screen
+      @action public FullScreen = () => {
+            if (document.fullscreenElement === this._contentRef) {
+                  this._fullScreen = false;
+                  this.player && this._contentRef && document.exitFullscreen();
+            }
+            else {
+                  this._fullScreen = true;
+                  this.player && this._contentRef && this._contentRef.requestFullscreen();
+
+            }
+            try {
+                  this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
+            } catch (e) {
+                  console.log("Video FullScreen Exception:", e);
+            }
+      }
+
+
+      // creates and links snapshot photo of current video frame
+      @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
+            const width = NumCast(this.layoutDoc._width);
+            const canvas = document.createElement('canvas');
+            canvas.width = 640;
+            canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
+            const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+            if (ctx) {
+                  this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+            }
+
+            if (!this._videoRef) {
+                  const b = Docs.Create.LabelDocument({
+                        x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
+                        _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
+                        _isLinkButton: true
+                  });
+                  this.props.addDocument?.(b);
+                  DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
+                  Networking.PostToServer("/youtubeScreenshot", {
+                        id: this.youtubeVideoId,
+                        timecode: this.layoutDoc._currentTimecode
+                  }).then(response => {
+                        const resolved = response?.accessPaths?.agnostic?.client;
+                        if (resolved) {
+                              this.props.removeDocument?.(b);
+                              this.createRealSummaryLink(resolved);
+                        }
+                  });
+            } else {
+                  //convert to desired file format
+                  const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+                  // if you want to preview the captured image,
+                  const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, "");
+                  const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
+                  const filename = basename(encodedFilename);
+                  VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
+                        returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+            }
+      }
+
+      updateIcon = () => {
+            const makeIcon = (returnedfilename: string) => {
+                  this.dataDoc.icon = new ImageField(returnedfilename);
+                  this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
+                  this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
+            };
+            this.Snapshot(undefined, undefined, makeIcon);
+      }
+
+      // creates link for snapshot
+      createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+            const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
+            const width = NumCast(this.layoutDoc._width) || 1;
+            const height = NumCast(this.layoutDoc._height);
+            const imageSummary = Docs.Create.ImageDocument(url, {
+                  _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+                  x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
+                  _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
             });
-            subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
-            subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
-            subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
-            subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
-            ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
-        }
-    }
-
-
-    // ref for updating time
-    setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
-
-    // renders the video and audio
-    @computed get content() {
-        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
-        const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
-        return !field ? <div key="loading">Loading</div> :
-            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
-                <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
-                    {this.uIButtons}
-                    <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
-                        onCanPlay={this.videoLoad}
-                        controls={VideoBox._nativeControls}
-                        onPlay={() => {
-                            // console.log("PLAY from CONTENT")
-                            //this.Play()
-                        }}
-                        onSeeked={this.updateTimecode}
-                        // onPause={() => this.Pause() }
-                        onClick={e => e.preventDefault()}>
-                        <source src={field.url.href} type="video/mp4" />
-                        Not supported.
-                    </video>
-                    {!this.audiopath || this.audiopath === field.url.href ? (null) :
-                        <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
-                            <source src={this.audiopath} type="audio/mpeg" />
-                            Not supported.
-                        </audio>}
-                </div>
-            </div>;
-    }
-
-
-    @action youtubeIframeLoaded = (e: any) => {
-        if (!this._youtubeContentCreated) {
-            this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
-            return;
-        }
-        else this._youtubeContentCreated = false;
-
-        this.loadYouTube(e.target);
-    }
-
-    loadYouTube = (iframe: any) => {
-        let started = true;
-        const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
-            if (started && event.data === YT.PlayerState.PLAYING) {
-                started = false;
-                this._youtubePlayer?.unMute();
-                //this.Pause();
-                return;
+            Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
+            Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
+            this.props.addDocument?.(imageSummary);
+            const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
+            link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
+            setTimeout(() =>
+                  (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
+      }
+
+
+      getAnchor = () => {
+            const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
+            const marquee = AnchorMenu.Instance.GetAnchor?.();
+            return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+      }
+
+
+      // sets video info on load
+      videoLoad = action(() => {
+            const aspect = this.player!.videoWidth / this.player!.videoHeight;
+            Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
+            Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
+            this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+            if (Number.isFinite(this.player!.duration)) {
+                  this.rawDuration = this.player!.duration;
+            } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
+      });
+
+
+      // updates video time
+      @action
+      updateTimecode = () => {
+            this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+            try {
+                  this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
+            } catch (e) {
+                  console.log("Video Timecode Exception:", e);
+            }
+      }
+
+
+      // sets video element ref
+      @action
+      setVideoRef = (vref: HTMLVideoElement | null) => {
+            console.log("SEt video ref " + vref)
+            this._videoRef = vref;
+            if (vref) {
+                  this._videoRef!.ontimeupdate = this.updateTimecode;
+                  // @ts-ignore
+                  // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+                  this._disposers.reactionDisposer?.();
+                  this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
+                        time => {
+                              !this._playing && (vref.currentTime = time);
+                              console.log("vref time = " + vref.currentTime)
+                        }, { fireImmediately: true });
             }
-            if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
-            if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
-        });
-        const onYoutubePlayerReady = (event: any) => {
-            this._disposers.reactionDisposer?.();
-            this._disposers.youtubeReactionDisposer?.();
-            this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
-            this._disposers.youtubeReactionDisposer = reaction(
-                () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
-                (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
-        };
-        if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
-        else {
-            (YT as any)?.ready(() => {
-                this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
-                    events: {
-                        'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
-                        'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
-                    }
-                });
+      }
+
+      // set ref for div that wraps video and controls for fullscreen
+      @action
+      setContentRef = (cref: HTMLDivElement | null) => {
+            this._contentRef = cref;
+            if (cref) {
+                  cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref));
+            }
+      }
+
+
+      // context menu
+      specificContextMenu = (e: React.MouseEvent): void => {
+            const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+            if (field) {
+                  const url = field.url.href;
+                  const subitems: ContextMenuProps[] = [];
+                  subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
+                  subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
+                  this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
+                        description: "Screen Capture", event: (async () => {
+                              runInAction(() => this._screenCapture = !this._screenCapture);
+                              this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+                        }), icon: "expand-arrows-alt"
+                  });
+                  subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
+                  subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
+                  subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
+                  subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
+                  // if the videobox was turned from a recording box
+                  if (this.dataDoc[this.fieldKey + "-recorded"] === true) {
+                        subitems.push({
+                              description: "Recreate recording", event: () => {
+                                    this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+                                    // delete assoicated video data
+                                    this.dataDoc[this.props.fieldKey] = "";
+                                    this.dataDoc[this.fieldKey + "-duration"] = "";
+                                    // delete assoicated presentation data
+                                    this.dataDoc[this.fieldKey + "-presentation"] = "";
+                              }, icon: "expand-arrows-alt"
+                        });
+
+                  }
+                  ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
+            }
+      }
+
+
+      // ref for updating time
+      setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
+
+      // renders the video and audio
+      @computed get content() {
+            const field = Cast(this.dataDoc[this.fieldKey], VideoField);
+            const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
+            const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+            return !field ? <div key="loading">Loading</div> :
+                  <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
+                        <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
+                              {this.uIButtons}
+                              <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
+                                    onCanPlay={this.videoLoad}
+                                    controls={VideoBox._nativeControls}
+                                    onPlay={() => {
+                                          // console.log("PLAY from CONTENT")
+                                          //this.Play()
+                                    }}
+                                    onSeeked={this.updateTimecode}
+                                    // onPause={() => this.Pause() }
+                                    onClick={e => e.preventDefault()}>
+                                    <source src={field.url.href} type="video/mp4" />
+                                    Not supported.
+                              </video>
+                              {!this.audiopath || this.audiopath === field.url.href ? (null) :
+                                    <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+                                          <source src={this.audiopath} type="audio/mpeg" />
+                                          Not supported.
+                                    </audio>}
+                        </div>
+                  </div>;
+      }
+
+
+      @action youtubeIframeLoaded = (e: any) => {
+            if (!this._youtubeContentCreated) {
+                  this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
+                  return;
+            }
+            else this._youtubeContentCreated = false;
+
+            this.loadYouTube(e.target);
+      }
+
+      loadYouTube = (iframe: any) => {
+            let started = true;
+            const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
+                  if (started && event.data === YT.PlayerState.PLAYING) {
+                        started = false;
+                        this._youtubePlayer?.unMute();
+                        //this.Pause();
+                        return;
+                  }
+                  if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+                  if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
             });
-        }
-    }
-
-
-    // for play button
-
-    onPlayDown = () => {
-        console.log("PLAY DOWN");
-        this._playing ? this.Pause() : this.Play();
-    }
-
-    // for fullscreen button
-    onFullDown = (e: React.PointerEvent) => {
-        this.FullScreen();
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    // for snapshot button
-    onSnapshotDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, (e) => {
-            this.Snapshot(e.clientX, e.clientY);
-            return true;
-        }, emptyFunction, () => this.Snapshot());
-    }
-
-    // for show/hide timeline button, transitions between show/hide
-    @action
-    onTimelineHdlDown = (e: React.PointerEvent) => {
-        this._clicking = true;
-        setupMoveUpEvents(this, e,
-            action(encodeURIComponent => {
-                this._clicking = false;
-                if (this.props.isContentActive()) {
-                    // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
-                    // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
-
-                    this.layoutDoc._timelineHeightPercent = 80;
-                }
-                return false;
-            }), emptyFunction,
-            () => {
-                this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
-                setTimeout(action(() => this._clicking = false), 500);
-            }, this.props.isContentActive(), this.props.isContentActive());
-    }
-
-
-    // removes video from currently playing display
-    @action
-    removeCurrentlyPlaying = () => {
-        if (CollectionStackedTimeline.CurrentlyPlaying) {
-            const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
-            index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
-        }
-    }
-
-    // adds video to currently playing display
-    @action
-    addCurrentlyPlaying = () => {
-        if (!CollectionStackedTimeline.CurrentlyPlaying) {
-            CollectionStackedTimeline.CurrentlyPlaying = [];
-        }
-        if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
-            CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
-        }
-    }
-
-
-    @computed get youtubeContent() {
-        this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
-        this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
-        const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
-        const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
-        return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
-            onPointerLeave={this.updateTimecode}
-            onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
-            src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
-    }
-
-
-    // for annotating, adds doc with time info
-    @action.bound
-    addDocWithTimecode(doc: Doc | Doc[]): boolean {
-        const docs = doc instanceof Doc ? [doc] : doc;
-        const curTime = NumCast(this.layoutDoc._currentTimecode);
-        docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
-        return this.addDocument(doc);
-    }
-
-
-    // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
-    @action
-    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
-        clearTimeout(this._playRegionTimer);
-        if (Number.isNaN(this.player?.duration)) {
-            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
-        }
-        else if (this.player) {
-            // trimBounds override requested playback bounds
-            const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
-            const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
-            const playRegionDuration = end - start;
-            // checks if times are within clip range
-            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
-                this.player.currentTime = start;
-                this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
-                this.player.play();
-                this._audioPlayer?.play();
-                this._playing = true;
-                this.addCurrentlyPlaying();
-                this._playRegionTimer = setTimeout(
-                    () => {
-                        // need to keep track of if end of clip is reached so on next play, clip restarts
-                        if (fullPlay) {
-                            Doc.UserDoc().presentationMode = 'none';
-                            this._finished = true;
-                         }
-                        // removes from currently playing if playback has reached end of range marker
-                        else this.removeCurrentlyPlaying();
+            const onYoutubePlayerReady = (event: any) => {
+                  this._disposers.reactionDisposer?.();
+                  this._disposers.youtubeReactionDisposer?.();
+                  this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
+                  this._disposers.youtubeReactionDisposer = reaction(
+                        () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
+                        (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
+            };
+            if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
+            else {
+                  (YT as any)?.ready(() => {
+                        this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
+                              events: {
+                                    'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
+                                    'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
+                              }
+                        });
+                  });
+            }
+      }
+
+
+      // for play button
+
+      onPlayDown = () => {
+            console.log("PLAY DOWN");
+            this._playing ? this.Pause() : this.Play();
+      }
+
+      // for fullscreen button
+      onFullDown = (e: React.PointerEvent) => {
+            this.FullScreen();
+            e.stopPropagation();
+            e.preventDefault();
+      }
+
+      // for snapshot button
+      onSnapshotDown = (e: React.PointerEvent) => {
+            setupMoveUpEvents(this, e, (e) => {
+                  this.Snapshot(e.clientX, e.clientY);
+                  return true;
+            }, emptyFunction, () => this.Snapshot());
+      }
+
+      // for show/hide timeline button, transitions between show/hide
+      @action
+      onTimelineHdlDown = (e: React.PointerEvent) => {
+            this._clicking = true;
+            setupMoveUpEvents(this, e,
+                  action(encodeURIComponent => {
+                        this._clicking = false;
+                        if (this.props.isContentActive()) {
+                              // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
+                              // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
+
+                              this.layoutDoc._timelineHeightPercent = 80;
+                        }
+                        return false;
+                  }), emptyFunction,
+                  () => {
+                        this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
+                        setTimeout(action(() => this._clicking = false), 500);
+                  }, this.props.isContentActive(), this.props.isContentActive());
+      }
+
+
+      // removes video from currently playing display
+      @action
+      removeCurrentlyPlaying = () => {
+            if (CollectionStackedTimeline.CurrentlyPlaying) {
+                  const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+                  index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+            }
+      }
+
+      // adds video to currently playing display
+      @action
+      addCurrentlyPlaying = () => {
+            if (!CollectionStackedTimeline.CurrentlyPlaying) {
+                  CollectionStackedTimeline.CurrentlyPlaying = [];
+            }
+            if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
+                  CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+            }
+      }
+
+
+      @computed get youtubeContent() {
+            this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
+            this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
+            const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+            const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
+            return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+                  onPointerLeave={this.updateTimecode}
+                  onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+                  src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+      }
+
+
+      // for annotating, adds doc with time info
+      @action.bound
+      addDocWithTimecode(doc: Doc | Doc[]): boolean {
+            const docs = doc instanceof Doc ? [doc] : doc;
+            const curTime = NumCast(this.layoutDoc._currentTimecode);
+            docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
+            return this.addDocument(doc);
+      }
+
+
+      // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
+      @action
+      playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
+            clearTimeout(this._playRegionTimer);
+            if (Number.isNaN(this.player?.duration)) {
+                  setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
+            }
+            else if (this.player) {
+                  // trimBounds override requested playback bounds
+                  const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
+                  const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
+                  const playRegionDuration = end - start;
+                  // checks if times are within clip range
+                  if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
+                        this.player.currentTime = start;
+                        this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
+                        this.player.play();
+                        this._audioPlayer?.play();
+                        this._playing = true;
+                        this.addCurrentlyPlaying();
+                        this._playRegionTimer = setTimeout(
+                              () => {
+                                    // need to keep track of if end of clip is reached so on next play, clip restarts
+                                    if (fullPlay) {
+                                          Doc.UserDoc().presentationMode = 'none';
+                                          this._finished = true;
+                                    }
+                                    // removes from currently playing if playback has reached end of range marker
+                                    else this.removeCurrentlyPlaying();
+                                    this.Pause();
+                              }, playRegionDuration * 1000);
+                  } else {
                         this.Pause();
-                    }, playRegionDuration * 1000);
-            } else {
-                this.Pause();
+                  }
             }
-        }
-    }
-
-
-    // ends trim, hides trim controls and displays new clip
-    @undoBatch
-    finishTrim = action(() => {
-        this.Pause();
-        this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
-        this.timeline?.StopTrimming();
-    });
-
-    // displays trim controls to start trimming clip
-    startTrim = (scope: TrimScope) => {
-        this.Pause();
-        this.timeline?.StartTrimming(scope);
-    }
-
-    // for trim button, double click displays full clip, single displays curr trim bounds
-    onClipPointerDown = (e: React.PointerEvent) => {
-        // if timeline isn't shown, show first then trim
-        this.heightPercent >= 100 && this.onTimelineHdlDown(e);
-        this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
-            if (doubleTap) {
-                this.startTrim(TrimScope.All);
-            } else if (this.timeline) {
-                this.Pause();
-                this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+      }
+
+
+      // ends trim, hides trim controls and displays new clip
+      @undoBatch
+      finishTrim = action(() => {
+            this.Pause();
+            this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
+            this.timeline?.StopTrimming();
+      });
+
+      // displays trim controls to start trimming clip
+      startTrim = (scope: TrimScope) => {
+            this.Pause();
+            this.timeline?.StartTrimming(scope);
+      }
+
+      // for trim button, double click displays full clip, single displays curr trim bounds
+      onClipPointerDown = (e: React.PointerEvent) => {
+            // if timeline isn't shown, show first then trim
+            this.heightPercent >= 100 && this.onTimelineHdlDown(e);
+            this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
+                  if (doubleTap) {
+                        this.startTrim(TrimScope.All);
+                  } else if (this.timeline) {
+                        this.Pause();
+                        this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+                  }
+            }));
+      }
+
+
+      // for volume slider sets volume
+      @action
+      setVolume = (volume: number) => {
+            if (this.player) {
+                  this._volume = volume;
+                  this.player.volume = volume;
+                  if (this._muted) {
+                        this.toggleMute();
+                  }
             }
-        }));
-    }
-
-
-    // for volume slider sets volume
-    @action
-    setVolume = (volume: number) => {
-        if (this.player) {
-            this._volume = volume;
-            this.player.volume = volume;
-            if (this._muted) {
-                this.toggleMute();
+      }
+
+      // toggles video mute
+      @action
+      toggleMute = () => {
+            if (this.player) {
+                  this._muted = !this._muted;
+                  this.player.muted = this._muted;
             }
-        }
-    }
+      }
 
-    // toggles video mute
-    @action
-    toggleMute = () => {
-        if (this.player) {
-            this._muted = !this._muted;
-            this.player.muted = this._muted;
-        }
-    }
-
-
-    // stretches vertically or horizontally depending on video orientation so video fits full screen
-    fullScreenSize() {
-        if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
-            return { height: "100%" };
-        }
-        else {
-            return { width: "100%" };
-        }
-    }
-
-
-    // for zoom slider, sets timeline waveform zoom
-    zoom = (zoom: number) => {
-        this.timeline?.setZoom(zoom);
-    }
-
-
-    // plays link
-    playLink = (doc: Doc) => {
-        const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
-        const endTime = this.timeline?.anchorEnd(doc);
-        if (startTime !== undefined) {
-            if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
-            else this.Seek(startTime);
-        }
-    }
-
-
-    // starts marquee selection
-    marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
-            setupMoveUpEvents(this, e, action(e => {
-                MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                this._marqueeing = [e.clientX, e.clientY];
-                return true;
-            }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
-        }
-    }
-
-    // ends marquee selection
-    @action
-    finishMarquee = () => {
-        this._marqueeing = undefined;
-        this.props.select(true);
-    }
-
-    timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
-
-    timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
-
-    setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
-
-    timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
-
-    playing = () => this._playing;
-
-    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
-    scaling = () => this.props.scaling?.() || 1;
-
-    panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
-    panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
-
-    screenToLocalTransform = () => {
-        const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
-        return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
-    }
-
-    marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
-    marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
-
-    timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
-
-
-    // renders video controls
-    @computed get uIButtons() {
-        const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
-        return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
-            <div className="videobox-button"
-                title={this._playing ? "play" : "pause"}
-                onPointerDown={this.onPlayDown}>
-                <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
-            </div>
-
-            {this.timeline && <div className="timecode-controls">
-                <div className="timecode-current">
-                    {formatTime(curTime)}
-                </div>
-
-                {this._fullScreen || this.heightPercent === 100 ?
-                    <div className="timeline-slider">
-                        <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime}
-                            className="toolbar-slider time-progress"
-                            onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
-                            onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
-                        />
-                    </div>
-                    :
-                    <div>/</div>}
-
-                <div className="timecode-end">
-                    {formatTime(this.timeline.clipDuration)}
-                </div>
-            </div>}
-
-            <div className="videobox-button"
-                title={"full screen"}
-                onPointerDown={this.onFullDown}>
-                <FontAwesomeIcon icon="expand" />
-            </div>
-
-            {!this._fullScreen && <div className="videobox-button"
-                title={"show timeline"}
-                onPointerDown={this.onTimelineHdlDown}>
-                <FontAwesomeIcon icon="eye" />
-            </div>}
-
-            {!this._fullScreen && <div className="videobox-button"
-                title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}
-                onPointerDown={this.onClipPointerDown}>
-                <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} />
-            </div>}
-
-            <div className="videobox-button show-slider"
-                title={this._muted ? "unmute" : "mute"}
-                onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
-                <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
-            </div>
-            <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume}
-                className="toolbar-slider volume"
-                onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
-                onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
-            />
-
-            {!this._fullScreen && this.heightPercent !== 100 &&
-                <>
-                    <div className="videobox-button" title="zoom">
-                        <FontAwesomeIcon icon="search-plus" />
-                    </div>
-                    <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
-                        className="toolbar-slider zoom"
-                        onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
-                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
-                    />
-                </>}
-        </div>;
-    }
-
-    // renders CollectionStackedTimeline
-    @computed get renderTimeline() {
-        return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
-            <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
-                fieldKey={this.annotationKey}
-                dictationKey={this.fieldKey + "-dictation"}
-                mediaPath={this.audiopath}
-                renderDepth={this.props.renderDepth + 1}
-                startTag={"_timecodeToShow" /* videoStart */}
-                endTag={"_timecodeToHide" /* videoEnd */}
-                bringToFront={emptyFunction}
-                CollectionView={undefined}
-                playFrom={this.playFrom}
-                setTime={this.setPlayheadTime}
-                playing={this.playing}
-                isAnyChildContentActive={this.isAnyChildContentActive}
-                whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
-                moveDocument={this.moveDocument}
-                addDocument={this.addDocument}
-                removeDocument={this.removeDocument}
-                ScreenToLocalTransform={this.timelineScreenToLocal}
-                Play={this.Play}
-                Pause={this.Pause}
-                playLink={this.playLink}
-                PanelHeight={this.timelineHeight}
-                rawDuration={this.rawDuration}
-            />
-        </div>;
-    }
-
-    // renders annotation layer
-    @computed get annotationLayer() {
-        return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
-    }
-
-    render() {
-        const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
-        const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
-        return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
-            style={{
-                pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
-                borderRadius,
-                overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
-            }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
-            <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
-                <div style={{
-                    position: "absolute", transition: this.transition,
-                    width: this.panelWidth(),
-                    height: this.panelHeight(),
-                    top: 0,
-                    left: (this.props.PanelWidth() - this.panelWidth()) / 2
-                }}>
-                    <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
-                        renderDepth={this.props.renderDepth + 1}
+
+      // stretches vertically or horizontally depending on video orientation so video fits full screen
+      fullScreenSize() {
+            if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
+                  return { height: "100%" };
+            }
+            else {
+                  return { width: "100%" };
+            }
+      }
+
+
+      // for zoom slider, sets timeline waveform zoom
+      zoom = (zoom: number) => {
+            this.timeline?.setZoom(zoom);
+      }
+
+
+      // plays link
+      playLink = (doc: Doc) => {
+            const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
+            const endTime = this.timeline?.anchorEnd(doc);
+            if (startTime !== undefined) {
+                  if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
+                  else this.Seek(startTime);
+            }
+      }
+
+
+      // starts marquee selection
+      marqueeDown = (e: React.PointerEvent) => {
+            if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+                  setupMoveUpEvents(this, e, action(e => {
+                        MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+                        this._marqueeing = [e.clientX, e.clientY];
+                        return true;
+                  }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+            }
+      }
+
+      // ends marquee selection
+      @action
+      finishMarquee = () => {
+            this._marqueeing = undefined;
+            this.props.select(true);
+      }
+
+      timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+
+      timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+
+      setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
+      timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+
+      playing = () => this._playing;
+
+      contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
+
+      scaling = () => this.props.scaling?.() || 1;
+
+      panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
+      panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+
+      screenToLocalTransform = () => {
+            const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
+            return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
+      }
+
+      marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
+      marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+
+      timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+
+
+      // renders video controls
+      @computed get uIButtons() {
+            const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+            return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
+                  <div className="videobox-button"
+                        title={this._playing ? "play" : "pause"}
+                        onPointerDown={this.onPlayDown}>
+                        <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
+                  </div>
+
+                  {this.timeline && <div className="timecode-controls">
+                        <div className="timecode-current">
+                              {formatTime(curTime)}
+                        </div>
+
+                        {this._fullScreen || this.heightPercent === 100 ?
+                              <div className="timeline-slider">
+                                    <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime}
+                                          className="toolbar-slider time-progress"
+                                          onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                                          onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+                                    />
+                              </div>
+                              :
+                              <div>/</div>}
+
+                        <div className="timecode-end">
+                              {formatTime(this.timeline.clipDuration)}
+                        </div>
+                  </div>}
+
+                  <div className="videobox-button"
+                        title={"full screen"}
+                        onPointerDown={this.onFullDown}>
+                        <FontAwesomeIcon icon="expand" />
+                  </div>
+
+                  {!this._fullScreen && <div className="videobox-button"
+                        title={"show timeline"}
+                        onPointerDown={this.onTimelineHdlDown}>
+                        <FontAwesomeIcon icon="eye" />
+                  </div>}
+
+                  {!this._fullScreen && <div className="videobox-button"
+                        title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}
+                        onPointerDown={this.onClipPointerDown}>
+                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} />
+                  </div>}
+
+                  <div className="videobox-button show-slider"
+                        title={this._muted ? "unmute" : "mute"}
+                        onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
+                        <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
+                  </div>
+                  <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume}
+                        className="toolbar-slider volume"
+                        onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+                  />
+
+                  {!this._fullScreen && this.heightPercent !== 100 &&
+                        <>
+                              <div className="videobox-button" title="zoom">
+                                    <FontAwesomeIcon icon="search-plus" />
+                              </div>
+                              <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
+                                    className="toolbar-slider zoom"
+                                    onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
+                              />
+                        </>}
+            </div>;
+      }
+
+      // renders CollectionStackedTimeline
+      @computed get renderTimeline() {
+            return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+                  <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
                         fieldKey={this.annotationKey}
+                        dictationKey={this.fieldKey + "-dictation"}
+                        mediaPath={this.audiopath}
+                        renderDepth={this.props.renderDepth + 1}
+                        startTag={"_timecodeToShow" /* videoStart */}
+                        endTag={"_timecodeToHide" /* videoEnd */}
+                        bringToFront={emptyFunction}
                         CollectionView={undefined}
-                        isAnnotationOverlay={true}
-                        annotationLayerHostsContent={true}
-                        PanelWidth={this.panelWidth}
-                        PanelHeight={this.panelHeight}
-                        ScreenToLocalTransform={this.screenToLocalTransform}
-                        docFilters={this.timelineDocFilter}
-                        select={emptyFunction}
-                        scaling={returnOne}
-                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                        removeDocument={this.removeDocument}
+                        playFrom={this.playFrom}
+                        setTime={this.setPlayheadTime}
+                        playing={this.playing}
+                        isAnyChildContentActive={this.isAnyChildContentActive}
+                        whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
                         moveDocument={this.moveDocument}
-                        addDocument={this.addDocWithTimecode}>
-                        {this.contentFunc}
-                    </CollectionFreeFormView>
-                </div>
-                {this.annotationLayer}
-                {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
-                    <MarqueeAnnotator
-                        rootDoc={this.rootDoc}
-                        scrollTop={0}
-                        down={this._marqueeing}
-                        scaling={this.marqueeFitScaling}
-                        docView={this.props.docViewPath().slice(-1)[0]}
-                        containerOffset={this.marqueeOffset}
-                        addDocument={this.addDocWithTimecode}
-                        finishMarquee={this.finishMarquee}
-                        savedAnnotations={this._savedAnnotations}
-                        annotationLayer={this._annotationLayer.current}
-                        mainCont={this._mainCont.current}
-                    />}
-                {this.renderTimeline}
-            </div>
-        </div >);
-    }
+                        addDocument={this.addDocument}
+                        removeDocument={this.removeDocument}
+                        ScreenToLocalTransform={this.timelineScreenToLocal}
+                        Play={this.Play}
+                        Pause={this.Pause}
+                        playLink={this.playLink}
+                        PanelHeight={this.timelineHeight}
+                        rawDuration={this.rawDuration}
+                  />
+            </div>;
+      }
+
+      // renders annotation layer
+      @computed get annotationLayer() {
+            return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
+      }
+
+      render() {
+            const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
+            const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
+            return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
+                  style={{
+                        pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
+                        borderRadius,
+                        overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
+                  }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
+                  <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
+                        <div style={{
+                              position: "absolute", transition: this.transition,
+                              width: this.panelWidth(),
+                              height: this.panelHeight(),
+                              top: 0,
+                              left: (this.props.PanelWidth() - this.panelWidth()) / 2
+                        }}>
+                              <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                                    renderDepth={this.props.renderDepth + 1}
+                                    fieldKey={this.annotationKey}
+                                    CollectionView={undefined}
+                                    isAnnotationOverlay={true}
+                                    annotationLayerHostsContent={true}
+                                    PanelWidth={this.panelWidth}
+                                    PanelHeight={this.panelHeight}
+                                    ScreenToLocalTransform={this.screenToLocalTransform}
+                                    docFilters={this.timelineDocFilter}
+                                    select={emptyFunction}
+                                    scaling={returnOne}
+                                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                                    removeDocument={this.removeDocument}
+                                    moveDocument={this.moveDocument}
+                                    addDocument={this.addDocWithTimecode}>
+                                    {this.contentFunc}
+                              </CollectionFreeFormView>
+                        </div>
+                        {this.annotationLayer}
+                        {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+                              <MarqueeAnnotator
+                                    rootDoc={this.rootDoc}
+                                    scrollTop={0}
+                                    down={this._marqueeing}
+                                    scaling={this.marqueeFitScaling}
+                                    docView={this.props.docViewPath().slice(-1)[0]}
+                                    containerOffset={this.marqueeOffset}
+                                    addDocument={this.addDocWithTimecode}
+                                    finishMarquee={this.finishMarquee}
+                                    savedAnnotations={this._savedAnnotations}
+                                    annotationLayer={this._annotationLayer.current}
+                                    mainCont={this._mainCont.current}
+                              />}
+                        {this.renderTimeline}
+                  </div>
+            </div >);
+      }
 }
 
 VideoBox._nativeControls = false;
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 19f0eca0f5f1b0d12ac369f149caf787f88dd9d6 Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 12 May 2022 01:39:26 -0400
Subject: feat: added checkbox for optional screen tracking

---
 .../views/nodes/RecordingBox/RecordingBox.tsx      | 11 ++-
 .../views/nodes/RecordingBox/RecordingView.scss    | 30 +++++---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 83 +++++++---------------
 src/client/views/nodes/VideoBox.tsx                |  5 +-
 4 files changed, 54 insertions(+), 75 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 807624394..e5b71c183 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -38,26 +38,23 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       @action
-      setResult = (info: Upload.FileInformation) => {
-            // console.log("Setting result to " + info)
+      setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
             this.result = info
-            // console.log(this.result.accessPaths.agnostic.client)
             this.dataDoc.type = DocumentType.VID;
-            // console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
             // stringify the presenation and store it
-            this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
+            if (trackScreen) {
+                  this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
+            }
       }
 
       render() {
             return <div className="recordingBox" ref={this._ref}>
                   {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} />}
-                  {/* {!this.result ? <RecordingView setResult={this.setResult} /> :
-                        <p>video box</p>} */}
             </div>;
       }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index c55af5952..9b2f6d070 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -1,5 +1,5 @@
 video {
-    // flex: 100%;
+      // flex: 100%;
       width: 100%;
       // min-height: 400px;
       //height: auto;
@@ -22,12 +22,12 @@ button {
 }
 
 .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;
 }
@@ -176,7 +176,7 @@ button {
 
 }
 
-.video-edit-wrapper {
+.options-wrapper {
       height: 100%;
       display: flex;
       flex-direction: row;
@@ -184,10 +184,24 @@ button {
       position: absolute;
       top: 0;
       bottom: 0;
-      right: 50% - 15;
 
-      .video-edit-buttons {
-            margin: 0 5px;
+      &.video-edit-wrapper {
+
+            right: 50% - 15;
+
+            .track-screen {
+                  font-weight: 200;
+            }
+
       }
 
+      &.track-screen-wrapper {
+
+            right: 50% - 30;
+
+            .track-screen {
+                  font-weight: 200;
+            }
+
+      }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index efacf7735..e9450a5dd 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -10,20 +10,13 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 
 import { RecordingApi } from '../../../util/RecordingApi';
 
-
-enum RecordingStatus {
-      Recording,
-      Stopped,
-      Paused
-}
-
 interface MediaSegment {
       videoChunks: any[],
       endTime: number
 }
 
 interface IRecordingViewProps {
-      setResult: (info: Upload.FileInformation) => void
+      setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
       setDuration: (seconds: number) => void
 }
 
@@ -36,27 +29,21 @@ export function RecordingView(props: IRecordingViewProps) {
       const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
       const [playing, setPlaying] = useState(false);
       const [progress, setProgress] = useState(0);
-      const [speed, setSpeed] = useState(1);
-      const [muted, setMuted] = useState(false);
 
       const [videos, setVideos] = useState<MediaSegment[]>([]);
-      const [currentVid, setCurrentVid] = useState<number>(0);
       const videoRecorder = useRef<MediaRecorder | null>(null);
-      const audioRecorder = useRef<MediaRecorder | null>(null);
       const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
-    const [finished, setFinished] = useState<Boolean>(false)
+      const [finished, setFinished] = useState<boolean>(false)
+      const [trackScreen, setTrackScreen] = useState<boolean>(true)
 
 
 
       const DEFAULT_MEDIA_CONSTRAINTS = {
-            // video: true,
-            // audio: true
             video: {
                   width: 1280,
                   height: 720,
             },
-            // audio: true,
             audio: {
                   echoCancellation: true,
                   noiseSuppression: true,
@@ -80,7 +67,7 @@ export function RecordingView(props: IRecordingViewProps) {
                         .then((data) => {
                               const result = data[0].result
                               if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                                    props.setResult(result)
+                                    props.setResult(result, trackScreen)
                               } else {
                                     alert("video conversion failed");
                               }
@@ -138,11 +125,9 @@ export function RecordingView(props: IRecordingViewProps) {
       const record = async () => {
             const stream = await startShowingStream();
             videoRecorder.current = new MediaRecorder(stream)
-            // audioRecorder.current = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
 
             // temporary chunks of video
             let videoChunks: any = []
-            // let audioChunks: any = []
 
             videoRecorder.current.ondataavailable = (event: any) => {
                   if (event.data.size > 0) {
@@ -150,16 +135,9 @@ export function RecordingView(props: IRecordingViewProps) {
                   }
             }
 
-            // audioRecorder.current.ondataavailable = (event: any) => {
-            //       if (event.data.size > 0) {
-            //             audioChunks.push(event.data)
-            //       }
-            // }
-
             videoRecorder.current.onstart = (event: any) => {
-                setRecording(true);
-                // RecordingApi.Instance.clear();
-                RecordingApi.Instance.start();
+                  setRecording(true);
+                  trackScreen && RecordingApi.Instance.start();
             }
 
             videoRecorder.current.onstop = () => {
@@ -172,8 +150,8 @@ export function RecordingView(props: IRecordingViewProps) {
                   // reset the temporary chunks
                   videoChunks = []
                   setRecording(false);
-                setFinished(true);
-                RecordingApi.Instance.pause();
+                  setFinished(true);
+                  trackScreen && RecordingApi.Instance.pause();
             }
 
             // recording paused
@@ -183,14 +161,14 @@ export function RecordingView(props: IRecordingViewProps) {
 
                   // reset the temporary chunks
                   videoChunks = []
-                setRecording(false);
-                RecordingApi.Instance.pause();
+                  setRecording(false);
+                  trackScreen && RecordingApi.Instance.pause();
             }
 
             videoRecorder.current.onresume = async (event: any) => {
                   await startShowingStream();
-                setRecording(true);
-                RecordingApi.Instance.resume();
+                  setRecording(true);
+                  trackScreen && RecordingApi.Instance.resume();
             }
 
             videoRecorder.current.start(200)
@@ -229,18 +207,6 @@ export function RecordingView(props: IRecordingViewProps) {
             setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
       }
 
-      // const handleVideoProgress = (event: any) => {
-      //       const manualChange = Number(event.target.value);
-      //       videoElement!.currentTime = (videoElement!.duration / 100) * manualChange;
-      //       setProgress(manualChange)
-      // };
-
-      // const handleVideoSpeed = (event: any) => {
-      //       const newSpeed = Number(event.target.value);
-      //       videoElement!.playbackRate = speed;
-      //       setSpeed(newSpeed)
-      // };
-
       const handleOnTimeUpdate = () => {
             if (playing) {
                   setVideoProgressHelper(videoElementRef.current!.currentTime)
@@ -256,7 +222,7 @@ export function RecordingView(props: IRecordingViewProps) {
             return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
       }
 
-    return (
+      return (
             <div className="recording-container">
                   <div className="video-wrapper">
                         <video id="video"
@@ -277,9 +243,10 @@ export function RecordingView(props: IRecordingViewProps) {
                                                 <button className="record-button" onClick={startOrResume} />
                                           }
                                     </div>
-                                    {!recording && videos.length > 0 ?
 
-                                          <div className="video-edit-wrapper">
+                                    {!recording && (videos.length > 0 ?
+
+                                          <div className="options-wrapper video-edit-wrapper">
                                                 {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
                                                       <MdBackspace onClick={clearPrevious} />
                                                 </IconContext.Provider> */}
@@ -288,7 +255,13 @@ export function RecordingView(props: IRecordingViewProps) {
                                                 </IconContext.Provider>
                                           </div>
 
-                                          : <></>}
+                                          : <div className="options-wrapper track-screen-wrapper">
+                                                <label className="track-screen">
+                                                      <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
+                                                      <span className="checkmark"></span>
+                                                      Track Screen
+                                                </label>
+                                          </div>)}
 
                               </div>
 
@@ -297,15 +270,7 @@ export function RecordingView(props: IRecordingViewProps) {
                                     marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
                               // playSegment={playSegment}
                               />
-
-                              {/* <button className="mute-btn" onClick={() => setMuted(!muted)}>
-                                          {!muted ? (
-                                                <i className="bx bxs-volume-full"></i>
-                                          ) : (
-                                                <i className="bx bxs-volume-mute"></i>
-                                          )}
-                                    </button>  */}
-                  </div>
+                        </div>
                   </div>
             </div>)
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 58e9f390c..902f19716 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -163,8 +163,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
             // if presentation isn't null, call followmovements on the recording api
             if (this.presentation) {
+                  console.log("presentation isn't null")
                   const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
                   err && console.log(err)
+            } else {
+                  console.log("presentation is null")
             }
 
             this._playing = true;
@@ -354,7 +357,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
       // sets video element ref
       @action
       setVideoRef = (vref: HTMLVideoElement | null) => {
-            console.log("SEt video ref " + vref)
+            console.log("Set video ref " + vref)
             this._videoRef = vref;
             if (vref) {
                   this._videoRef!.ontimeupdate = this.updateTimecode;
-- 
cgit v1.2.3-70-g09d2


From 22d39de00ed9a246c520c4c9b1d049a151465d73 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Wed, 1 Jun 2022 14:01:06 -0400
Subject: now tracks and replays scaling accurately

---
 src/client/util/RecordingApi.ts                    | 83 +++++++++++-----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 10 ++-
 src/client/views/nodes/VideoBox.tsx                |  5 +-
 3 files changed, 49 insertions(+), 49 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index 7d8df0bfc..87f44bec4 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -3,11 +3,14 @@ import { IReactionDisposer, observable, reaction } from "mobx";
 import { NumCast } from "../../fields/Types";
 import { Doc } from "../../fields/Doc";
 import { VideoBox } from "../views/nodes/VideoBox";
+import { scaleDiverging } from "d3-scale";
+import { Transform } from "./Transform";
 
 type Movement = {
     time: number,
     panX: number,
     panY: number,
+    scale: number,
 }
 
 type Presentation = {
@@ -120,7 +123,7 @@ export class RecordingApi {
         this.absoluteStart = new Date().getTime() - this.absoluteStart
     }
 
-    private trackMovements = (panX: number, panY: number): Error | undefined => {
+    private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
         // ensure we are recording
         if (!this.isRecording) {
             return new Error('[recordingApi.ts] trackMovements()')
@@ -133,8 +136,7 @@ export class RecordingApi {
         // get the time
         const time = new Date().getTime() - this.absoluteStart
         // make new movement object
-        console.log(time)
-        const movement: Movement = { time, panX, panY }
+        const movement: Movement = { time, panX, panY, scale }
         
         // add that movement to the current presentation data's movement array
         this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
@@ -147,13 +149,16 @@ export class RecordingApi {
     // set the FFView that will be used in a reaction to track the movements
     public setRecordingFFView = (view: CollectionFreeFormView): void => {
         // set the view to the current view
-        if (view === this.recordingFFView || view === null) return;
+        if (view === this.recordingFFView || view == null) return;
         
         // this.recordingFFView = view;
         // set the reaction to track the movements
         this.disposeFunc = reaction(
-            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1) }),
-            (res) => (res.x !== -1 && res.y !== -1) && this.trackMovements(res.x, res.y)
+            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
+            (res) => {
+                console.log('scale', res.scale);
+                (res.x !== -1 && res.y !== -1 && this.isRecording) && this.trackMovements(res.x, res.y, res.scale);
+            }
         )
 
         // for now, set the most recent recordingFFView to the playFFView
@@ -175,6 +180,8 @@ export class RecordingApi {
         this.playFFView = view
     }
 
+    // pausing movements will dispose all timers that are planned to replay the movements
+    // play movemvents will recreate them when the user resumes the presentation
     public pauseMovements = (): undefined | Error => {
         if (this.playFFView === null) {
             return new Error('[recordingApi.ts] pauseMovements() failed: no view')
@@ -196,7 +203,9 @@ export class RecordingApi {
     // by calling pause on the VideoBox, the pauseMovements will be called
     public pauseVideoAndMovements = (): boolean => {
         this.videoBox?.Pause()
-        return this.videoBox === null
+
+        this.pauseMovements()
+        return this.videoBox == null
     }
 
     public _isPlaying = false;
@@ -215,48 +224,36 @@ export class RecordingApi {
         Doc.UserDoc().presentationMode = 'watching';
 
         // TODO: consider this bug at the end of the clip on seek
-        // console.log(timeViewed)
         this.videoBox = videoBox || null;
 
-        const document = this.playFFView.Document
-        const { movements } = presentation
+        // only get the movements that are remaining in the video time left
+        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
 
-        const filteredMovements = movements.filter(movement => movement.time > timeViewed * 1000)
-        
-        document._panX = filteredMovements[0].panX;
-        document._panY = filteredMovements[0].panY;
-        
+        // helper to replay a movement
+        const document = this.playFFView
+        let preScale = -1;
+        const zoomAndPan = (movement: Movement) => {
+            const { panX, panY, scale } = movement;
+            (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+            document.Document._panX = panX;
+            document.Document._panY = panY;
+
+            preScale = scale;
+        }
+
+        // set the first frame to be at the start of the pres
+        zoomAndPan(filteredMovements[0]);
+
+        // make timers that will execute each movement at the correct replay time
         this.timers = filteredMovements.map(movement => {
-            const { panX, panY, time } = movement
-            const timeDiff = time - timeViewed*1000
+            const timeDiff = movement.time - timeViewed*1000
             return setTimeout(() => {
-                document._panX = panX;
-                document._panY = panY;
-                if (movement === movements[movements.length - 1]) {
-                    // stuff for recording API
-                    RecordingApi.Instance._isPlaying = false;
-                }
+                // 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)
-        }, [])
-
-        // this.timers = movements.reduce((arr: NodeJS.Timeout[], movement) => {
-        //     const { panX, panY, time } = movement
-
-        //     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;
-        //         if (movement === movements[movements.length - 1]) {
-        //             // stuff for recording API
-        //             RecordingApi.Instance._isPlaying = false;
-        //         }
-        //     }, 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 aa2e0c417..19ae99743 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -942,6 +942,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale;
         }
 
+        // console.log('zoom scaling', this.zoomScaling())
+        console.log('delta scaling', deltaScale)
+
+        // console.log('transform abt zoomScaling', this.getLocalTransform().inverse().scaleAbout(this.zoomScaling(), x, y));
+
+
         const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
         if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) {
             const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20);
@@ -966,9 +972,9 @@ 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);
+        (Doc.UserDoc()?.presentationMode === 'recording') && RecordingApi.Instance.setRecordingFFView(this);
         // TODO: make this based off the specific recording FFView
-        Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this);
+        (Doc.UserDoc()?.presentationMode === 'none') && 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 902f19716..baa0ae709 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -365,10 +365,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                   // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
                   this._disposers.reactionDisposer?.();
                   this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
-                        time => {
-                              !this._playing && (vref.currentTime = time);
-                              console.log("vref time = " + vref.currentTime)
-                        }, { fireImmediately: true });
+                        time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
             }
       }
 
-- 
cgit v1.2.3-70-g09d2

