From 8e0b274fa0fe0a1e8cfa0d3f9d147c3e6a639d38 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Thu, 2 Jun 2022 11:27:17 -0400
Subject: ffmpeg code added, but there is an error with sharedArrayBuffer

---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 82 ++++++++++++++++------
 1 file changed, 59 insertions(+), 23 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 87716e9cc..5cfb03414 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -7,12 +7,13 @@ import { FaCheckCircle } from 'react-icons/fa';
 import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
+import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
 
 import { RecordingApi } from '../../../util/RecordingApi';
 
 interface MediaSegment {
-      videoChunks: any[],
-      endTime: number
+    videoChunks: any[],
+    endTime: number
 }
 
 interface IRecordingViewProps {
@@ -31,7 +32,8 @@ export function RecordingView(props: IRecordingViewProps) {
       const [playing, setPlaying] = useState(false);
       const [progress, setProgress] = useState(0);
 
-      const [videos, setVideos] = useState<MediaSegment[]>([]);
+    const [videos, setVideos] = useState<MediaSegment[]>([]);
+    // const [order, setOrder] = useState<number[]>([]);
       const videoRecorder = useRef<MediaRecorder | null>(null);
       const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
@@ -52,28 +54,52 @@ export function RecordingView(props: IRecordingViewProps) {
             }
       }
 
-      useEffect(() => {
+    useEffect(() => {
+          
+        console.log('in finish useEffect')
 
-            if (finished) {
+          if (finished) {
+                        // load ffmpe
+              (async () => {
+                  console.log('crossOriginIsolated', crossOriginIsolated)
                   props.setDuration(recordingTimer * 100)
-                  let allVideoChunks: any = []
-                  videos.forEach((vid) => {
-                        console.log(vid.videoChunks)
-                        allVideoChunks = allVideoChunks.concat(vid.videoChunks)
-                  })
-
-                  const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
-
-                  Networking.UploadFilesToServer(videoFile)
-                        .then((data) => {
-                              const result = data[0].result
-                              if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                                    props.setResult(result, trackScreen)
-                              } else {
-                                    alert("video conversion failed");
-                              }
-                        })
 
+                  console.log('Loading ffmpeg-core.js');
+                  const ffmpeg = createFFmpeg({ log: true });
+                  await ffmpeg.load();
+                  console.log('ffmpeg-core.js loaded');
+                  
+                  let allVideoChunks: any = [];
+                  const inputPaths: string[] = [];
+                  // write each segment into it's indexed file
+                  videos.forEach(async (vid, i) => {
+                      const vidName = `segvideo${i}.mkv`
+                      inputPaths.push(vidName)
+                      const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() });
+                      ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile));
+                  })
+                  ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n'));
+                  
+                  console.log('concat')
+                  await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4');
+
+                  const { buffer } = ffmpeg.FS('readFile', 'output.mp4');
+                  const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" });
+
+                  const data = await Networking.UploadFilesToServer(concatVideo)
+                  const result = data[0].result
+                  if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                      props.setResult(result, trackScreen)
+                  } else {
+                      alert("video conversion failed");
+                  }
+                  
+                  // delete all files in MEMFS
+                  inputPaths.forEach(path => ffmpeg.FS('unlink', path));
+                  ffmpeg.FS('unlink', 'order.txt');
+                  ffmpeg.FS('unlink', 'output.mp4');
+              })();
+              
             }
 
 
@@ -84,7 +110,7 @@ export function RecordingView(props: IRecordingViewProps) {
             if (!navigator.mediaDevices) {
                   console.log('This browser does not support getUserMedia.')
             }
-            console.log('This device has the correct media devices.')
+          console.log('This device has the correct media devices.')
       }, [])
 
       useEffect(() => {
@@ -222,6 +248,16 @@ export function RecordingView(props: IRecordingViewProps) {
             const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
             return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
       }
+    
+      const doTranscode = async () => {
+          
+        // console.log('Start transcoding');
+        // ffmpeg.FS('writeFile', 'test.avi', await fetchFile('/flame.avi'));
+        // await ffmpeg.run('-i', 'test.avi', 'test.mp4');
+        // console.log('Complete transcoding');
+        // const data = ffmpeg.FS('readFile', 'test.mp4');
+        // console.log(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })));
+    };
 
       return (
             <div className="recording-container">
-- 
cgit v1.2.3-70-g09d2


From fee544d218768cb02ef3cfc0df722ac563fb78d2 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Thu, 2 Jun 2022 14:19:04 -0400
Subject: switch to fluent ffmpeg

---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 88 +++++++++++++---------
 1 file changed, 53 insertions(+), 35 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 5cfb03414..994111bd4 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -7,7 +7,7 @@ import { FaCheckCircle } from 'react-icons/fa';
 import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
-import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
+const ffmpeg = require("fluent-ffmpeg");
 
 import { RecordingApi } from '../../../util/RecordingApi';
 
@@ -59,45 +59,63 @@ export function RecordingView(props: IRecordingViewProps) {
         console.log('in finish useEffect')
 
           if (finished) {
-                        // load ffmpe
               (async () => {
-                  console.log('crossOriginIsolated', crossOriginIsolated)
-                  props.setDuration(recordingTimer * 100)
+                const inputPaths: string[] = [];
+                  videos.forEach(async (vid, i) => {
+                    const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() });
+                    const { name } = videoFile;
+                    ffmpeg(name)
+                    inputPaths.push(name)
+                  });
 
-                  console.log('Loading ffmpeg-core.js');
-                  const ffmpeg = createFFmpeg({ log: true });
-                  await ffmpeg.load();
-                  console.log('ffmpeg-core.js loaded');
                   
-                  let allVideoChunks: any = [];
-                  const inputPaths: string[] = [];
-                  // write each segment into it's indexed file
-                  videos.forEach(async (vid, i) => {
-                      const vidName = `segvideo${i}.mkv`
-                      inputPaths.push(vidName)
-                      const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() });
-                      ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile));
-                  })
-                  ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n'));
                   
-                  console.log('concat')
-                  await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4');
-
-                  const { buffer } = ffmpeg.FS('readFile', 'output.mp4');
-                  const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" });
-
-                  const data = await Networking.UploadFilesToServer(concatVideo)
-                  const result = data[0].result
-                  if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                      props.setResult(result, trackScreen)
-                  } else {
-                      alert("video conversion failed");
-                  }
+
+                  if (format.includes("x-matroska")) {
+                    await new Promise(res => ffmpeg(file.path)
+                        .videoCodec("copy") // this will copy the data instead of reencode it
+                        .save(file.path.replace(".mkv", ".mp4"))
+                        .on('end', res));
+                    file.path = file.path.replace(".mkv", ".mp4");
+                    format = ".mp4";
+                }
+                //   console.log('crossOriginIsolated', crossOriginIsolated)
+                //   props.setDuration(recordingTimer * 100)
+
+                //   console.log('Loading ffmpeg-core.js');
+                //   const ffmpeg = createFFmpeg({ log: true });
+                //   await ffmpeg.load();
+                //   console.log('ffmpeg-core.js loaded');
+                  
+                //   let allVideoChunks: any = [];
+                //   const inputPaths: string[] = [];
+                //   // write each segment into it's indexed file
+                //   videos.forEach(async (vid, i) => {
+                //       const vidName = `segvideo${i}.mkv`
+                //       inputPaths.push(vidName)
+                //       const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() });
+                //       ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile));
+                // //   })
+                //   ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n'));
+                  
+                //   console.log('concat')
+                //   await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4');
+
+                //   const { buffer } = ffmpeg.FS('readFile', 'output.mp4');
+                //   const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" });
+
+                //   const data = await Networking.UploadFilesToServer(concatVideo)
+                //   const result = data[0].result
+                //   if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                //       props.setResult(result, trackScreen)
+                //   } else {
+                //       alert("video conversion failed");
+                //   }
                   
-                  // delete all files in MEMFS
-                  inputPaths.forEach(path => ffmpeg.FS('unlink', path));
-                  ffmpeg.FS('unlink', 'order.txt');
-                  ffmpeg.FS('unlink', 'output.mp4');
+                //   // delete all files in MEMFS
+                //   inputPaths.forEach(path => ffmpeg.FS('unlink', path));
+                //   ffmpeg.FS('unlink', 'order.txt');
+                //   ffmpeg.FS('unlink', 'output.mp4');
               })();
               
             }
-- 
cgit v1.2.3-70-g09d2


From a08eff4abbcdb1ae78b4b27f0171c4046486219c Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Fri, 3 Jun 2022 12:25:31 -0400
Subject: ffmpeg testing

---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 50 ++++++++++++++++------
 src/server/DashUploadUtils.ts                      | 17 ++++++++
 2 files changed, 55 insertions(+), 12 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 994111bd4..3fd062483 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -7,9 +7,9 @@ import { FaCheckCircle } from 'react-icons/fa';
 import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
-const ffmpeg = require("fluent-ffmpeg");
 
 import { RecordingApi } from '../../../util/RecordingApi';
+import { DashUploadUtils } from '../../../../server/DashUploadUtils';
 
 interface MediaSegment {
     videoChunks: any[],
@@ -61,24 +61,50 @@ export function RecordingView(props: IRecordingViewProps) {
           if (finished) {
               (async () => {
                 const inputPaths: string[] = [];
+                const videoFiles: File[] = []
                   videos.forEach(async (vid, i) => {
                     const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() });
+                    videoFiles.push(videoFile);
+                    
                     const { name } = videoFile;
-                    ffmpeg(name)
                     inputPaths.push(name)
                   });
-
-                  
+                
+                
+                
+                // const inputListName = 'order.txt';
+                // fs.writeFileSync(inputListName, inputPaths.join('\n'));
+
+                // var merge = ffmpeg();
+                // merge.input(inputListName)
+                //   .inputOptions(['-f concat', '-safe 0'])
+                //   .outputOptions('-c copy')
+                //   .save('output.mp4')
+                
+                // fs.unlinkSync(inputListName);
+
+                const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths)
+                console.log('combined', combined)
+                
+                // const outputFile = new File(['output.mp4'], 'output.mp4', { type: 'video/mp4', lastModified: Date.now() });
+
+                const data = await Networking.UploadFilesToServer(combined)
+                  const result = data[0].result
+                  if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                      props.setResult(result, trackScreen)
+                  } else {
+                      alert("video conversion failed");
+                  }
                   
 
-                  if (format.includes("x-matroska")) {
-                    await new Promise(res => ffmpeg(file.path)
-                        .videoCodec("copy") // this will copy the data instead of reencode it
-                        .save(file.path.replace(".mkv", ".mp4"))
-                        .on('end', res));
-                    file.path = file.path.replace(".mkv", ".mp4");
-                    format = ".mp4";
-                }
+                //   if (format.includes("x-matroska")) {
+                //     await new Promise(res => ffmpeg(file.path)
+                //         .videoCodec("copy") // this will copy the data instead of reencode it
+                //         .save(file.path.replace(".mkv", ".mp4"))
+                //         .on('end', res));
+                //     file.path = file.path.replace(".mkv", ".mp4");
+                //     format = ".mp4";
+                // }
                 //   console.log('crossOriginIsolated', crossOriginIsolated)
                 //   props.setDuration(recordingTimer * 100)
 
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 552ab57a5..0c4f87905 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -17,9 +17,11 @@ import { resolvedServerUrl } from "./server_Initialization";
 import { AcceptableMedia, Upload } from './SharedMediaTypes';
 import request = require('request-promise');
 import formidable = require('formidable');
+import { output } from '../../webpack.config';
 const { exec } = require("child_process");
 const parse = require('pdf-parse');
 const ffmpeg = require("fluent-ffmpeg");
+const fs = require("fs");
 const requestImageSize = require("../client/util/request-image-size");
 
 export enum SizeSuffix {
@@ -60,6 +62,21 @@ export namespace DashUploadUtils {
     const type = "content-type";
 
     const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr 
+    
+    export async function combineSegments(filePtr: File[], inputPaths: string[]): Promise<File> {
+        const inputListName = 'order.txt';
+
+        return new Promise<File>((resolve, reject) => {
+            fs.writeFileSync(inputListName, inputPaths.join('\n'));
+            ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save('output.mp4')
+                .on("error", reject)
+                .on("end", () => {
+                    fs.unlinkSync(inputListName);
+                    filePtr[0].path = 'output.mp4';
+                    resolve(filePtr[0]);
+                });
+        });
+    }
 
     export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
         console.log("UPLOAD " + videoId);
-- 
cgit v1.2.3-70-g09d2


From f4a384b194f1267d1a5a22cedf2a06edcd6e1b26 Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Tue, 7 Jun 2022 17:26:53 -0400
Subject: started working with the pointer events to drag - very laggy, may try
 something more simple for now

---
 .../views/nodes/RecordingBox/ProgressBar.scss      | 33 ++++++++-
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 80 ++++++++++++++++++----
 .../views/nodes/RecordingBox/RecordingView.tsx     | 34 ++++-----
 3 files changed, 116 insertions(+), 31 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index a493b0b89..5d27c8a7d 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -1,11 +1,14 @@
 
 .progressbar {
+  touch-action: none;
+
+
       position: absolute;
       display: flex;
       justify-content: flex-start;
       bottom: 10px;
       width: 80%;
-      height: 5px;
+      height: 20px;
       background-color: gray;
 
       &.done {
@@ -23,4 +26,30 @@
             z-index: 3;
             pointer-events: none; 
       }
-}
\ No newline at end of file
+}
+
+.segment {
+  border: 3px solid black;
+  background-color: red;
+  margin: 1px;
+  padding: 0;
+  cursor: pointer;
+  transition-duration: .25s;
+
+  text-align: center;
+}
+
+.segment:first-child {
+  margin-left: 2px;
+}
+.segment:last-child {
+  margin-right: 2px;
+}
+
+.segment:hover {
+  margin: 0px;
+  border: 4px solid red;
+  background-color: black;
+  min-width: 10px;
+  border-radius: 2px;
+}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 82d5e1f04..3e71239ce 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -1,14 +1,26 @@
+import { isInteger } from 'lodash';
 import * as React from 'react';
-import { useEffect } from "react"
+import { useEffect, useState, useCallback, useRef } from "react"
 import "./ProgressBar.scss"
+import { MediaSegment } from './RecordingView';
 
 interface ProgressBarProps {
-      progress: number,
-      marks: number[],
+  videos: MediaSegment[],
 }
 
 export function ProgressBar(props: ProgressBarProps) {
 
+  const progressBarRef = useRef<HTMLDivElement | null>(null)
+
+  // array for the order of video segments
+  const [segments, setSegments] = useState([]);
+
+  // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos])
+
+  const totalTime = () => props.videos.lastElement().endTime
+
+
+
       // const handleClick = (e: React.MouseEvent) => {
       //       let progressbar = document.getElementById('progressbar')!
       //       let bounds = progressbar!.getBoundingClientRect();
@@ -26,20 +38,62 @@ export function ProgressBar(props: ProgressBarProps) {
       //             }
       //       }
       // }
+  
+  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) =>{
+    // e.persist()
+    // console.log('event', e)
+    // don't move the videobox element
+    e.stopPropagation()
+
+    const progressBar = progressBarRef.current
+    if (progressBar == null) return
+    console.log('progressbar', progressBar)
+
+    const updateSegmentOrder = (event: PointerEvent): void => {
+      // event.stopPropagation()
+
+      // const hoveredSegment = event.target as HTMLDivElement & EventTarget
+      // console.log('updateSegmentOrder called', hoveredSegment.id === clickedSegment.id)
+      // if (hoveredSegment.id === clickedSegment.id) return;
+
+      console.log('updateSegmentOrder : target', event.target.id)
+     }
+    
+    // progressBar.addEventListener('pointermove', updateSegmentOrder)
+
+
+    const clickedSegment = e.target as HTMLDivElement & EventTarget
+    console.log('segment', clickedSegment.getBoundingClientRect())
+
+    const rect = clickedSegment.getBoundingClientRect()
+
+    const dragSegment = (event: PointerEvent): void => {
+      // event.stopPropagation()
+      clickedSegment.style.position = 'absolute';
+      clickedSegment.style.zIndex = '999';
+      clickedSegment.style.width = `${rect.width}px`;
+      clickedSegment.style.height = `${rect.height}px`;
+      clickedSegment.style.left = `${event.offsetX - rect.width/2}px`;
+      clickedSegment.style.top = `${event.offsetY - rect.height/2}px`;
+     }
+
+    progressBar.setPointerCapture(e.pointerId)
+    progressBar.addEventListener('pointermove', dragSegment)
+    progressBar.addEventListener('pointerup', () => {
+      progressBar.removeEventListener('pointermove', dragSegment), { once: true }
+      clickedSegment.style.position = clickedSegment.style.zIndex = 'inherit';
+      // progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true }
+    })
+  }
 
       return (
-            <div className="progressbar" id="progressbar">
-                  <div
+        <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
+                  {/* <div
                         className="progressbar done"
                         style={{ width: `${props.progress}%` }}
                   // onClick={handleClick}
-                  ></div>
-                  {props.marks.map((mark) => {
-                        return <div
-                              className="progressbar mark"
-                              style={{ width: `${mark}%` }}
-                        ></div>
-                  })}
-            </div>
+                  ></div> */}
+          {props.videos.map((vid, i) => <div id={`segment-${i.toString()}`} className='segment' style={{ width: `${((vid.endTime - vid.startTime) / totalTime()) * 100}%` }}></div>)}
+        </div>
       )
 }
\ 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 3fd062483..2420c126f 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -11,9 +11,10 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 import { RecordingApi } from '../../../util/RecordingApi';
 import { DashUploadUtils } from '../../../../server/DashUploadUtils';
 
-interface MediaSegment {
+export interface MediaSegment {
     videoChunks: any[],
-    endTime: number
+  endTime: number,
+  startTime: number
 }
 
 interface IRecordingViewProps {
@@ -70,6 +71,8 @@ export function RecordingView(props: IRecordingViewProps) {
                     inputPaths.push(name)
                   });
                 
+                console.log(videoFiles)
+                
                 
                 
                 // const inputListName = 'order.txt';
@@ -83,18 +86,18 @@ export function RecordingView(props: IRecordingViewProps) {
                 
                 // fs.unlinkSync(inputListName);
 
-                const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths)
-                console.log('combined', combined)
+                // const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths)
+                // console.log('combined', combined)
                 
                 // const outputFile = new File(['output.mp4'], 'output.mp4', { type: 'video/mp4', lastModified: Date.now() });
 
-                const data = await Networking.UploadFilesToServer(combined)
-                  const result = data[0].result
-                  if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                      props.setResult(result, trackScreen)
-                  } else {
-                      alert("video conversion failed");
-                  }
+                // const data = await Networking.UploadFilesToServer(combined)
+                //   const result = data[0].result
+                //   if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                //       props.setResult(result, trackScreen)
+                //   } else {
+                //       alert("video conversion failed");
+                //   }
                   
 
                 //   if (format.includes("x-matroska")) {
@@ -215,7 +218,7 @@ export function RecordingView(props: IRecordingViewProps) {
                   // if we have a last portion
                   if (videoChunks.length > 1) {
                         // append the current portion to the video pieces
-                        setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+                        setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
                   }
 
                   // reset the temporary chunks
@@ -228,7 +231,7 @@ export function RecordingView(props: IRecordingViewProps) {
             // recording paused
             videoRecorder.current.onpause = (event: any) => {
                   // append the current portion to the video pieces
-                  setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+                  setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
 
                   // reset the temporary chunks
                   videoChunks = []
@@ -346,9 +349,8 @@ export function RecordingView(props: IRecordingViewProps) {
 
                               </div>
 
-                              <ProgressBar
-                                    progress={progress}
-                                    marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
+              <ProgressBar
+                videos={videos}
                               // playSegment={playSegment}
                               />
                         </div>
-- 
cgit v1.2.3-70-g09d2


From ac76c4836c61cc6564367f35e14014bb9489257b Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Tue, 7 Jun 2022 19:22:37 -0400
Subject: got basic ui for segmentation to work - W overall

---
 .../views/nodes/RecordingBox/ProgressBar.scss      |   2 +-
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 113 ++++++++++++++++-----
 .../views/nodes/RecordingBox/RecordingView.tsx     |   1 +
 3 files changed, 91 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index 5d27c8a7d..67f96033a 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -8,7 +8,7 @@
       justify-content: flex-start;
       bottom: 10px;
       width: 80%;
-      height: 20px;
+      height: 30px;
       background-color: gray;
 
       &.done {
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 3e71239ce..b17d27d2e 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -1,3 +1,5 @@
+import { resolveTxt } from 'dns';
+import { videointelligence } from 'googleapis/build/src/apis/videointelligence';
 import { isInteger } from 'lodash';
 import * as React from 'react';
 import { useEffect, useState, useCallback, useRef } from "react"
@@ -6,6 +8,7 @@ import { MediaSegment } from './RecordingView';
 
 interface ProgressBarProps {
   videos: MediaSegment[],
+  setVideos,
 }
 
 export function ProgressBar(props: ProgressBarProps) {
@@ -13,12 +16,30 @@ export function ProgressBar(props: ProgressBarProps) {
   const progressBarRef = useRef<HTMLDivElement | null>(null)
 
   // array for the order of video segments
-  const [segments, setSegments] = useState([]);
+  const [segments, setSegments] = useState<JSX.Element[]>([]);
+  const [ordered, setOrdered] = useState<Object[]>([]);
+
+  const [clicked, setClicked] = useState<number>(-1);
 
   // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos])
 
   const totalTime = () => props.videos.lastElement().endTime
 
+  useEffect(() => {
+    const segmentsJSX = ordered.map((vid, i) =>
+      <div id={`segment-${i.toString()}`} className='segment' style={{ width: `${((vid.endTime - vid.startTime) / totalTime()) * 100}%`, backgroundColor: clicked === i ? 'inherit' : 'red', }}>{vid.order}</div>);
+      
+    setSegments(segmentsJSX)
+  }, [clicked, ordered])
+
+  useEffect(() => {
+    setOrdered(props.videos.map((vid, order) => {
+    //const { endTime, startTime } = vid
+      // TODO: not tranfer the blobs around
+      return { ...vid, order };
+    }))
+  }, [props.videos]);
+
 
 
       // const handleClick = (e: React.MouseEvent) => {
@@ -39,6 +60,19 @@ export function ProgressBar(props: ProgressBarProps) {
       //       }
       // }
   
+  const updateLastHover = (index) => {
+        // id for segment is like 'segment-1' or 'segment-10'
+    const segId = index
+    console.log(segId)
+    const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
+    console.log(rect)
+    return {
+      index: segId,
+      minX: rect.x,
+      maxX: rect.x + rect.width,
+    }
+  }
+  
   const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) =>{
     // e.persist()
     // console.log('event', e)
@@ -48,41 +82,72 @@ export function ProgressBar(props: ProgressBarProps) {
     const progressBar = progressBarRef.current
     if (progressBar == null) return
     console.log('progressbar', progressBar)
-
-    const updateSegmentOrder = (event: PointerEvent): void => {
-      // event.stopPropagation()
-
-      // const hoveredSegment = event.target as HTMLDivElement & EventTarget
-      // console.log('updateSegmentOrder called', hoveredSegment.id === clickedSegment.id)
-      // if (hoveredSegment.id === clickedSegment.id) return;
-
-      console.log('updateSegmentOrder : target', event.target.id)
-     }
     
     // progressBar.addEventListener('pointermove', updateSegmentOrder)
 
 
     const clickedSegment = e.target as HTMLDivElement & EventTarget
-    console.log('segment', clickedSegment.getBoundingClientRect())
 
     const rect = clickedSegment.getBoundingClientRect()
-
+    setClicked(parseInt(clickedSegment.id.split('-')[1]))
+    let lastHover = {
+      index: parseInt(clickedSegment.id.split('-')[1]),
+      minX: rect.x,
+      maxX: rect.x + rect.width,
+    }
+
+    // clickedSegment.style.backgroundColor = `inherit`;
+    const dot = document.createElement("div")
+    dot.classList.add("segment")
+    dot.style.position = 'absolute';
+    dot.style.zIndex = '999';
+    dot.style.width = `${rect.width}px`;
+    dot.style.height = `${rect.height}px`;
+    document.body.append(dot)
     const dragSegment = (event: PointerEvent): void => {
       // event.stopPropagation()
-      clickedSegment.style.position = 'absolute';
-      clickedSegment.style.zIndex = '999';
-      clickedSegment.style.width = `${rect.width}px`;
-      clickedSegment.style.height = `${rect.height}px`;
-      clickedSegment.style.left = `${event.offsetX - rect.width/2}px`;
-      clickedSegment.style.top = `${event.offsetY - rect.height/2}px`;
+      dot.style.left = `${event.clientX - rect.width/2}px`;
+      dot.style.top = `${event.clientY - rect.height/2}px`;
+     }
+
+    const updateSegmentOrder = (event: PointerEvent): void => {
+      event.stopPropagation();
+
+      dragSegment(event)
+
+      const oldIndex = lastHover.index
+      const swapSegments = (newIndex) => {
+        if (newIndex == null) return
+        setOrdered(prevOrdered => {
+          const cpy = [...prevOrdered]
+          cpy[oldIndex] = cpy[newIndex]
+          cpy[newIndex] = prevOrdered[oldIndex]
+          return cpy
+        })
+        setClicked(newIndex)
+      }
+
+      const curX = event.clientX;
+      if (curX < lastHover.minX && lastHover.index > 0) {
+        swapSegments(lastHover.index - 1)
+        lastHover = updateLastHover(lastHover.index - 1)
+      }
+      else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) {
+        swapSegments(lastHover.index + 1)
+        lastHover = updateLastHover(lastHover.index + 1)
+      }
      }
 
     progressBar.setPointerCapture(e.pointerId)
-    progressBar.addEventListener('pointermove', dragSegment)
-    progressBar.addEventListener('pointerup', () => {
-      progressBar.removeEventListener('pointermove', dragSegment), { once: true }
-      clickedSegment.style.position = clickedSegment.style.zIndex = 'inherit';
+    progressBar.addEventListener('pointermove', updateSegmentOrder)
+    progressBar.addEventListener('pointerup', (event) => {
+      progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true }
+      // clickedSegment.style.position = clickedSegment.style.zIndex = 'inherit';
       // progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true }
+      // clickedSegment.style.backgroundColor = 'red';
+      setClicked(-1);
+      // updateSegmentOrder(event);
+      dot.remove();
     })
   }
 
@@ -93,7 +158,7 @@ export function ProgressBar(props: ProgressBarProps) {
                         style={{ width: `${props.progress}%` }}
                   // onClick={handleClick}
                   ></div> */}
-          {props.videos.map((vid, i) => <div id={`segment-${i.toString()}`} className='segment' style={{ width: `${((vid.endTime - vid.startTime) / totalTime()) * 100}%` }}></div>)}
+          {segments}
         </div>
       )
 }
\ 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 2420c126f..640e4f5f2 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -351,6 +351,7 @@ export function RecordingView(props: IRecordingViewProps) {
 
               <ProgressBar
                 videos={videos}
+                setVideos={setVideos}
                               // playSegment={playSegment}
                               />
                         </div>
-- 
cgit v1.2.3-70-g09d2


From c656abad84ac56c3067a48ca6d34d567e3fcdd88 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Wed, 8 Jun 2022 00:05:34 -0400
Subject: Took stab at ffmpeg in server. did some reworking of recordingview
 css. fix smoothness bug for the moving segment that follow cursor.

---
 src/client/Network.ts                              | 15 +++++++-
 .../views/nodes/RecordingBox/ProgressBar.scss      | 33 +++++++++++++++---
 .../views/nodes/RecordingBox/ProgressBar.tsx       |  9 ++---
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  4 +--
 .../views/nodes/RecordingBox/RecordingView.scss    | 29 ++++++++++------
 .../views/nodes/RecordingBox/RecordingView.tsx     | 23 +++++++++----
 src/server/ApiManagers/UploadManager.ts            | 28 +++++++++++++--
 src/server/DashUploadUtils.ts                      | 40 ++++++++++++++--------
 8 files changed, 136 insertions(+), 45 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index 1255e5ce0..2c6d9d711 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -19,7 +19,6 @@ export namespace Networking {
       }
 
       export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
-            console.log(files)
             const formData = new FormData();
             if (Array.isArray(files)) {
                   if (!files.length) {
@@ -36,6 +35,19 @@ export namespace Networking {
             const response = await fetch("/uploadFormData", parameters);
             return response.json();
       }
+    
+    export async function UploadSegmentsAndConcatenate<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+            console.log(files)
+            const formData = new FormData();
+            if (!Array.isArray(files) || !files.length) return [];
+            files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+            const parameters = {
+                  method: 'POST',
+                  body: formData
+            };
+            const response = await fetch("/uploadVideosandConcatenate", parameters);
+            return response.json();
+      }
 
       export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
             const parameters = {
@@ -46,5 +58,6 @@ export namespace Networking {
             const response = await fetch("/uploadYoutubeVideo", parameters);
             return response.json();
       }
+    
 
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index 67f96033a..d387468c6 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -1,13 +1,17 @@
 
 .progressbar {
   touch-action: none;
+  vertical-align: middle;
+  text-align: center;
+
+  align-items: center;
 
 
       position: absolute;
       display: flex;
       justify-content: flex-start;
-      bottom: 10px;
-      width: 80%;
+      bottom: 2px;
+      width: 99%;
       height: 30px;
       background-color: gray;
 
@@ -34,11 +38,24 @@
   margin: 1px;
   padding: 0;
   cursor: pointer;
-  transition-duration: .25s;
+  transition-duration: .5s;
+  user-select: none;
 
+  vertical-align: middle;
   text-align: center;
 }
 
+.segment-hide {
+    background-color: inherit;
+    text-align: center;
+    vertical-align: middle;
+    user-select: none;
+    // /* Hide the text. */
+    // text-indent: 100%;
+    // white-space: nowrap;
+    // overflow: hidden;
+}
+
 .segment:first-child {
   margin-left: 2px;
 }
@@ -47,9 +64,17 @@
 }
 
 .segment:hover {
+    background-color: white;
+}
+
+.segment:hover, .segment-selected {
   margin: 0px;
   border: 4px solid red;
-  background-color: black;
   min-width: 10px;
   border-radius: 2px;
 }
+
+.segment-selected {
+  border: 4px solid grey;
+  background-color: red;
+}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index b17d27d2e..34a771367 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -27,7 +27,7 @@ export function ProgressBar(props: ProgressBarProps) {
 
   useEffect(() => {
     const segmentsJSX = ordered.map((vid, i) =>
-      <div id={`segment-${i.toString()}`} className='segment' style={{ width: `${((vid.endTime - vid.startTime) / totalTime()) * 100}%`, backgroundColor: clicked === i ? 'inherit' : 'red', }}>{vid.order}</div>);
+        <div id={`segment-${i.toString()}`} className={ clicked === i ? 'segment-hide' : 'segment'} style={{ width: `${((vid.endTime - vid.startTime) / totalTime()) * 100}%` }}>{vid.order}</div>);
       
     setSegments(segmentsJSX)
   }, [clicked, ordered])
@@ -98,7 +98,8 @@ export function ProgressBar(props: ProgressBarProps) {
 
     // clickedSegment.style.backgroundColor = `inherit`;
     const dot = document.createElement("div")
-    dot.classList.add("segment")
+    dot.classList.add("segment-selected")
+    dot.style.transitionDuration = '0s';
     dot.style.position = 'absolute';
     dot.style.zIndex = '999';
     dot.style.width = `${rect.width}px`;
@@ -106,8 +107,8 @@ export function ProgressBar(props: ProgressBarProps) {
     document.body.append(dot)
     const dragSegment = (event: PointerEvent): void => {
       // event.stopPropagation()
-      dot.style.left = `${event.clientX - rect.width/2}px`;
-      dot.style.top = `${event.clientY - rect.height/2}px`;
+        dot.style.left = `${event.clientX - rect.width/2}px`;
+        dot.style.top = `${event.clientY - rect.height/2}px`;
      }
 
     const updateSegmentOrder = (event: PointerEvent): void => {
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 159271223..68f3b3ad4 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -54,9 +54,9 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       render() {
-            // console.log("Proto[Is]: ", this.rootDoc.proto?.[Id])
+            console.log("Proto[Is]: ", this.rootDoc.proto?.[Id])
             return <div className="recordingBox" ref={this._ref}>
-                  {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id]} />}
+                  {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
             </div>;
       }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index 9b2f6d070..0b7b1918f 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -33,7 +33,7 @@ button {
 }
 
 .video-wrapper:hover .controls {
-      bottom: 30px;
+      bottom: 34.5px;
       transform: translateY(0%);
       opacity: 100%;
 }
@@ -43,8 +43,8 @@ button {
       align-items: center;
       justify-content: space-evenly;
       position: absolute;
-      padding: 14px;
-      width: 100%;
+      // padding: 14px;
+      //width: 100%;
       max-width: 500px;
       // max-height: 20%;
       flex-wrap: wrap;
@@ -56,7 +56,14 @@ button {
       // transform: translateY(150%);
       transition: all 0.3s ease-in-out;
       // opacity: 0%;
-      bottom: 30px;
+      bottom: 34.5px;
+      height: 60px;
+      right: 2px;
+      // bottom: -150px;
+}
+
+.controls:active {
+      bottom: 40px;
       // bottom: -150px;
 }
 
@@ -127,9 +134,8 @@ button {
 .controls-inner-container {
       display: flex;
       flex-direction: row;
-      justify-content: center;
-      width: 100%;
-
+      align-content: center;
+      position: relative;
 }
 
 .record-button-wrapper {
@@ -180,14 +186,14 @@ button {
       height: 100%;
       display: flex;
       flex-direction: row;
-      align-items: center;
-      position: absolute;
+      align-content: center;
+      position: relative;
       top: 0;
       bottom: 0;
 
       &.video-edit-wrapper {
 
-            right: 50% - 15;
+            // right: 50% - 15;
 
             .track-screen {
                   font-weight: 200;
@@ -197,10 +203,11 @@ button {
 
       &.track-screen-wrapper {
 
-            right: 50% - 30;
+            // right: 50% - 30;
 
             .track-screen {
                   font-weight: 200;
+                  color: aqua;
             }
 
       }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 640e4f5f2..1e9ce22f1 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -71,13 +71,21 @@ export function RecordingView(props: IRecordingViewProps) {
                     inputPaths.push(name)
                   });
                 
-                console.log(videoFiles)
+                  console.log(videoFiles)
+                  
+                  const data = await Networking.UploadSegmentsAndConcatenate(videoFiles)
+                  console.log('data', data)
+                  const result = data[0].result
+                  if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                      props.setResult(result, trackScreen)
+                  } else {
+                      alert("video conversion failed");
+                  }
                 
                 
                 
                 // const inputListName = 'order.txt';
                 // fs.writeFileSync(inputListName, inputPaths.join('\n'));
-
                 // var merge = ffmpeg();
                 // merge.input(inputListName)
                 //   .inputOptions(['-f concat', '-safe 0'])
@@ -345,16 +353,17 @@ export function RecordingView(props: IRecordingViewProps) {
                                                       <span className="checkmark"></span>
                                                       Track Screen
                                                 </label>
-                                          </div>)}
-
-                              </div>
+                              </div>)}
 
-              <ProgressBar
+                      </div>
+                              
+                  </div>
+                  
+                                         <ProgressBar
                 videos={videos}
                 setVideos={setVideos}
                               // playSegment={playSegment}
                               />
-                        </div>
                   </div>
             </div>)
 }
\ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 634548154..faf36c6e5 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -40,7 +40,31 @@ export function clientPathToFile(directory: Directory, filename: string) {
 
 export default class UploadManager extends ApiManager {
 
-      protected initialize(register: Registration): void {
+    protected initialize(register: Registration): void {
+          
+            register({
+                  method: Method.POST,
+                  subscription: "/uploadVideosAndConcatenate",
+                  secureHandler: async ({ req, res }) => {
+                        const form = new formidable.IncomingForm();
+                        form.keepExtensions = true;
+                        form.uploadDir = pathToDirectory(Directory.parsed_files);
+                        return new Promise<void>(resolve => {
+                              form.parse(req, async (_err, _fields, files) => {
+                                    const result: Upload.FileResponse[] = [];
+                                    for (const key in files) {
+                                        const f = files[key];
+                                        if (Array.isArray(f)) {
+                                            const result = await DashUploadUtils.concatenateVideos(f);
+                                            console.log('concatenated', result);
+                                            result && !(result.result instanceof Error) && _success(res, result);
+                                        }
+                                    }
+                                    resolve();
+                              });
+                        });
+                  }
+            });
 
             register({
                   method: Method.POST,
@@ -50,7 +74,7 @@ export default class UploadManager extends ApiManager {
                         form.keepExtensions = true;
                         form.uploadDir = pathToDirectory(Directory.parsed_files);
                         return new Promise<void>(resolve => {
-                              form.parse(req, async (_err, _fields, files) => {
+                            form.parse(req, async (_err, _fields, files) => {
                                     const results: Upload.FileResponse[] = [];
                                     for (const key in files) {
                                           const f = files[key];
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 0c4f87905..6a7c8543d 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -1,9 +1,9 @@
 import { green, red } from 'colors';
 import { ExifImage } from 'exif';
+import * as exifr from 'exifr';
 import { File } from 'formidable';
 import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
 import * as path from 'path';
-import * as exifr from 'exifr';
 import { basename } from "path";
 import * as sharp from 'sharp';
 import { Stream } from 'stream';
@@ -17,7 +17,6 @@ import { resolvedServerUrl } from "./server_Initialization";
 import { AcceptableMedia, Upload } from './SharedMediaTypes';
 import request = require('request-promise');
 import formidable = require('formidable');
-import { output } from '../../webpack.config';
 const { exec } = require("child_process");
 const parse = require('pdf-parse');
 const ffmpeg = require("fluent-ffmpeg");
@@ -63,19 +62,31 @@ export namespace DashUploadUtils {
 
     const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr 
     
-    export async function combineSegments(filePtr: File[], inputPaths: string[]): Promise<File> {
-        const inputListName = 'order.txt';
-
-        return new Promise<File>((resolve, reject) => {
-            fs.writeFileSync(inputListName, inputPaths.join('\n'));
-            ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save('output.mp4')
+    export async function concatenateVideos(videoFiles: File[]): Promise<Upload.FileResponse> {
+        // make a list of paths to create the ordered text file for ffmpeg
+        const filePaths = videoFiles.map(file => file.path);
+        // write the text file to the file system
+        const inputListName = 'concat.txt';
+        const textFilePath = path.join(filesDirectory, "concat.txt");
+        writeFile(textFilePath, filePaths.join("\n"), (err) => console.log(err));
+
+
+        // make output file name based on timestamp
+        const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
+        await new Promise((resolve, reject) => {
+            ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save(outputFileName)
                 .on("error", reject)
-                .on("end", () => {
-                    fs.unlinkSync(inputListName);
-                    filePtr[0].path = 'output.mp4';
-                    resolve(filePtr[0]);
-                });
-        });
+                .on("end", resolve);
+        })
+
+        // delete concat.txt from the file system
+        unlinkSync(textFilePath);
+
+        // read the output file from the file system
+        const outputFile = fs.readFileSync(outputFileName);
+        console.log('outputFile', outputFile);
+        // move only the output file to the videos directory
+        return MoveParsedFile(outputFile, Directory.videos)
     }
 
     export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
@@ -237,6 +248,7 @@ export namespace DashUploadUtils {
         }
         let resolvedUrl: string;
         /**
+         * 
          * At this point, we want to take whatever url we have and make sure it's requestable.
          * Anything that's hosted by some other website already is, but if the url is a local file url
          * (locates the file on this server machine), we have to resolve the client side url by cutting out the
-- 
cgit v1.2.3-70-g09d2


From d570ab0d8ea08363c651caf8ee67c43c5a5823a3 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Wed, 8 Jun 2022 02:49:39 -0400
Subject: got it to run, but not concatenating - only first video

---
 src/client/Network.ts                              |  4 +-
 .../views/nodes/RecordingBox/ProgressBar.scss      |  1 -
 .../views/nodes/RecordingBox/RecordingView.tsx     |  2 +-
 src/server/ApiManagers/UploadManager.ts            | 20 +++----
 src/server/DashUploadUtils.ts                      | 67 ++++++++++++++++++----
 5 files changed, 69 insertions(+), 25 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index 2c6d9d711..8c1f31488 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -36,8 +36,8 @@ export namespace Networking {
             return response.json();
       }
     
-    export async function UploadSegmentsAndConcatenate<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
-            console.log(files)
+    export async function UploadSegmentsAndConcatenate<T extends Upload.FileInformation = Upload.FileInformation>(files: File[]): Promise<Upload.FileResponse<T>[]> {
+            console.log("network.ts : uploading segments and concatenating", files);
             const formData = new FormData();
             if (!Array.isArray(files) || !files.length) return [];
             files.forEach(file => formData.append(Utils.GenerateGuid(), file));
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index d387468c6..c7a190566 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -70,7 +70,6 @@
 .segment:hover, .segment-selected {
   margin: 0px;
   border: 4px solid red;
-  min-width: 10px;
   border-radius: 2px;
 }
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 1e9ce22f1..aea7f56b5 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -69,7 +69,7 @@ export function RecordingView(props: IRecordingViewProps) {
                     
                     const { name } = videoFile;
                     inputPaths.push(name)
-                  });
+                  })
                 
                   console.log(videoFiles)
                   
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index faf36c6e5..398b007b5 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -44,22 +44,22 @@ export default class UploadManager extends ApiManager {
           
             register({
                   method: Method.POST,
-                  subscription: "/uploadVideosAndConcatenate",
+                  subscription: "/uploadVideosandConcatenate",
                   secureHandler: async ({ req, res }) => {
                         const form = new formidable.IncomingForm();
                         form.keepExtensions = true;
                         form.uploadDir = pathToDirectory(Directory.parsed_files);
                         return new Promise<void>(resolve => {
                               form.parse(req, async (_err, _fields, files) => {
-                                    const result: Upload.FileResponse[] = [];
-                                    for (const key in files) {
-                                        const f = files[key];
-                                        if (Array.isArray(f)) {
-                                            const result = await DashUploadUtils.concatenateVideos(f);
-                                            console.log('concatenated', result);
-                                            result && !(result.result instanceof Error) && _success(res, result);
-                                        }
-                                    }
+                                  const results: Upload.FileResponse[] = [];
+
+                                  // create an array of all the file paths
+                                  const filePaths: string[] = Object.keys(files).map(key => files[key].path);
+                                    console.log("uploading files", Array.isArray(filePaths));
+                                    const result = await DashUploadUtils.concatenateVideos(filePaths);
+                                    console.log('concatenated', result);
+                                    result && !(result.result instanceof Error) && results.push(result);
+                                    _success(res, results)
                                     resolve();
                               });
                         });
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 6a7c8543d..148b0df65 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -17,6 +17,7 @@ import { resolvedServerUrl } from "./server_Initialization";
 import { AcceptableMedia, Upload } from './SharedMediaTypes';
 import request = require('request-promise');
 import formidable = require('formidable');
+import { file } from 'jszip';
 const { exec } = require("child_process");
 const parse = require('pdf-parse');
 const ffmpeg = require("fluent-ffmpeg");
@@ -62,19 +63,31 @@ export namespace DashUploadUtils {
 
     const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr 
     
-    export async function concatenateVideos(videoFiles: File[]): Promise<Upload.FileResponse> {
+    export async function concatenateVideos(filePaths: string[]): Promise<Upload.FileResponse> {
         // make a list of paths to create the ordered text file for ffmpeg
-        const filePaths = videoFiles.map(file => file.path);
+        //const filePaths = videoFiles.map(file => file.path);
         // write the text file to the file system
         const inputListName = 'concat.txt';
-        const textFilePath = path.join(filesDirectory, "concat.txt");
-        writeFile(textFilePath, filePaths.join("\n"), (err) => console.log(err));
-
+        const textFilePath = path.join(filesDirectory, inputListName);
+        // make a list of paths to create the ordered text file for ffmpeg
+        const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
+        writeFile(textFilePath, filePathsText, (err) => console.log(err));
+        console.log(filePathsText)
 
         // make output file name based on timestamp
-        const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
+        const outputFileName = `output-${Utils.GenerateGuid()}.mkv`;
+        // create the output file path in the parsed_file directory
+        const outputFilePath = path.join(filesDirectory, outputFileName);
+
+        // concatenate the videos
         await new Promise((resolve, reject) => {
-            ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save(outputFileName)
+            console.log('concatenating videos');
+            var merge = ffmpeg();
+            merge.input(textFilePath)
+            .inputOptions(['-f concat', '-safe 0'])
+                .outputOptions('-c copy')
+                //.videoCodec("copy")
+                .save(outputFilePath)
                 .on("error", reject)
                 .on("end", resolve);
         })
@@ -83,10 +96,41 @@ export namespace DashUploadUtils {
         unlinkSync(textFilePath);
 
         // read the output file from the file system
-        const outputFile = fs.readFileSync(outputFileName);
-        console.log('outputFile', outputFile);
-        // move only the output file to the videos directory
-        return MoveParsedFile(outputFile, Directory.videos)
+        // const outputFile = fs.readFileSync(outputFilePath);
+
+        // make a new blob object with the output file buffer
+        // const outputFileBlob = new Blob([outputFile.buffer], { type: 'x-matroska/mkv' });
+
+        // TODO: make with toJSON()
+
+        // make a data object
+        const outputFileObject: formidable.File = {
+            size: 0,
+            name: outputFileName,
+            path: outputFilePath,
+            // size: outputFileBlob.size,
+            type: 'video/x-matroska;codecs=avc1,opus',
+            lastModifiedDate: new Date(),
+
+            toJSON: () => ({ ...outputFileObject, filename: outputFilePath.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any })
+        }
+
+       // const file = { ...outputFileObject, toJSON: () => ({ ...outputFileObject, filename: outputFilePath.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) };
+
+        // this will convert it to mp4 and save it to the server
+        //return await MoveParsedFile(outputFileObject, Directory.videos);
+
+        return await upload(outputFileObject);
+
+        // // return only the output (first) file to the videos directory
+        // return {
+        //     source: file, result: {
+        //         accessPaths: {
+        //             agnostic: getAccessPaths(Directory.videos, outputFileName)
+        //         },
+        //         rawText: undefined
+        //     }
+        // }
     }
 
     export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
@@ -122,6 +166,7 @@ export namespace DashUploadUtils {
                 }
             case "video":
                 if (format.includes("x-matroska")) {
+                    console.log("case video");
                     await new Promise(res => ffmpeg(file.path)
                         .videoCodec("copy") // this will copy the data instead of reencode it
                         .save(file.path.replace(".mkv", ".mp4"))
-- 
cgit v1.2.3-70-g09d2


From bc6aa7b8e7c9e43901f500d58acb0ebb6450b0a5 Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Wed, 8 Jun 2022 15:38:40 -0400
Subject: got basic ordering to work for the videos that go to the server

---
 .../views/nodes/RecordingBox/ProgressBar.tsx       |  7 +++++-
 .../views/nodes/RecordingBox/RecordingView.tsx     | 25 ++++++++++++++++------
 2 files changed, 25 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 3314da355..a91656cbc 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -9,6 +9,7 @@ import { MediaSegment } from './RecordingView';
 interface ProgressBarProps {
   videos: MediaSegment[],
   setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
+  orderVideos: boolean,
 }
 
 interface SegmentBox {
@@ -46,7 +47,7 @@ export function ProgressBar(props: ProgressBarProps) {
 
   useEffect(() => {
     const order = props.videos.length
-    if (order) {
+    if (order && !props.orderVideos) {
       const { endTime, startTime } = props.videos.lastElement();
       setOrdered(prevOrdered => {
         return [...prevOrdered, { endTime, startTime , order }];
@@ -59,6 +60,10 @@ export function ProgressBar(props: ProgressBarProps) {
     // }))
   }, [props.videos]);
 
+  useEffect(() => {
+    props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1]));
+  }, [props.orderVideos]);
+
 
 
       // const handleClick = (e: React.MouseEvent) => {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index aea7f56b5..a5c2dc85c 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -34,7 +34,7 @@ export function RecordingView(props: IRecordingViewProps) {
       const [progress, setProgress] = useState(0);
 
     const [videos, setVideos] = useState<MediaSegment[]>([]);
-    // const [order, setOrder] = useState<number[]>([]);
+    const [orderVideos, setOrderVideos] = useState<boolean>(false);
       const videoRecorder = useRef<MediaRecorder | null>(null);
       const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
@@ -57,13 +57,14 @@ export function RecordingView(props: IRecordingViewProps) {
 
     useEffect(() => {
           
-        console.log('in finish useEffect')
+        console.log('in videos useEffect')
 
           if (finished) {
               (async () => {
                 const inputPaths: string[] = [];
                 const videoFiles: File[] = []
-                  videos.forEach(async (vid, i) => {
+                videos.forEach(async (vid, i) => {
+                    console.log(vid)
                     const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() });
                     videoFiles.push(videoFile);
                     
@@ -71,7 +72,7 @@ export function RecordingView(props: IRecordingViewProps) {
                     inputPaths.push(name)
                   })
                 
-                  console.log(videoFiles)
+                  console.log(inputPaths)
                   
                   const data = await Networking.UploadSegmentsAndConcatenate(videoFiles)
                   console.log('data', data)
@@ -158,6 +159,17 @@ export function RecordingView(props: IRecordingViewProps) {
             }
 
 
+    }, [videos])
+  
+  useEffect(() => {
+          
+        console.log('in finish useEffect')
+
+    if (finished) {
+      setOrderVideos(true);
+    }
+
+
       }, [finished])
 
       useEffect(() => {
@@ -360,8 +372,9 @@ export function RecordingView(props: IRecordingViewProps) {
                   </div>
                   
                                          <ProgressBar
-                videos={videos}
-                setVideos={setVideos}
+              videos={videos}
+              setVideos={setVideos}
+              orderVideos={orderVideos}
                               // playSegment={playSegment}
                               />
                   </div>
-- 
cgit v1.2.3-70-g09d2


From 48c60bd982734676f972514f7074be6121e7c5df Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Wed, 8 Jun 2022 18:24:53 -0400
Subject: big commit. FINALLY got the combining segments to work using ffmpeg
 using a different workflow. small ui changes as well.

---
 src/client/Network.ts                              |  15 +--
 .../views/nodes/RecordingBox/ProgressBar.tsx       |  20 +++-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   7 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     | 126 ++++-----------------
 src/server/ApiManagers/UploadManager.ts            |  23 +---
 src/server/DashUploadUtils.ts                      |  59 +++-------
 6 files changed, 60 insertions(+), 190 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index 8c1f31488..b26f2458d 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -35,20 +35,7 @@ export namespace Networking {
             const response = await fetch("/uploadFormData", parameters);
             return response.json();
       }
-    
-    export async function UploadSegmentsAndConcatenate<T extends Upload.FileInformation = Upload.FileInformation>(files: File[]): Promise<Upload.FileResponse<T>[]> {
-            console.log("network.ts : uploading segments and concatenating", files);
-            const formData = new FormData();
-            if (!Array.isArray(files) || !files.length) return [];
-            files.forEach(file => formData.append(Utils.GenerateGuid(), file));
-            const parameters = {
-                  method: 'POST',
-                  body: formData
-            };
-            const response = await fetch("/uploadVideosandConcatenate", parameters);
-            return response.json();
-      }
-
+  
       export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
             const parameters = {
                   method: 'POST',
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index a91656cbc..effc3d8a8 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -53,11 +53,6 @@ export function ProgressBar(props: ProgressBarProps) {
         return [...prevOrdered, { endTime, startTime , order }];
       });
     }
-    // }props.videos.map((vid, order) => {
-    // //const { endTime, startTime } = vid
-    //   // TODO: not tranfer the blobs around
-    //   return { ...vid, order };
-    // }))
   }, [props.videos]);
 
   useEffect(() => {
@@ -96,7 +91,6 @@ export function ProgressBar(props: ProgressBarProps) {
   }
   
   const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
-    console.log('pointer down')
     // don't move the videobox element
     e.stopPropagation()
 
@@ -107,6 +101,20 @@ export function ProgressBar(props: ProgressBarProps) {
     // don't do anything if null
     const progressBar = progressBarRef.current
     if (progressBar == null || clickedSegment.id === progressBar.id) return
+
+    console.log('pointer down')
+    // if holding shift key, let's remove that segment
+    if (e.shiftKey) {
+      // TODO: fix this
+      const segId = parseInt(clickedSegment.id.split('-')[1])
+      const o = ordered[segId].order
+      setOrdered(prevOrdered => {
+        return prevOrdered.filter((seg, i) => i !== segId)
+      })
+
+      return
+    }
+
     const ptrId = e.pointerId;
     progressBar.setPointerCapture(ptrId)
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 68f3b3ad4..bd4af0a75 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -30,7 +30,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             Doc.SetNativeHeight(this.dataDoc, 720);
       }
 
-      @observable result: Upload.FileInformation | undefined = undefined
+      @observable result: Upload.AccessPathInfo | undefined = undefined
       @observable videoDuration: number | undefined = undefined
 
       @action
@@ -39,13 +39,14 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       @action
-      setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
+      setResult = (info: Upload.AccessPathInfo, trackScreen: boolean) => {
+        if (info == null) return
             this.result = info
             this.dataDoc.type = DocumentType.VID;
             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.props.fieldKey] = new VideoField(this.result.accessPaths.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
             // stringify the presenation and store it
             if (trackScreen) {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index a5c2dc85c..8c8728fc3 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -18,7 +18,7 @@ export interface MediaSegment {
 }
 
 interface IRecordingViewProps {
-      setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
+      setResult: (info: Upload.AccessPathInfo, trackScreen: boolean) => void
       setDuration: (seconds: number) => void
       id: string
 }
@@ -57,7 +57,7 @@ export function RecordingView(props: IRecordingViewProps) {
 
     useEffect(() => {
           
-        console.log('in videos useEffect')
+        // console.log('in videos useEffect', finished)
 
           if (finished) {
               (async () => {
@@ -71,89 +71,18 @@ export function RecordingView(props: IRecordingViewProps) {
                     const { name } = videoFile;
                     inputPaths.push(name)
                   })
-                
-                  console.log(inputPaths)
                   
-                  const data = await Networking.UploadSegmentsAndConcatenate(videoFiles)
-                  console.log('data', data)
-                  const result = data[0].result
-                  if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                      props.setResult(result, trackScreen)
-                  } else {
-                      alert("video conversion failed");
-                  }
-                
-                
+                const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
+                  .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
                 
-                // const inputListName = 'order.txt';
-                // fs.writeFileSync(inputListName, inputPaths.join('\n'));
-                // var merge = ffmpeg();
-                // merge.input(inputListName)
-                //   .inputOptions(['-f concat', '-safe 0'])
-                //   .outputOptions('-c copy')
-                //   .save('output.mp4')
-                
-                // fs.unlinkSync(inputListName);
-
-                // const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths)
-                // console.log('combined', combined)
-                
-                // const outputFile = new File(['output.mp4'], 'output.mp4', { type: 'video/mp4', lastModified: Date.now() });
-
-                // const data = await Networking.UploadFilesToServer(combined)
-                //   const result = data[0].result
-                //   if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                //       props.setResult(result, trackScreen)
-                //   } else {
-                //       alert("video conversion failed");
-                //   }
-                  
-
-                //   if (format.includes("x-matroska")) {
-                //     await new Promise(res => ffmpeg(file.path)
-                //         .videoCodec("copy") // this will copy the data instead of reencode it
-                //         .save(file.path.replace(".mkv", ".mp4"))
-                //         .on('end', res));
-                //     file.path = file.path.replace(".mkv", ".mp4");
-                //     format = ".mp4";
-                // }
-                //   console.log('crossOriginIsolated', crossOriginIsolated)
-                //   props.setDuration(recordingTimer * 100)
-
-                //   console.log('Loading ffmpeg-core.js');
-                //   const ffmpeg = createFFmpeg({ log: true });
-                //   await ffmpeg.load();
-                //   console.log('ffmpeg-core.js loaded');
-                  
-                //   let allVideoChunks: any = [];
-                //   const inputPaths: string[] = [];
-                //   // write each segment into it's indexed file
-                //   videos.forEach(async (vid, i) => {
-                //       const vidName = `segvideo${i}.mkv`
-                //       inputPaths.push(vidName)
-                //       const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() });
-                //       ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile));
-                // //   })
-                //   ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n'));
-                  
-                //   console.log('concat')
-                //   await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4');
-
-                //   const { buffer } = ffmpeg.FS('readFile', 'output.mp4');
-                //   const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" });
-
-                //   const data = await Networking.UploadFilesToServer(concatVideo)
-                //   const result = data[0].result
-                //   if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                //       props.setResult(result, trackScreen)
-                //   } else {
-                //       alert("video conversion failed");
-                //   }
-                  
-                //   // delete all files in MEMFS
-                //   inputPaths.forEach(path => ffmpeg.FS('unlink', path));
-                //   ffmpeg.FS('unlink', 'order.txt');
-                //   ffmpeg.FS('unlink', 'output.mp4');
+                const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths)
+                console.log(result)
+                if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                    props.setResult(result, trackScreen)
+                } else {
+                    alert("video conversion failed");
+                }
+              
               })();
               
             }
@@ -162,8 +91,6 @@ export function RecordingView(props: IRecordingViewProps) {
     }, [videos])
   
   useEffect(() => {
-          
-        console.log('in finish useEffect')
 
     if (finished) {
       setOrderVideos(true);
@@ -177,7 +104,7 @@ export function RecordingView(props: IRecordingViewProps) {
             if (!navigator.mediaDevices) {
                   console.log('This browser does not support getUserMedia.')
             }
-          console.log('This device has the correct media devices.')
+          // console.log('This device has the correct media devices.')
       }, [])
 
       useEffect(() => {
@@ -244,7 +171,6 @@ export function RecordingView(props: IRecordingViewProps) {
                   // reset the temporary chunks
                   videoChunks = []
                   setRecording(false);
-                  setFinished(true);
                   trackScreen && RecordingApi.Instance.pause();
             }
 
@@ -269,8 +195,10 @@ export function RecordingView(props: IRecordingViewProps) {
       }
 
 
-      const stop = () => {
-            if (videoRecorder.current) {
+  const stop = (e: React.MouseEvent) => {
+        e.stopPropagation()
+        if (videoRecorder.current) {
+                                      setFinished(true);
                   if (videoRecorder.current.state !== "inactive") {
                         videoRecorder.current.stop();
                         // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
@@ -278,19 +206,21 @@ export function RecordingView(props: IRecordingViewProps) {
             }
       }
 
-      const pause = () => {
+      const pause = (e: React.MouseEvent) => {
+        e.stopPropagation()
             if (videoRecorder.current) {
                   if (videoRecorder.current.state === "recording") {
-                        videoRecorder.current.pause();
+                    videoRecorder.current.stop();
                   }
             }
       }
 
-      const startOrResume = () => {
+      const startOrResume =  (e: React.MouseEvent) => {
+        e.stopPropagation()
             if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
                   record();
             } else if (videoRecorder.current.state === "paused") {
-                  videoRecorder.current.resume();
+              videoRecorder.current.start();
             }
       }
 
@@ -315,16 +245,6 @@ export function RecordingView(props: IRecordingViewProps) {
             const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
             return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
       }
-    
-      const doTranscode = async () => {
-          
-        // console.log('Start transcoding');
-        // ffmpeg.FS('writeFile', 'test.avi', await fetchFile('/flame.avi'));
-        // await ffmpeg.run('-i', 'test.avi', 'test.mp4');
-        // console.log('Complete transcoding');
-        // const data = ffmpeg.FS('readFile', 'test.mp4');
-        // console.log(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })));
-    };
 
       return (
             <div className="recording-container">
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 398b007b5..0ee0a34df 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -44,25 +44,10 @@ export default class UploadManager extends ApiManager {
           
             register({
                   method: Method.POST,
-                  subscription: "/uploadVideosandConcatenate",
-                  secureHandler: async ({ req, res }) => {
-                        const form = new formidable.IncomingForm();
-                        form.keepExtensions = true;
-                        form.uploadDir = pathToDirectory(Directory.parsed_files);
-                        return new Promise<void>(resolve => {
-                              form.parse(req, async (_err, _fields, files) => {
-                                  const results: Upload.FileResponse[] = [];
-
-                                  // create an array of all the file paths
-                                  const filePaths: string[] = Object.keys(files).map(key => files[key].path);
-                                    console.log("uploading files", Array.isArray(filePaths));
-                                    const result = await DashUploadUtils.concatenateVideos(filePaths);
-                                    console.log('concatenated', result);
-                                    result && !(result.result instanceof Error) && results.push(result);
-                                    _success(res, results)
-                                    resolve();
-                              });
-                        });
+                  subscription: "/concatVideos",
+              secureHandler: async ({ req, res }) => {
+                    // req.body contains the array of server paths to the videos
+                    _success(res, await DashUploadUtils.concatVideos(req.body));
                   }
             });
 
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 148b0df65..be30c115d 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -63,21 +63,20 @@ export namespace DashUploadUtils {
 
     const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr 
     
-    export async function concatenateVideos(filePaths: string[]): Promise<Upload.FileResponse> {
+    export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> {
         // make a list of paths to create the ordered text file for ffmpeg
-        //const filePaths = videoFiles.map(file => file.path);
-        // write the text file to the file system
         const inputListName = 'concat.txt';
         const textFilePath = path.join(filesDirectory, inputListName);
         // make a list of paths to create the ordered text file for ffmpeg
-        const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
+      const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
+        // write the text file to the file system
         writeFile(textFilePath, filePathsText, (err) => console.log(err));
-        console.log(filePathsText)
+        console.log('fileTextPaths', filePathsText)
 
         // make output file name based on timestamp
-        const outputFileName = `output-${Utils.GenerateGuid()}.mkv`;
-        // create the output file path in the parsed_file directory
-        const outputFilePath = path.join(filesDirectory, outputFileName);
+        const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
+        // create the output file path in the videos directory
+        const outputFilePath = path.join(pathToDirectory(Directory.videos), outputFileName);
 
         // concatenate the videos
         await new Promise((resolve, reject) => {
@@ -92,45 +91,15 @@ export namespace DashUploadUtils {
                 .on("end", resolve);
         })
 
-        // delete concat.txt from the file system
-        unlinkSync(textFilePath);
-
-        // read the output file from the file system
-        // const outputFile = fs.readFileSync(outputFilePath);
-
-        // make a new blob object with the output file buffer
-        // const outputFileBlob = new Blob([outputFile.buffer], { type: 'x-matroska/mkv' });
-
-        // TODO: make with toJSON()
+      // delete concat.txt from the file system
+      unlinkSync(textFilePath);
+      // delete the old segment videos from the server
+      filePaths.forEach(filePath => unlinkSync(filePath));
 
-        // make a data object
-        const outputFileObject: formidable.File = {
-            size: 0,
-            name: outputFileName,
-            path: outputFilePath,
-            // size: outputFileBlob.size,
-            type: 'video/x-matroska;codecs=avc1,opus',
-            lastModifiedDate: new Date(),
-
-            toJSON: () => ({ ...outputFileObject, filename: outputFilePath.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any })
+      // return the path(s) to the output file
+      return {
+          accessPaths: getAccessPaths(Directory.videos, outputFileName)
         }
-
-       // const file = { ...outputFileObject, toJSON: () => ({ ...outputFileObject, filename: outputFilePath.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) };
-
-        // this will convert it to mp4 and save it to the server
-        //return await MoveParsedFile(outputFileObject, Directory.videos);
-
-        return await upload(outputFileObject);
-
-        // // return only the output (first) file to the videos directory
-        // return {
-        //     source: file, result: {
-        //         accessPaths: {
-        //             agnostic: getAccessPaths(Directory.videos, outputFileName)
-        //         },
-        //         rawText: undefined
-        //     }
-        // }
     }
 
     export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
-- 
cgit v1.2.3-70-g09d2


From bb8d6b6ad83bf20cdaf0f9a4b6665b3c1371cb95 Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Thu, 9 Jun 2022 10:40:34 -0400
Subject: got the removing on the shift key to work.

---
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 72 +++++++++++++---------
 .../views/nodes/RecordingBox/RecordingView.tsx     |  9 ++-
 2 files changed, 52 insertions(+), 29 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index effc3d8a8..7ceae6696 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -33,51 +33,71 @@ export function ProgressBar(props: ProgressBarProps) {
 
   const [dragged, setDragged] = useState<number>(-1);
 
+  const [totalTime, setTotalTime] = useState<number>(0);
+
+  // this holds the index of the videoc segment to be removed
+  const [removed, setRemoved] = useState<number>(-1);
+
   // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos])
 
-  const totalTime = () => props.videos.lastElement().endTime
+  // const totalTime = () => props.videos.lastElement().endTime
   // const memoTotal = useMemo(totalTime, [props.videos])
 
   useEffect(() => {
     const segmentsJSX = ordered.map((seg, i) =>
-        <div id={`segment-${i.toString()}`} className={ dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime()) * 100}%` }}>{seg.order}</div>);
+        <div id={`segment-${i.toString()}`} className={ dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order}</div>);
       
     setSegments(segmentsJSX)
   }, [dragged, ordered])
 
   useEffect(() => {
+    // this useEffect fired when the videos are being rearragned to the order
+    // in this case, do nothing.
+    if (props.orderVideos) return;
+
     const order = props.videos.length
-    if (order && !props.orderVideos) {
+    // in this case, a new video is added
+    if (order && order > ordered.length) {
       const { endTime, startTime } = props.videos.lastElement();
+      // update total time
+      setTotalTime(prevTime => prevTime + (endTime - startTime));
+      // add the new segment to the ordered array
       setOrdered(prevOrdered => {
         return [...prevOrdered, { endTime, startTime , order }];
       });
     }
+
+    // in this case, a segment is removed
+    else if (order < ordered.length) {
+      // update the total time
+      setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime));
+      //remove the segment from the ordered array and decrement the order of the remaining segments 
+      // if they are greater than the removed segment's order 
+      const removedOrder = ordered[removed].order;
+      setOrdered(prevOrdered => 
+        prevOrdered.reduce((acc, seg, i) => {
+          (i !== removed) && acc.push({ ...seg, order: seg.order > removedOrder ? seg.order - 1 : seg.order });
+          return acc;
+        },[] as SegmentBox[])
+      );
+      // reset to default/nullish state
+      setRemoved(-1);
+    }
   }, [props.videos]);
 
   useEffect(() => {
     props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1]));
   }, [props.orderVideos]);
 
+  useEffect(() => {
+    if (removed === -1) return;
 
+    console.log('removed', removed)
 
-      // const handleClick = (e: React.MouseEvent) => {
-      //       let progressbar = document.getElementById('progressbar')!
-      //       let bounds = progressbar!.getBoundingClientRect();
-      //       let x = e.clientX - bounds.left;
-      //       let percent = x / progressbar.clientWidth * 100
-
-      //       for (let i = 0; i < props.marks.length; i++) {
-      //             let start = i == 0 ? 0 : props.marks[i-1];
-      //             if (percent > start && percent < props.marks[i]) {
-      //                   props.playSegment(i)
-      //                   // console.log(i)
-      //                   // console.log(percent)
-      //                   // console.log(props.marks[i])
-      //                   break
-      //             }
-      //       }
-      // }
+    // update the videos array -> this will fire the useEffect above, where we update the orders & totalTime
+    const index = ordered[removed].order - 1;
+    props.setVideos(vids => vids.filter((vid, i) => i !== index));
+  }, [removed]);
   
   const updateLastHover = (segId: number): CurrentHover | null => {
     // get the segId of the segment that will become the new bounding area
@@ -102,16 +122,12 @@ export function ProgressBar(props: ProgressBarProps) {
     const progressBar = progressBarRef.current
     if (progressBar == null || clickedSegment.id === progressBar.id) return
 
-    console.log('pointer down')
     // if holding shift key, let's remove that segment
+    // TODO: think of a way to accomodate touch -> maybe if poiuntermove isn't fired after x secs?
     if (e.shiftKey) {
-      // TODO: fix this
-      const segId = parseInt(clickedSegment.id.split('-')[1])
-      const o = ordered[segId].order
-      setOrdered(prevOrdered => {
-        return prevOrdered.filter((seg, i) => i !== segId)
-      })
-
+      const segId = parseInt(clickedSegment.id.split('-')[1]);
+      console.log('removing segment', segId)
+      setRemoved(segId);
       return
     }
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 8c8728fc3..68d85855c 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -55,6 +55,7 @@ export function RecordingView(props: IRecordingViewProps) {
             }
       }
 
+
     useEffect(() => {
           
         // console.log('in videos useEffect', finished)
@@ -90,6 +91,12 @@ export function RecordingView(props: IRecordingViewProps) {
 
     }, [videos])
   
+    
+  // make useEffect for progess and log when it fires
+  useEffect(() => {
+    console.log('progress', progress)
+  }, [progress])
+  
   useEffect(() => {
 
     if (finished) {
@@ -198,7 +205,7 @@ export function RecordingView(props: IRecordingViewProps) {
   const stop = (e: React.MouseEvent) => {
         e.stopPropagation()
         if (videoRecorder.current) {
-                                      setFinished(true);
+          setFinished(true);
                   if (videoRecorder.current.state !== "inactive") {
                         videoRecorder.current.stop();
                         // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
-- 
cgit v1.2.3-70-g09d2


From 28ec1d48ee9e8f4d8d82337c2116ac8027ed653d Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Thu, 9 Jun 2022 11:08:34 -0400
Subject: got the progress bar to move while recording, but it is a tad slow

---
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 35 +++++++++++++++++++---
 .../views/nodes/RecordingBox/RecordingView.tsx     |  8 ++---
 2 files changed, 33 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 7ceae6696..ce231ba62 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -10,6 +10,8 @@ interface ProgressBarProps {
   videos: MediaSegment[],
   setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
   orderVideos: boolean,
+  progress: number,
+  recording: boolean,
 }
 
 interface SegmentBox {
@@ -33,7 +35,11 @@ export function ProgressBar(props: ProgressBarProps) {
 
   const [dragged, setDragged] = useState<number>(-1);
 
-  const [totalTime, setTotalTime] = useState<number>(0);
+  // total length of the video, in seconds*100
+  // const [totalTime, setTotalTime] = useState<number>(0);
+
+  // length of the time removed from the video, in seconds*100
+  const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
 
   // this holds the index of the videoc segment to be removed
   const [removed, setRemoved] = useState<number>(-1);
@@ -43,12 +49,31 @@ export function ProgressBar(props: ProgressBarProps) {
   // const totalTime = () => props.videos.lastElement().endTime
   // const memoTotal = useMemo(totalTime, [props.videos])
 
+  // useEffect that updates total time based on progress
+  useEffect(() => {
+    console.log("useEffect progress", props.progress, 'totalRemovedTime', totalRemovedTime)
+    // setTotalTime(prev => {
+    //   // progress is in seconds, prev is in deciseconds?
+    //   const toAdd = props.progress * 10 - prev
+    //   return prev + toAdd
+    // })
+    // setTotalTime(prev => props.progress * 10)
+  }, [props.progress])
+
   useEffect(() => {
+    console.log("useEffect recording", props.recording)
+  }, [props.recording])
+  
+
+  useEffect(() => {
+    console.log('rendering new segments')
+    const totalTime = props.progress*1000 - totalRemovedTime
     const segmentsJSX = ordered.map((seg, i) =>
         <div id={`segment-${i.toString()}`} className={ dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order}</div>);
       
     setSegments(segmentsJSX)
-  }, [dragged, ordered])
+  }, [dragged, ordered, props.progress])
+
 
   useEffect(() => {
     // this useEffect fired when the videos are being rearragned to the order
@@ -60,7 +85,7 @@ export function ProgressBar(props: ProgressBarProps) {
     if (order && order > ordered.length) {
       const { endTime, startTime } = props.videos.lastElement();
       // update total time
-      setTotalTime(prevTime => prevTime + (endTime - startTime));
+      // setTotalTime(prevTime => prevTime + (endTime - startTime));
       // add the new segment to the ordered array
       setOrdered(prevOrdered => {
         return [...prevOrdered, { endTime, startTime , order }];
@@ -70,7 +95,9 @@ export function ProgressBar(props: ProgressBarProps) {
     // in this case, a segment is removed
     else if (order < ordered.length) {
       // update the total time
-      setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime));
+      // setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime));
+      // update total removed time
+      setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
       //remove the segment from the ordered array and decrement the order of the remaining segments 
       // if they are greater than the removed segment's order 
       const removedOrder = ordered[removed].order;
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 68d85855c..40117c41c 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -91,12 +91,6 @@ export function RecordingView(props: IRecordingViewProps) {
 
     }, [videos])
   
-    
-  // make useEffect for progess and log when it fires
-  useEffect(() => {
-    console.log('progress', progress)
-  }, [progress])
-  
   useEffect(() => {
 
     if (finished) {
@@ -302,6 +296,8 @@ export function RecordingView(props: IRecordingViewProps) {
               videos={videos}
               setVideos={setVideos}
               orderVideos={orderVideos}
+              progress={progress}
+              recording={recording}
                               // playSegment={playSegment}
                               />
                   </div>
-- 
cgit v1.2.3-70-g09d2


From 9024621c15962f3cdfccee6fc35294b44d4cf1ee Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Thu, 9 Jun 2022 16:27:27 -0400
Subject: fix undo, add disabled ui while recording, create expanding-segment
 to fill the space while recording

---
 .../views/nodes/RecordingBox/ProgressBar.scss      |  25 +++-
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 158 ++++++++++-----------
 .../views/nodes/RecordingBox/RecordingView.tsx     |  40 +++---
 3 files changed, 122 insertions(+), 101 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index 3ed7cee3e..1cf60b82b 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -45,8 +45,31 @@
   text-align: center;
 }
 
-.no-transition {
+// .no-transition {
+//   transition-duration: 0s;
+// }
+
+.segment-expanding {
+border-color: red;
+  background-color: white;
+  transition-duration: 0s;
+  opacity: .75;
+  pointer-events: none;
+}
+
+.segment-expanding:hover {
+    background-color: inherit;
+    cursor: not-allowed;
+}
+
+.segment-disabled {
+  pointer-events: none;
+  opacity: 0.5;
   transition-duration: 0s;
+  /* Hide the text. */
+    text-indent: 100%;
+    white-space: nowrap;
+    overflow: hidden;
 }
 
 .segment-hide {
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 54bfbc792..a00a4643d 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -1,8 +1,9 @@
+import { disable } from 'colors';
 import { resolveTxt } from 'dns';
 import { videointelligence } from 'googleapis/build/src/apis/videointelligence';
 import { isInteger } from 'lodash';
 import * as React from 'react';
-import { useEffect, useState, useCallback, useRef, useMemo } from "react"
+import { useEffect, useState, useCallback, useRef } from "react"
 import "./ProgressBar.scss"
 import { MediaSegment } from './RecordingView';
 
@@ -11,7 +12,8 @@ interface ProgressBarProps {
   setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
   orderVideos: boolean,
   progress: number,
-  recording: boolean,
+    recording: boolean,
+    doUndo: boolean,
 }
 
 interface SegmentBox {
@@ -31,68 +33,73 @@ export function ProgressBar(props: ProgressBarProps) {
 
   // array for the order of video segments
   const [segments, setSegments] = useState<JSX.Element[]>([]);
-  const [ordered, setOrdered] = useState<SegmentBox[]>([]);
+    const [ordered, setOrdered] = useState<SegmentBox[]>([]);
+    
+    const [undoStack, setUndoStack] = useState<SegmentBox[]>([]);
 
   const [dragged, setDragged] = useState<number>(-1);
 
-  // total length of the video, in seconds*100
-  // const [totalTime, setTotalTime] = useState<number>(0);
-
   // length of the time removed from the video, in seconds*100
   const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
 
   // this holds the index of the videoc segment to be removed
   const [removed, setRemoved] = useState<number>(-1);
-
-  // const totalTime = useMemo(() => props.progress*1000 - totalRemovedTime, [props.progress, totalRemovedTime]);
-
-  // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos])
-
-  // const totalTime = () => props.videos.lastElement().endTime
-  // const memoTotal = useMemo(totalTime, [props.videos])
-
-  // useEffect that updates total time based on progress
-  // useEffect(() => {
-  //   console.log("useEffect progress", props.progress, 'totalRemovedTime', totalRemovedTime)
-  //   // setTotalTime(prev => {
-  //   //   // progress is in seconds, prev is in deciseconds?
-  //   //   const toAdd = props.progress * 10 - prev
-  //   //   return prev + toAdd
-  //   // })
-  //   // setTotalTime(prev => props.progress * 10)
-  // }, [props.progress])
+    
+    
+    // abstracted for other uses - brings back the most recently deleted segment
+    const handleUndo = () => {
+        // get the last element from the undo if it exists
+        if (undoStack.length === 0) return;
+        // get and remove the last element from the undo stack
+        const last = undoStack.lastElement();
+        setUndoStack(prevUndo => prevUndo.slice(0, -1));
+
+        // update the removed time and place element back into ordered
+        setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime));
+        setOrdered(prevOrdered => [...prevOrdered, last]);
+    }
+    useEffect(() => handleUndo(), [props.doUndo])
 
   useEffect(() => {
-    console.log("useEffect recording", props.recording)
-    segments.forEach((seg, i) => {
-      const htmlId = seg.props.id;
-      const segmentHtml = document.getElementById(htmlId);
-      segmentHtml?.classList.toggle('no-transition', props.recording);
-      console.log(segmentHtml)
-    });
+      console.log("useEffect recording", props.recording)
+      // get segments segment's html using it's id -> make them appeared disabled (or enabled)
+      segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
+      
+      if (props.recording)
+          setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding' style={{ width: 'auto' }}>{props.videos.length + 1}</div>]);
   }, [props.recording])
   
 
   useEffect(() => {
-    console.log('useEffect dragged, ordered')
     const totalTime = props.progress * 1000 - totalRemovedTime;
     const segmentsJSX = ordered.map((seg, i) =>
-      <div id={`segment-${i.toString()}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order}</div>);
+        <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>);
       
     setSegments(segmentsJSX)
   }, [dragged, ordered]);
 
   // to imporve performance, we only want to update the width, not re-render the whole thing
-  useEffect(() => {
-    // console.log('updating width on progress useEffect')
-    const totalTime = props.progress * 1000 - totalRemovedTime;
-    segments.forEach((seg, i) => {
-      const htmlId = seg.props.id;
-      const segmentHtml = document.getElementById(htmlId);
-      segmentHtml?.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
+    useEffect(() => {
+      if (!props.recording) return
+        const totalTime = props.progress * 1000 - totalRemovedTime;
+        let remainingTime = totalTime;
+        segments.forEach((seg, i) => {
+            // for the last segment, we need to set that directly
+            if (i === segments.length - 1) return;
+            // update remaining time
+            remainingTime -= (ordered[i].endTime - ordered[i].startTime);
+
+        // update the width for this segment
+            const htmlId = seg.props.id;
+            const segmentHtml = document.getElementById(htmlId);
+            if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
+      });
 
-      // console.log('updating width on progress useEffect', htmlId, segmentHtml, segmentHtml?.style.width)
-    });
+        // update the width of the expanding segment using the remaining time
+        const segExapandHtml = document.getElementById('segment-expanding');
+        if (segExapandHtml)
+            segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`;
+        
   }, [props.progress]);
       
 
@@ -102,52 +109,36 @@ export function ProgressBar(props: ProgressBarProps) {
     // in this case, do nothing.
     if (props.orderVideos) return;
 
-    const order = props.videos.length
-    // in this case, a new video is added
-    if (order && order > ordered.length) {
+    const order = props.videos.length - 1;
+    // in this case, a new video is added -> push it onto ordered
+    if (order >= ordered.length) {
       const { endTime, startTime } = props.videos.lastElement();
-      // update total time
-      // setTotalTime(prevTime => prevTime + (endTime - startTime));
-      // add the new segment to the ordered array
       setOrdered(prevOrdered => {
         return [...prevOrdered, { endTime, startTime , order }];
       });
     }
 
-    // in this case, a segment is removed
+    // in this case, a video is removed
     else if (order < ordered.length) {
-      // update the total time
-      // setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime));
-
-      // update total removed time
-      setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
-
-      // remove the segment from the ordered array and decrement the order of the remaining segments 
-      // if they are greater than the removed segment's order 
-      const removedOrder = ordered[removed].order;
-      setOrdered(prevOrdered => 
-        prevOrdered.reduce((acc, seg, i) => {
-          (i !== removed) && acc.push({ ...seg, order: seg.order > removedOrder ? seg.order - 1 : seg.order });
-          return acc;
-        },[] as SegmentBox[])
-      );
-      // reset to default/nullish state
-      setRemoved(-1);
+        console.error('warning: video removed from parent');
     }
   }, [props.videos]);
 
   useEffect(() => {
-    props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1]));
+    props.setVideos(vids => ordered.map((seg) => vids[seg.order]));
   }, [props.orderVideos]);
 
   useEffect(() => {
     if (removed === -1) return;
-
-    console.log('removed', removed)
-
-    // update the videos array -> this will fire the useEffect above, where we update the orders & totalTime
-    const index = ordered[removed].order - 1;
-    props.setVideos(vids => vids.filter((vid, i) => i !== index));
+      // update total removed time
+      setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
+        
+      // put the element on the undo stack
+      setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
+      // remove the segment from the array
+      setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
+      // reset to default/nullish state
+      setRemoved(-1);
   }, [removed]);
   
   const updateLastHover = (segId: number): CurrentHover | null => {
@@ -163,7 +154,10 @@ export function ProgressBar(props: ProgressBarProps) {
   
   const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
     // don't move the videobox element
-    e.stopPropagation()
+      e.stopPropagation()
+      
+      // if recording, do nothing
+        if (props.recording) return;
 
     // get the segment the user clicked on to be dragged
     const clickedSegment = e.target as HTMLDivElement & EventTarget
@@ -177,10 +171,15 @@ export function ProgressBar(props: ProgressBarProps) {
     // TODO: think of a way to accomodate touch -> maybe if poiuntermove isn't fired after x secs?
     if (e.shiftKey) {
       const segId = parseInt(clickedSegment.id.split('-')[1]);
-      console.log('removing segment', segId)
       setRemoved(segId);
       return
     }
+    
+      if (e.ctrlKey) {
+        handleUndo();
+        return;
+    // todo: implement undo stack
+    }
 
     const ptrId = e.pointerId;
     progressBar.setPointerCapture(ptrId)
@@ -209,7 +208,6 @@ export function ProgressBar(props: ProgressBarProps) {
       event.preventDefault();
 
       // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
-      console.log('update cursor', progressBar.hasPointerCapture(ptrId), ptrId)
       if (!progressBar.hasPointerCapture(ptrId)) {
         placeSegmentandCleanup();
         return;
@@ -269,9 +267,6 @@ export function ProgressBar(props: ProgressBarProps) {
     dot.draggable = false;
     document.body.append(dot)
   }
-  const cleanupDetachSegment = (dot: HTMLDivElement) => {
-    dot.remove()
-  }
   const followCursor = (event: PointerEvent, dot: HTMLDivElement, rect: DOMRect): void => {
     // event.stopPropagation()
     // const { width, height } = dot.getBoundingClientRect()
@@ -283,11 +278,6 @@ export function ProgressBar(props: ProgressBarProps) {
 
       return (
         <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
-                  {/* <div
-                        className="progressbar done"
-                        style={{ width: `${props.progress}%` }}
-                  // onClick={handleClick}
-                  ></div> */}
           {segments}
         </div>
       )
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 40117c41c..e131420c3 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -31,7 +31,9 @@ export function RecordingView(props: IRecordingViewProps) {
       const recordingTimerRef = useRef<number>(0);
       const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
       const [playing, setPlaying] = useState(false);
-      const [progress, setProgress] = useState(0);
+    const [progress, setProgress] = useState(0);
+    
+    const [doUndo, setDoUndo] = useState(false);
 
     const [videos, setVideos] = useState<MediaSegment[]>([]);
     const [orderVideos, setOrderVideos] = useState<boolean>(false);
@@ -225,11 +227,15 @@ export function RecordingView(props: IRecordingViewProps) {
             }
       }
 
-      const clearPrevious = () => {
-            const numVideos = videos.length
-            setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-            setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-            setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
+    const undoPrevious = (e: React.MouseEvent) => {
+        e.stopPropagation();
+        console.log('undo previous', doUndo)
+        setDoUndo(prev => !prev);
+            // const numVideos = videos.length
+            // setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+            // setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+            // setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
+          
       }
 
       const handleOnTimeUpdate = () => {
@@ -247,7 +253,8 @@ export function RecordingView(props: IRecordingViewProps) {
             return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
       }
 
-      return (
+    // TODO: have the undo button only appear if there is something to undo
+    return (
             <div className="recording-container">
                   <div className="video-wrapper">
                         <video id={`video-${props.id}`}
@@ -271,10 +278,10 @@ export function RecordingView(props: IRecordingViewProps) {
 
                                     {!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> */}
+                              <div className="options-wrapper video-edit-wrapper">
+                                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+                                                      <MdBackspace onClick={undoPrevious} />
+                                                </IconContext.Provider>
                                                 <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
                                                       <FaCheckCircle onClick={stop} />
                                                 </IconContext.Provider>
@@ -293,11 +300,12 @@ export function RecordingView(props: IRecordingViewProps) {
                   </div>
                   
                                          <ProgressBar
-              videos={videos}
-              setVideos={setVideos}
-              orderVideos={orderVideos}
-              progress={progress}
-              recording={recording}
+                      videos={videos}
+                      setVideos={setVideos}
+                      orderVideos={orderVideos}
+                      progress={progress}
+                      recording={recording}
+                      doUndo={doUndo}
                               // playSegment={playSegment}
                               />
                   </div>
-- 
cgit v1.2.3-70-g09d2


From f52e07fb2d1355fe783f05ced76555fa9980b692 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Thu, 9 Jun 2022 16:36:22 -0400
Subject: remove console.logs and cleaup code

---
 src/client/views/nodes/RecordingBox/RecordingView.tsx | 3 ---
 1 file changed, 3 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index e131420c3..701277cbd 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -67,7 +67,6 @@ export function RecordingView(props: IRecordingViewProps) {
                 const inputPaths: string[] = [];
                 const videoFiles: File[] = []
                 videos.forEach(async (vid, i) => {
-                    console.log(vid)
                     const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() });
                     videoFiles.push(videoFile);
                     
@@ -79,7 +78,6 @@ export function RecordingView(props: IRecordingViewProps) {
                   .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
                 
                 const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths)
-                console.log(result)
                 if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
                     props.setResult(result, trackScreen)
                 } else {
@@ -229,7 +227,6 @@ export function RecordingView(props: IRecordingViewProps) {
 
     const undoPrevious = (e: React.MouseEvent) => {
         e.stopPropagation();
-        console.log('undo previous', doUndo)
         setDoUndo(prev => !prev);
             // const numVideos = videos.length
             // setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-- 
cgit v1.2.3-70-g09d2


From 552d2e840e772652b9919531b056ba21858edbfd Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Thu, 9 Jun 2022 17:09:00 -0400
Subject: small ui changes, comments, and formatting :)

---
 .../views/nodes/RecordingBox/ProgressBar.scss      |   7 +-
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 425 ++++++++++----------
 .../views/nodes/RecordingBox/RecordingView.tsx     | 443 ++++++++++-----------
 3 files changed, 433 insertions(+), 442 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index 7768a2f03..28ad25ffa 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -37,6 +37,9 @@
     cursor: not-allowed;
 }
 
+.progressbar-dragging {
+    cursor: grabbing;
+}
 
 // citation: https://codepen.io/_Master_/pen/PRdjmQ
 @keyframes blinker {
@@ -113,6 +116,8 @@ border-color: red;
 }
 
 .segment-selected {
-  border: 4px solid grey;
+  border: 4px solid #202020;
   background-color: red;
+  opacity: .75;
+  cursor: grabbing;
 }
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 7da3f45be..81f753e44 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -1,51 +1,47 @@
-import { disable } from 'colors';
-import { resolveTxt } from 'dns';
-import { videointelligence } from 'googleapis/build/src/apis/videointelligence';
-import { isInteger } from 'lodash';
 import * as React from 'react';
 import { useEffect, useState, useCallback, useRef } from "react"
 import "./ProgressBar.scss"
 import { MediaSegment } from './RecordingView';
 
 interface ProgressBarProps {
-  videos: MediaSegment[],
-  setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
-  orderVideos: boolean,
-  progress: number,
+    videos: MediaSegment[],
+    setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
+    orderVideos: boolean,
+    progress: number,
     recording: boolean,
     doUndo: boolean,
 }
 
 interface SegmentBox {
-  endTime: number,
-  startTime: number,
-  order: number,
+    endTime: number,
+    startTime: number,
+    order: number,
 }
 interface CurrentHover {
-  index: number,
-  minX: number,
-  maxX: number
+    index: number,
+    minX: number,
+    maxX: number
 }
 
 export function ProgressBar(props: ProgressBarProps) {
 
-  const progressBarRef = useRef<HTMLDivElement | null>(null)
+    const progressBarRef = useRef<HTMLDivElement | null>(null)
 
-  // array for the order of video segments
-  const [segments, setSegments] = useState<JSX.Element[]>([]);
+    // array for the order of video segments
+    const [segments, setSegments] = useState<JSX.Element[]>([]);
     const [ordered, setOrdered] = useState<SegmentBox[]>([]);
-    
+
     const [undoStack, setUndoStack] = useState<SegmentBox[]>([]);
 
-  const [dragged, setDragged] = useState<number>(-1);
+    const [dragged, setDragged] = useState<number>(-1);
+
+    // length of the time removed from the video, in seconds*100
+    const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
+
+    // this holds the index of the videoc segment to be removed
+    const [removed, setRemoved] = useState<number>(-1);
 
-  // length of the time removed from the video, in seconds*100
-  const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
 
-  // this holds the index of the videoc segment to be removed
-  const [removed, setRemoved] = useState<number>(-1);
-    
-    
     // abstracted for other uses - brings back the most recently deleted segment
     const handleUndo = () => {
         // get the last element from the undo if it exists
@@ -60,27 +56,32 @@ export function ProgressBar(props: ProgressBarProps) {
     }
     useEffect(() => handleUndo(), [props.doUndo])
 
-  useEffect(() => {
-      // get segments segment's html using it's id -> make them appeared disabled (or enabled)
-      segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
-      progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);
-      
-      if (props.recording)
-          setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'min-content' }}>{props.videos.length + 1}</div>]);
-  }, [props.recording])
-  
-
-  useEffect(() => {
-    const totalTime = props.progress * 1000 - totalRemovedTime;
-    const segmentsJSX = ordered.map((seg, i) =>
-        <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>);
-      
-    setSegments(segmentsJSX)
-  }, [dragged, ordered]);
-
-  // to imporve performance, we only want to update the width, not re-render the whole thing
     useEffect(() => {
-      if (!props.recording) return
+        // get segments segment's html using it's id -> make them appeared disabled (or enabled)
+        segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
+        progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);
+
+        if (props.recording)
+            setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'min-content' }}>{props.videos.length + 1}</div>]);
+    }, [props.recording])
+
+
+    useEffect(() => {
+        const totalTime = props.progress * 1000 - totalRemovedTime;
+        const segmentsJSX = ordered.map((seg, i) =>
+            <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>);
+
+        setSegments(segmentsJSX)
+    }, [dragged, ordered]);
+
+    // update the cursor to be dragging while moving the floating segment
+    useEffect(() => {
+        progressBarRef.current?.classList.toggle('progressbar-dragging', dragged !== -1);
+    }, [dragged]);
+
+    // to imporve performance, we only want to update the width, not re-render the whole thing
+    useEffect(() => {
+        if (!props.recording) return
         const totalTime = props.progress * 1000 - totalRemovedTime;
         let remainingTime = totalTime;
         segments.forEach((seg, i) => {
@@ -89,197 +90,195 @@ export function ProgressBar(props: ProgressBarProps) {
             // update remaining time
             remainingTime -= (ordered[i].endTime - ordered[i].startTime);
 
-        // update the width for this segment
+            // update the width for this segment
             const htmlId = seg.props.id;
             const segmentHtml = document.getElementById(htmlId);
             if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
-      });
+        });
 
         // update the width of the expanding segment using the remaining time
         const segExapandHtml = document.getElementById('segment-expanding');
         if (segExapandHtml)
             segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`;
-        
-  }, [props.progress]);
-      
-
-
-  useEffect(() => {
-    // this useEffect fired when the videos are being rearragned to the order
-    // in this case, do nothing.
-    if (props.orderVideos) return;
-
-    const order = props.videos.length - 1;
-    // in this case, a new video is added -> push it onto ordered
-    if (order >= ordered.length) {
-      const { endTime, startTime } = props.videos.lastElement();
-      setOrdered(prevOrdered => {
-        return [...prevOrdered, { endTime, startTime , order }];
-      });
-    }
 
-    // in this case, a video is removed
-    else if (order < ordered.length) {
-        console.error('warning: video removed from parent');
-    }
-  }, [props.videos]);
-
-  useEffect(() => {
-    props.setVideos(vids => ordered.map((seg) => vids[seg.order]));
-  }, [props.orderVideos]);
-
-  useEffect(() => {
-    if (removed === -1) return;
-      // update total removed time
-      setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
-        
-      // put the element on the undo stack
-      setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
-      // remove the segment from the array
-      setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
-      // reset to default/nullish state
-      setRemoved(-1);
-  }, [removed]);
-  
-  const updateLastHover = (segId: number): CurrentHover | null => {
-    // get the segId of the segment that will become the new bounding area
-    const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
-    if (rect == null) return null
-    return {
-      index: segId,
-      minX: rect.x,
-      maxX: rect.x + rect.width,
-    }
-  }
-  
-  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
-    // don't move the videobox element
-      e.stopPropagation()
-      
-      // if recording, do nothing
-        if (props.recording) return;
+    }, [props.progress]);
 
-    // get the segment the user clicked on to be dragged
-    const clickedSegment = e.target as HTMLDivElement & EventTarget
 
-    // get the profess bar ro add event listeners
-    // don't do anything if null
-    const progressBar = progressBarRef.current
-    if (progressBar == null || clickedSegment.id === progressBar.id) return
 
-    // if holding shift key, let's remove that segment
-    // TODO: think of a way to accomodate touch -> maybe if pointerup isn't fired after x secs? or if dragged into a trash bin or smthn
-    if (e.shiftKey) {
-      const segId = parseInt(clickedSegment.id.split('-')[1]);
-      setRemoved(segId);
-      return
-    }
-    
-      if (e.ctrlKey) {
-        handleUndo();
-        return;
-    // todo: implement undo stack
-    }
+    useEffect(() => {
+        // this useEffect fired when the videos are being rearragned to the order
+        // in this case, do nothing.
+        if (props.orderVideos) return;
+
+        const order = props.videos.length - 1;
+        // in this case, a new video is added -> push it onto ordered
+        if (order >= ordered.length) {
+            const { endTime, startTime } = props.videos.lastElement();
+            setOrdered(prevOrdered => {
+                return [...prevOrdered, { endTime, startTime, order }];
+            });
+        }
+
+        // in this case, a video is removed
+        else if (order < ordered.length) {
+            console.warn('warning: video removed from parent');
+        }
+    }, [props.videos]);
 
-    const ptrId = e.pointerId;
-    progressBar.setPointerCapture(ptrId)
-
-    const rect = clickedSegment.getBoundingClientRect()
-    // id for segment is like 'segment-1' or 'segment-10', 
-    // so this works to get the id
-    const segId = parseInt(clickedSegment.id.split('-')[1])
-    // set the selected segment to be the one dragged
-    setDragged(segId)
-
-    // this is the logic for storing the lower X bound and upper X bound
-    // to know whether a swap is needed between two segments
-    let lastHover: CurrentHover = {
-      index: segId,
-      minX: rect.x,
-      maxX: rect.x + rect.width,
-    }
+    useEffect(() => {
+        props.setVideos(vids => ordered.map((seg) => vids[seg.order]));
+    }, [props.orderVideos]);
 
-    // create the div element that tracks the cursor
-    const detchedSegment = document.createElement("div")
-    initDeatchSegment(detchedSegment, rect);
-
-    const updateSegmentOrder = (event: PointerEvent): void => {
-      event.stopPropagation();
-      event.preventDefault();
-
-      // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
-      if (!progressBar.hasPointerCapture(ptrId)) {
-        placeSegmentandCleanup();
-        return;
-      }
-
-      followCursor(event, detchedSegment, rect)
-
-      const curX = event.clientX;
-      if (curX < lastHover.minX && lastHover.index > 0) {
-        swapSegments(lastHover.index, lastHover.index - 1)
-        lastHover = updateLastHover(lastHover.index - 1) ?? lastHover
-      }
-      else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) {
-        swapSegments(lastHover.index, lastHover.index + 1)
-        lastHover = updateLastHover(lastHover.index + 1) ?? lastHover
-      }
-    }
-    
-    const placeSegmentandCleanup = (event?: PointerEvent): void => {
-      event?.stopPropagation();
-      event?.preventDefault();
-      // remove the update event listener for pointermove
-      progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true }
-      // remove the floating segment from the DOM
-      detchedSegment.remove()
-      // dragged is -1 is equiv to nothing being dragged, so the normal state
-      // so this will place the segment in it's location and update the segment bar
-      setDragged(-1);
+    useEffect(() => {
+        if (removed === -1) return;
+        // update total removed time
+        setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
+
+        // put the element on the undo stack
+        setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
+        // remove the segment from the array
+        setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
+        // reset to default/nullish state
+        setRemoved(-1);
+    }, [removed]);
+
+    const updateLastHover = (segId: number): CurrentHover | null => {
+        // get the segId of the segment that will become the new bounding area
+        const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
+        if (rect == null) return null
+        return {
+            index: segId,
+            minX: rect.x,
+            maxX: rect.x + rect.width,
+        }
     }
 
+    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+        // don't move the videobox element
+        e.stopPropagation()
 
-    progressBar.addEventListener('pointermove', updateSegmentOrder)
-    progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true })
-  }
+        // if recording, do nothing
+        if (props.recording) return;
 
-  const swapSegments = (oldIndex: number, newIndex: number) => {
-      if (newIndex == null) return
-      setOrdered(prevOrdered => {
-        const cpy = [...prevOrdered]
-        cpy[oldIndex] = cpy[newIndex]
-        cpy[newIndex] = prevOrdered[oldIndex]
-        return cpy
-      })
-      setDragged(newIndex)
+        // get the segment the user clicked on to be dragged
+        const clickedSegment = e.target as HTMLDivElement & EventTarget
+
+        // get the profess bar ro add event listeners
+        // don't do anything if null
+        const progressBar = progressBarRef.current
+        if (progressBar == null || clickedSegment.id === progressBar.id) return
+
+        // if holding shift key, let's remove that segment
+        // TODO: think of a way to accomodate touch -> maybe if pointerup isn't fired after x secs? or if dragged into a trash bin or smthn
+        if (e.shiftKey) {
+            const segId = parseInt(clickedSegment.id.split('-')[1]);
+            setRemoved(segId);
+            return
+        }
+
+        if (e.ctrlKey) {
+            handleUndo();
+            return;
+        }
+
+        const ptrId = e.pointerId;
+        progressBar.setPointerCapture(ptrId)
+
+        const rect = clickedSegment.getBoundingClientRect()
+        // id for segment is like 'segment-1' or 'segment-10', 
+        // so this works to get the id
+        const segId = parseInt(clickedSegment.id.split('-')[1])
+        // set the selected segment to be the one dragged
+        setDragged(segId)
+
+        // this is the logic for storing the lower X bound and upper X bound
+        // to know whether a swap is needed between two segments
+        let lastHover: CurrentHover = {
+            index: segId,
+            minX: rect.x,
+            maxX: rect.x + rect.width,
+        }
+
+        // create the div element that tracks the cursor
+        const detchedSegment = document.createElement("div")
+        initDeatchSegment(detchedSegment, rect);
+
+        const updateSegmentOrder = (event: PointerEvent): void => {
+            event.stopPropagation();
+            event.preventDefault();
+
+            // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
+            if (!progressBar.hasPointerCapture(ptrId)) {
+                placeSegmentandCleanup();
+                return;
+            }
+
+            followCursor(event, detchedSegment);
+
+            const curX = event.clientX;
+            if (curX < lastHover.minX && lastHover.index > 0) {
+                swapSegments(lastHover.index, lastHover.index - 1)
+                lastHover = updateLastHover(lastHover.index - 1) ?? lastHover
+            }
+            else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) {
+                swapSegments(lastHover.index, lastHover.index + 1)
+                lastHover = updateLastHover(lastHover.index + 1) ?? lastHover
+            }
+        }
+
+        const placeSegmentandCleanup = (event?: PointerEvent): void => {
+            event?.stopPropagation();
+            event?.preventDefault();
+            // remove the update event listener for pointermove
+            progressBar.removeEventListener('pointermove', updateSegmentOrder);
+            // remove the floating segment from the DOM
+            detchedSegment.remove();
+            // dragged is -1 is equiv to nothing being dragged, so the normal state
+            // so this will place the segment in it's location and update the segment bar
+            setDragged(-1);
+        }
+
+
+        progressBar.addEventListener('pointermove', updateSegmentOrder)
+        progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true })
     }
 
+    const swapSegments = (oldIndex: number, newIndex: number) => {
+        if (newIndex == null) return
+        setOrdered(prevOrdered => {
+            const cpy = [...prevOrdered]
+            cpy[oldIndex] = cpy[newIndex]
+            cpy[newIndex] = prevOrdered[oldIndex]
+            return cpy
+        })
+        setDragged(newIndex)
+    }
 
-  // functions for the floating segment that tracks the cursor while grabbing it  
-  const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => {
-    dot.classList.add("segment-selected")
-    dot.style.transitionDuration = '0s';
-    dot.style.position = 'absolute';
-    dot.style.zIndex = '999';
-    dot.style.width = `${rect.width}px`;
-    dot.style.height = `${rect.height}px`;
-    dot.style.left = `${rect.x}px`;
-    dot.style.top = `${rect.y}px`;
-    dot.draggable = false;
-    document.body.append(dot)
-  }
-  const followCursor = (event: PointerEvent, dot: HTMLDivElement, rect: DOMRect): void => {
-    // event.stopPropagation()
-    // const { width, height } = dot.getBoundingClientRect()
-    const { width, height } = rect;
-    dot.style.left = `${event.clientX - width/2}px`;
-    dot.style.top = `${event.clientY - height/2}px`;
-  }
+
+    // functions for the floating segment that tracks the cursor while grabbing it  
+    const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => {
+        dot.classList.add("segment-selected");
+        dot.style.transitionDuration = '0s';
+        dot.style.position = 'absolute';
+        dot.style.zIndex = '999';
+        dot.style.width = `${rect.width}px`;
+        dot.style.height = `${rect.height}px`;
+        dot.style.left = `${rect.x}px`;
+        dot.style.top = `${rect.y}px`;
+        dot.draggable = false;
+        document.body.append(dot);
+    }
+    const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => {
+        // event.stopPropagation()
+        const { width, height } = dot.getBoundingClientRect();
+        dot.style.left = `${event.clientX - width / 2}px`;
+        dot.style.top = `${event.clientY - height / 2}px`;
+    }
 
 
     return (
-    <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
-        {segments}
-    </div>
+        <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
+            {segments}
+        </div>
     )
 }
\ 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 701277cbd..3f54a97ce 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -13,298 +13,285 @@ import { DashUploadUtils } from '../../../../server/DashUploadUtils';
 
 export interface MediaSegment {
     videoChunks: any[],
-  endTime: number,
-  startTime: number
+    endTime: number,
+    startTime: number
 }
 
 interface IRecordingViewProps {
-      setResult: (info: Upload.AccessPathInfo, trackScreen: boolean) => void
-      setDuration: (seconds: number) => void
-      id: string
+    setResult: (info: Upload.AccessPathInfo, trackScreen: boolean) => void
+    setDuration: (seconds: number) => void
+    id: string
 }
 
 const MAXTIME = 100000;
 
 export function RecordingView(props: IRecordingViewProps) {
 
-      const [recording, setRecording] = useState(false);
-      const recordingTimerRef = useRef<number>(0);
-      const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
-      const [playing, setPlaying] = useState(false);
+    const [recording, setRecording] = useState(false);
+    const recordingTimerRef = useRef<number>(0);
+    const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
+    const [playing, setPlaying] = useState(false);
     const [progress, setProgress] = useState(0);
-    
+
+    // acts as a "refresh state" to tell progressBar when to undo
     const [doUndo, setDoUndo] = useState(false);
 
     const [videos, setVideos] = useState<MediaSegment[]>([]);
     const [orderVideos, setOrderVideos] = useState<boolean>(false);
-      const videoRecorder = useRef<MediaRecorder | null>(null);
-      const videoElementRef = useRef<HTMLVideoElement | null>(null);
+    const videoRecorder = useRef<MediaRecorder | null>(null);
+    const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
-      const [finished, setFinished] = useState<boolean>(false)
-      const [trackScreen, setTrackScreen] = useState<boolean>(true)
+    const [finished, setFinished] = useState<boolean>(false)
+    const [trackScreen, setTrackScreen] = useState<boolean>(true)
 
 
 
-      const DEFAULT_MEDIA_CONSTRAINTS = {
-            video: {
-                  width: 1280,
-                  height: 720,
-            },
-            audio: {
-                  echoCancellation: true,
-                  noiseSuppression: true,
-                  sampleRate: 44100
-            }
-      }
+    const DEFAULT_MEDIA_CONSTRAINTS = {
+        video: {
+            width: 1280,
+            height: 720,
+        },
+        audio: {
+            echoCancellation: true,
+            noiseSuppression: true,
+            sampleRate: 44100
+        }
+    }
 
 
     useEffect(() => {
-          
-        // console.log('in videos useEffect', finished)
-
-          if (finished) {
-              (async () => {
+        if (finished) {
+            (async () => {
                 const inputPaths: string[] = [];
                 const videoFiles: File[] = []
                 videos.forEach(async (vid, i) => {
                     const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() });
                     videoFiles.push(videoFile);
-                    
+
                     const { name } = videoFile;
                     inputPaths.push(name)
-                  })
-                  
+                })
+
+                // upload the segments to the server and get their server access paths
                 const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
-                  .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
-                
+                    .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+
+                // concat the segments together using post call
                 const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths)
-                if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                if (!(result instanceof Error)) {
                     props.setResult(result, trackScreen)
                 } else {
                     alert("video conversion failed");
                 }
-              
-              })();
-              
-            }
+            })();
+        }
+    }, [videos])
 
+    // this will call upon the progress bar to edit videos to be in the correct order
+    useEffect(() => {
+        if (finished) setOrderVideos(true);
+    }, [finished])
 
-    }, [videos])
-  
-  useEffect(() => {
+    useEffect(() => {
+        // check if the browser supports media devices on first load
+        if (!navigator.mediaDevices) {
+            console.log('This browser does not support getUserMedia.')
+        }
+        // console.log('This device has the correct media devices.')
+    }, [])
 
-    if (finished) {
-      setOrderVideos(true);
-    }
+    useEffect(() => {
+        // get access to the video element on every render
+        videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
+    });
 
+    useEffect(() => {
+        let interval: any = null;
+        if (recording) {
+            interval = setInterval(() => {
+                setRecordingTimer(unit => unit + 1);
+            }, 10);
+        } else if (!recording && recordingTimer !== 0) {
+            clearInterval(interval);
+        }
+        return () => clearInterval(interval);
+    }, [recording])
 
-      }, [finished])
+    useEffect(() => {
+        setVideoProgressHelper(recordingTimer)
+        recordingTimerRef.current = recordingTimer;
+    }, [recordingTimer])
 
-      useEffect(() => {
-            // check if the browser supports media devices on first load
-            if (!navigator.mediaDevices) {
-                  console.log('This browser does not support getUserMedia.')
-            }
-          // console.log('This device has the correct media devices.')
-      }, [])
-
-      useEffect(() => {
-            // get access to the video element on every render
-            videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
-      })
-
-      useEffect(() => {
-            let interval: any = null;
-            if (recording) {
-                  interval = setInterval(() => {
-                        setRecordingTimer(unit => unit + 1);
-                  }, 10);
-            } else if (!recording && recordingTimer !== 0) {
-                  clearInterval(interval);
-            }
-            return () => clearInterval(interval);
-      }, [recording])
-
-      useEffect(() => {
-            setVideoProgressHelper(recordingTimer)
-            recordingTimerRef.current = recordingTimer;
-      }, [recordingTimer])
-
-      const setVideoProgressHelper = (progress: number) => {
-            const newProgress = (progress / MAXTIME) * 100;
-            setProgress(newProgress)
-      }
-      const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
-            const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
-
-            videoElementRef.current!.src = ""
-            videoElementRef.current!.srcObject = stream
-            videoElementRef.current!.muted = true
-
-            return stream
-      }
-
-      const record = async () => {
-            const stream = await startShowingStream();
-            videoRecorder.current = new MediaRecorder(stream)
-
-            // temporary chunks of video
-            let videoChunks: any = []
-
-            videoRecorder.current.ondataavailable = (event: any) => {
-                  if (event.data.size > 0) {
-                        videoChunks.push(event.data)
-                  }
-            }
+    const setVideoProgressHelper = (progress: number) => {
+        const newProgress = (progress / MAXTIME) * 100;
+        setProgress(newProgress)
+    }
+    const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
+        const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
 
-            videoRecorder.current.onstart = (event: any) => {
-                  setRecording(true);
-                  trackScreen && RecordingApi.Instance.start();
-            }
+        videoElementRef.current!.src = ""
+        videoElementRef.current!.srcObject = stream
+        videoElementRef.current!.muted = true
 
-            videoRecorder.current.onstop = () => {
-                  // if we have a last portion
-                  if (videoChunks.length > 1) {
-                        // append the current portion to the video pieces
-                        setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
-                  }
-
-                  // reset the temporary chunks
-                  videoChunks = []
-                  setRecording(false);
-                  trackScreen && RecordingApi.Instance.pause();
-            }
+        return stream
+    }
 
-            // recording paused
-            videoRecorder.current.onpause = (event: any) => {
-                  // append the current portion to the video pieces
-                  setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
+    const record = async () => {
+        const stream = await startShowingStream();
+        videoRecorder.current = new MediaRecorder(stream)
 
-                  // reset the temporary chunks
-                  videoChunks = []
-                  setRecording(false);
-                  trackScreen && RecordingApi.Instance.pause();
-            }
+        // temporary chunks of video
+        let videoChunks: any = []
 
-            videoRecorder.current.onresume = async (event: any) => {
-                  await startShowingStream();
-                  setRecording(true);
-                  trackScreen && RecordingApi.Instance.resume();
+        videoRecorder.current.ondataavailable = (event: any) => {
+            if (event.data.size > 0) {
+                videoChunks.push(event.data)
+            }
+        }
+
+        videoRecorder.current.onstart = (event: any) => {
+            setRecording(true);
+            trackScreen && RecordingApi.Instance.start();
+        }
+
+        videoRecorder.current.onstop = () => {
+            // if we have a last portion
+            if (videoChunks.length > 1) {
+                // append the current portion to the video pieces
+                setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
             }
 
-            videoRecorder.current.start(200)
-      }
+            // reset the temporary chunks
+            videoChunks = []
+            setRecording(false);
+            trackScreen && RecordingApi.Instance.pause();
+        }
+
+        // recording paused
+        videoRecorder.current.onpause = (event: any) => {
+            // append the current portion to the video pieces
+            setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
+
+            // reset the temporary chunks
+            videoChunks = []
+            setRecording(false);
+            trackScreen && RecordingApi.Instance.pause();
+        }
+
+        videoRecorder.current.onresume = async (event: any) => {
+            await startShowingStream();
+            setRecording(true);
+            trackScreen && RecordingApi.Instance.resume();
+        }
+
+        videoRecorder.current.start(200)
+    }
 
 
-  const stop = (e: React.MouseEvent) => {
+    const stop = (e: React.MouseEvent) => {
         e.stopPropagation()
         if (videoRecorder.current) {
-          setFinished(true);
-                  if (videoRecorder.current.state !== "inactive") {
-                        videoRecorder.current.stop();
-                        // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
-                  }
+            setFinished(true);
+            if (videoRecorder.current.state !== "inactive") {
+                videoRecorder.current.stop();
+                // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
             }
-      }
+        }
+    }
 
-      const pause = (e: React.MouseEvent) => {
+    const pause = (e: React.MouseEvent) => {
         e.stopPropagation()
-            if (videoRecorder.current) {
-                  if (videoRecorder.current.state === "recording") {
-                    videoRecorder.current.stop();
-                  }
+        if (videoRecorder.current) {
+            if (videoRecorder.current.state === "recording") {
+                videoRecorder.current.stop();
             }
-      }
+        }
+    }
 
-      const startOrResume =  (e: React.MouseEvent) => {
+    const startOrResume = (e: React.MouseEvent) => {
         e.stopPropagation()
-            if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
-                  record();
-            } else if (videoRecorder.current.state === "paused") {
-              videoRecorder.current.start();
-            }
-      }
+        if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+            record();
+        } else if (videoRecorder.current.state === "paused") {
+            videoRecorder.current.start();
+        }
+    }
 
     const undoPrevious = (e: React.MouseEvent) => {
         e.stopPropagation();
         setDoUndo(prev => !prev);
-            // const numVideos = videos.length
-            // setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-            // setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-            // setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
-          
-      }
-
-      const handleOnTimeUpdate = () => {
-            if (playing) {
-                  setVideoProgressHelper(videoElementRef.current!.currentTime)
-            }
-      };
+    }
 
-      const millisecondToMinuteSecond = (milliseconds: number) => {
-            const toTwoDigit = (digit: number) => {
-                  return String(digit).length == 1 ? "0" + digit : digit
-            }
-            const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
-            const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
-            return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
-      }
+    const handleOnTimeUpdate = () => {
+        if (playing) {
+            setVideoProgressHelper(videoElementRef.current!.currentTime)
+        }
+    };
+
+    const millisecondToMinuteSecond = (milliseconds: number) => {
+        const toTwoDigit = (digit: number) => {
+            return String(digit).length == 1 ? "0" + digit : digit
+        }
+        const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
+        const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
+        return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
+    }
 
     // TODO: have the undo button only appear if there is something to undo
     return (
-            <div className="recording-container">
-                  <div className="video-wrapper">
-                        <video id={`video-${props.id}`}
-                              autoPlay
-                              muted
-                              onTimeUpdate={handleOnTimeUpdate}
-                        />
-                        <div className="recording-sign">
-                              <span className="dot" />
-                              <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+        <div className="recording-container">
+            <div className="video-wrapper">
+                <video id={`video-${props.id}`}
+                    autoPlay
+                    muted
+                    onTimeUpdate={handleOnTimeUpdate}
+                />
+                <div className="recording-sign">
+                    <span className="dot" />
+                    <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+                </div>
+                <div className="controls">
+
+                    <div className="controls-inner-container">
+                        <div className="record-button-wrapper">
+                            {recording ?
+                                <button className="stop-button" onClick={pause} /> :
+                                <button className="record-button" onClick={startOrResume} />
+                            }
                         </div>
-                        <div className="controls">
-
-                              <div className="controls-inner-container">
-                                    <div className="record-button-wrapper">
-                                          {recording ?
-                                                <button className="stop-button" onClick={pause} /> :
-                                                <button className="record-button" onClick={startOrResume} />
-                                          }
-                                    </div>
-
-                                    {!recording && (videos.length > 0 ?
-
-                              <div className="options-wrapper video-edit-wrapper">
-                                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
-                                                      <MdBackspace onClick={undoPrevious} />
-                                                </IconContext.Provider>
-                                                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
-                                                      <FaCheckCircle onClick={stop} />
-                                                </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>
-                              
-                  </div>
-                  
-                                         <ProgressBar
-                      videos={videos}
-                      setVideos={setVideos}
-                      orderVideos={orderVideos}
-                      progress={progress}
-                      recording={recording}
-                      doUndo={doUndo}
-                              // playSegment={playSegment}
-                              />
-                  </div>
-            </div>)
+
+                        {!recording && (videos.length > 0 ?
+
+                            <div className="options-wrapper video-edit-wrapper">
+                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+                                    <MdBackspace onClick={undoPrevious} />
+                                </IconContext.Provider>
+                                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+                                    <FaCheckCircle onClick={stop} />
+                                </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>
+
+                </div>
+
+                <ProgressBar
+                    videos={videos}
+                    setVideos={setVideos}
+                    orderVideos={orderVideos}
+                    progress={progress}
+                    recording={recording}
+                    doUndo={doUndo}
+                // playSegment={playSegment}
+                />
+            </div>
+        </div>)
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 0badf084bed0f5337cbf9ad89f1fea924481c9b0 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Thu, 9 Jun 2022 17:45:48 -0400
Subject: add way to remove segments using touch going off screen, only show
 undo button when something is on undo stack

---
 .../views/nodes/RecordingBox/ProgressBar.tsx       | 30 +++++++++++++---------
 .../views/nodes/RecordingBox/RecordingView.tsx     |  5 +++-
 2 files changed, 22 insertions(+), 13 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 81f753e44..131d5c837 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -10,6 +10,7 @@ interface ProgressBarProps {
     progress: number,
     recording: boolean,
     doUndo: boolean,
+    setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>,
 }
 
 interface SegmentBox {
@@ -41,6 +42,8 @@ export function ProgressBar(props: ProgressBarProps) {
     // this holds the index of the videoc segment to be removed
     const [removed, setRemoved] = useState<number>(-1);
 
+    useEffect(() => props.setCanUndo?.(undoStack.length > 0), [undoStack.length]);
+
 
     // abstracted for other uses - brings back the most recently deleted segment
     const handleUndo = () => {
@@ -142,7 +145,7 @@ export function ProgressBar(props: ProgressBarProps) {
         setRemoved(-1);
     }, [removed]);
 
-    const updateLastHover = (segId: number): CurrentHover | null => {
+    const updateCurrentHover = (segId: number): CurrentHover | null => {
         // get the segId of the segment that will become the new bounding area
         const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
         if (rect == null) return null
@@ -169,9 +172,9 @@ export function ProgressBar(props: ProgressBarProps) {
         if (progressBar == null || clickedSegment.id === progressBar.id) return
 
         // if holding shift key, let's remove that segment
-        // TODO: think of a way to accomodate touch -> maybe if pointerup isn't fired after x secs? or if dragged into a trash bin or smthn
         if (e.shiftKey) {
             const segId = parseInt(clickedSegment.id.split('-')[1]);
+            console.log('removing segment shift', segId);
             setRemoved(segId);
             return
         }
@@ -193,7 +196,7 @@ export function ProgressBar(props: ProgressBarProps) {
 
         // this is the logic for storing the lower X bound and upper X bound
         // to know whether a swap is needed between two segments
-        let lastHover: CurrentHover = {
+        let currentHover: CurrentHover = {
             index: segId,
             minX: rect.x,
             maxX: rect.x + rect.width,
@@ -216,19 +219,23 @@ export function ProgressBar(props: ProgressBarProps) {
             followCursor(event, detchedSegment);
 
             const curX = event.clientX;
-            if (curX < lastHover.minX && lastHover.index > 0) {
-                swapSegments(lastHover.index, lastHover.index - 1)
-                lastHover = updateLastHover(lastHover.index - 1) ?? lastHover
+            if (curX < currentHover.minX && currentHover.index > 0) {
+                swapSegments(currentHover.index, currentHover.index - 1)
+                currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover
             }
-            else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) {
-                swapSegments(lastHover.index, lastHover.index + 1)
-                lastHover = updateLastHover(lastHover.index + 1) ?? lastHover
+            else if (curX > currentHover.maxX && currentHover.index < segments.length - 1) {
+                swapSegments(currentHover.index, currentHover.index + 1)
+                currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover
             }
         }
 
         const placeSegmentandCleanup = (event?: PointerEvent): void => {
             event?.stopPropagation();
             event?.preventDefault();
+            // if they put the segment outside of the bounds, remove it
+            if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight))
+                setRemoved(currentHover.index);
+            
             // remove the update event listener for pointermove
             progressBar.removeEventListener('pointermove', updateSegmentOrder);
             // remove the floating segment from the DOM
@@ -238,9 +245,8 @@ export function ProgressBar(props: ProgressBarProps) {
             setDragged(-1);
         }
 
-
-        progressBar.addEventListener('pointermove', updateSegmentOrder)
-        progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true })
+        progressBar.addEventListener('pointermove', updateSegmentOrder);
+        progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true });
     }
 
     const swapSegments = (oldIndex: number, newIndex: number) => {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 3f54a97ce..7cff54ff2 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -35,6 +35,8 @@ export function RecordingView(props: IRecordingViewProps) {
 
     // acts as a "refresh state" to tell progressBar when to undo
     const [doUndo, setDoUndo] = useState(false);
+    // whether an undo can occur or not
+    const [canUndo, setCanUndo] = useState(false);
 
     const [videos, setVideos] = useState<MediaSegment[]>([]);
     const [orderVideos, setOrderVideos] = useState<boolean>(false);
@@ -263,7 +265,7 @@ export function RecordingView(props: IRecordingViewProps) {
                         {!recording && (videos.length > 0 ?
 
                             <div className="options-wrapper video-edit-wrapper">
-                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: {display: canUndo ? 'inherit' : 'none'} }}>
                                     <MdBackspace onClick={undoPrevious} />
                                 </IconContext.Provider>
                                 <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
@@ -290,6 +292,7 @@ export function RecordingView(props: IRecordingViewProps) {
                     progress={progress}
                     recording={recording}
                     doUndo={doUndo}
+                    setCanUndo={setCanUndo}
                 // playSegment={playSegment}
                 />
             </div>
-- 
cgit v1.2.3-70-g09d2


From 23f6651153333a9979b37fe725c54080babe0133 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Fri, 10 Jun 2022 12:29:49 -0400
Subject: cleanup code from performance. stop streams after video is done.

---
 .../views/nodes/RecordingBox/ProgressBar.tsx       |  2 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     | 48 +++++++---------------
 2 files changed, 15 insertions(+), 35 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index dfe57db3f..493069394 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -68,7 +68,7 @@ export function ProgressBar(props: ProgressBarProps) {
         progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);
 
         if (props.recording)
-            setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: '0px;' }}>{props.videos.length + 1}</div>]);
+            setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'fit-content' }}>{props.videos.length + 1}</div>]);
     }, [props.recording])
 
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index e8737dce2..a8cc82ec0 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -91,22 +91,16 @@ export function RecordingView(props: IRecordingViewProps) {
 
     // this will call upon the progress bar to edit videos to be in the correct order
     useEffect(() => {
-        if (finished) setOrderVideos(true);
+        finished && setOrderVideos(true);
     }, [finished])
 
     useEffect(() => {
         // check if the browser supports media devices on first load
         if (!navigator.mediaDevices) {
-            console.log('This browser does not support getUserMedia.')
+            console.warn('This browser does not support getUserMedia.')
         }
-        // console.log('This device has the correct media devices.')
     }, [])
 
-    useEffect(() => {
-        // get access to the video element on every render
-        videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
-    });
-
     useEffect(() => {
         let interval: any = null;
         if (recording) {
@@ -128,6 +122,7 @@ export function RecordingView(props: IRecordingViewProps) {
         const newProgress = (progress / MAXTIME) * 100;
         setProgress(newProgress)
     }
+
     const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
         const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
 
@@ -139,8 +134,8 @@ export function RecordingView(props: IRecordingViewProps) {
     }
 
     const record = async () => {
-        const stream = await startShowingStream();
-        videoRecorder.current = new MediaRecorder(stream)
+        // don't need to start a new stream every time we start recording a new segment
+        if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream())
 
         // temporary chunks of video
         let videoChunks: any = []
@@ -169,23 +164,6 @@ export function RecordingView(props: IRecordingViewProps) {
             trackScreen && RecordingApi.Instance.pause();
         }
 
-        // recording paused
-        // videoRecorder.current.onpause = (event: any) => {
-        //     // append the current portion to the video pieces
-        //     setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
-
-        //     // reset the temporary chunks
-        //     videoChunks = []
-        //     setRecording(false);
-        //     trackScreen && RecordingApi.Instance.pause();
-        // }
-
-        // videoRecorder.current.onresume = async (event: any) => {
-        //     await startShowingStream();
-        //     setRecording(true);
-        //     trackScreen && RecordingApi.Instance.resume();
-        // }
-
         videoRecorder.current.start(200)
     }
 
@@ -193,11 +171,16 @@ export function RecordingView(props: IRecordingViewProps) {
     const stop = (e: React.PointerEvent) => {
         e.stopPropagation();
         if (videoRecorder.current) {
-            setFinished(true);
             if (videoRecorder.current.state !== "inactive") {
                 videoRecorder.current.stop();
-                // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
             }
+
+            // this will call upon progessbar to update videos to be in the correct order
+            setFinished(true);
+
+            // end the streams (audio/video) to remove recording icon
+            const stream = videoElementRef.current!.srcObject;
+            stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
         }
     }
 
@@ -213,11 +196,7 @@ export function RecordingView(props: IRecordingViewProps) {
     const startOrResume = (e: React.PointerEvent) => {
         // the code to start or resume does not get triggered if we start dragging the button
         setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-            if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
-                record();
-            } else if (videoRecorder.current.state === "paused") {
-                videoRecorder.current.start();
-            }
+            (!videoRecorder.current || videoRecorder.current.state === "inactive") && record();
             return true; // cancels propagation to documentView to avoid selecting it.
         }, false, false);
     }
@@ -245,6 +224,7 @@ export function RecordingView(props: IRecordingViewProps) {
                     autoPlay
                     muted
                     onTimeUpdate={handleOnTimeUpdate}
+                    ref={videoElementRef}
                 />
                 <div className="recording-sign">
                     <span className="dot" />
-- 
cgit v1.2.3-70-g09d2


From 0f70c97be61235f57272b8b074b0c2c7399c0d4d Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Fri, 10 Jun 2022 12:40:44 -0400
Subject: small cleanup - remove imports, more descriptive logs/comments, etc.

---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 36 ++++++----------------
 1 file changed, 9 insertions(+), 27 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index a8cc82ec0..138e72274 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -1,15 +1,14 @@
 import * as React from 'react';
 import "./RecordingView.scss";
-import { ReactElement, useCallback, useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
 import { ProgressBar } from "./ProgressBar"
 import { MdBackspace } from 'react-icons/md';
 import { FaCheckCircle } from 'react-icons/fa';
 import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
-import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
 import { RecordingApi } from '../../../util/RecordingApi';
-import { DashUploadUtils } from '../../../../server/DashUploadUtils';
 
 export interface MediaSegment {
     videoChunks: any[],
@@ -63,28 +62,17 @@ export function RecordingView(props: IRecordingViewProps) {
 
     useEffect(() => {
         if (finished) {
+            // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
             (async () => {
-                const inputPaths: string[] = [];
-                const videoFiles: File[] = []
-                videos.forEach(async (vid, i) => {
-                    const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() });
-                    videoFiles.push(videoFile);
-
-                    const { name } = videoFile;
-                    inputPaths.push(name)
-                })
+                const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
 
                 // upload the segments to the server and get their server access paths
                 const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
                     .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
 
                 // concat the segments together using post call
-                const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths)
-                if (!(result instanceof Error)) {
-                    props.setResult(result, trackScreen)
-                } else {
-                    alert("video conversion failed");
-                }
+                const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
+                !(result instanceof Error) ? props.setResult(result, trackScreen) : console.error("video conversion failed");
             })();
         }
     }, [videos])
@@ -94,12 +82,8 @@ export function RecordingView(props: IRecordingViewProps) {
         finished && setOrderVideos(true);
     }, [finished])
 
-    useEffect(() => {
-        // check if the browser supports media devices on first load
-        if (!navigator.mediaDevices) {
-            console.warn('This browser does not support getUserMedia.')
-        }
-    }, [])
+    // check if the browser supports media devices on first load
+    useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, [])
 
     useEffect(() => {
         let interval: any = null;
@@ -141,9 +125,7 @@ export function RecordingView(props: IRecordingViewProps) {
         let videoChunks: any = []
 
         videoRecorder.current.ondataavailable = (event: any) => {
-            if (event.data.size > 0) {
-                videoChunks.push(event.data)
-            }
+            if (event.data.size > 0) videoChunks.push(event.data)
         }
 
         videoRecorder.current.onstart = (event: any) => {
-- 
cgit v1.2.3-70-g09d2


From 1688af3de54419029773fb85b78bca4500f7f0de Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Fri, 10 Jun 2022 15:41:38 -0400
Subject: big bug problems with recording. some reason interacting with the
 canvas is making the recording inactive - i think :/

---
 src/client/util/RecordingApi.ts                    | 153 +++++++++++++--------
 .../views/nodes/RecordingBox/ProgressBar.tsx       |   8 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  12 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  99 ++++++++-----
 4 files changed, 166 insertions(+), 106 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index 021feee9a..ab6935e3b 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -3,8 +3,6 @@ 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,
@@ -13,17 +11,17 @@ type Movement = {
     scale: number,
 }
 
-type Presentation = {
-    movements: Array<Movement> | null
-    meta: Object,
+export type Presentation = {
+  movements: Movement[] | null,
+  totalTime: number,
+  meta: any,
 }
 
 export class RecordingApi {
 
-    private static NULL_PRESENTATION: Presentation = {
-        movements: null,
-        meta: {},
-    }
+  private static get NULL_PRESENTATION(): Presentation {
+    return { movements: null, meta: {}, totalTime: -1,}
+  }
 
     // instance variables
     private currentPresentation: Presentation;
@@ -50,6 +48,8 @@ export class RecordingApi {
         // for now, set playFFView
         this.playFFView = null;
         this.timers = null;
+      
+      // put a pointerdown event on the doucment to see what the target
     }
 
     // little helper :)
@@ -59,8 +59,9 @@ export class RecordingApi {
 
     public start = (meta?: Object): Error | undefined => {
         // check if already init a presentation
-        if (!this.isInitPresenation) {
-            console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
+      if (!this.isInitPresenation) {
+          console.log(this.currentPresentation)
+            console.trace('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
             return new Error('[recordingApi.ts] start()')
         }
 
@@ -80,50 +81,61 @@ export class RecordingApi {
         this.isRecording = true
     }
 
-    public clear = (): Error | Presentation => {
+  /* stops the video and returns the presentatation; if no presentation, returns undefined */  
+  public getPresentation = (): undefined | Presentation => {
         // TODO: maybe archive the data?
-        if (this.isRecording) {
-            console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first')
-            return new Error('[recordingApi.ts] clear()')
-        }
-
-        // update the presentation mode
-        Doc.UserDoc().presentationMode = 'none'
-        // set the previus recording view to the play view
-        this.playFFView = this.recordingFFView
-
-        const presCopy = { ...this.currentPresentation }
-
-        // clear presenation data
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        // clear isRecording
-        this.isRecording = false
-        // clear absoluteStart
-        this.absoluteStart = -1
-        // clear the disposeFunc
-        this.removeRecordingFFView()
-
-        return presCopy;
+    if (this.isRecording) console.warn('[recordingApi.ts] getPresentation() : currently recording presentation.');
+
+    // update the presentation mode
+    Doc.UserDoc().presentationMode = 'none';
+    // set the previus recording view to the play view
+    this.playFFView = this.recordingFFView;
+
+    // ensure we add the endTime now that they are done recording    
+    return { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+  }
+
+  public stop = (): void => {
+    // make is recording false
+    this.isRecording = false
+  }
+
+  public clear = (): void => {
+    // clear presenation data
+    this.currentPresentation = RecordingApi.NULL_PRESENTATION
+    // clear isRecording
+    this.isRecording = false
+    // clear absoluteStart
+    this.absoluteStart = -1
+    // clear the disposeFunc
+    this.removeRecordingFFView()
+  }
+  
+  
+    // call on dispose function to stop tracking movements
+    public removeRecordingFFView = (): void => {
+        this.disposeFunc?.();
+        this.disposeFunc = null;
     }
 
-    public pause = (): Error | undefined => {
-        if (this.isInitPresenation) {
-            console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
-            return new Error('[recordingApi.ts] pause(): no presentation')
-        }
-        // don't allow track movments
-        this.isRecording = false
-
-        // set adjust absoluteStart to add the time difference
-        const timestamp = new Date().getTime()
-        this.absoluteStart = timestamp - this.absoluteStart
-    }
+    // public pause = (): Error | undefined => {
+    //     if (this.isInitPresenation) {
+    //         console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
+    //         return new Error('[recordingApi.ts] pause(): no presentation')
+    //     }
+    //     // don't allow track movments
+    //     this.isRecording = false
+
+    //     // set adjust absoluteStart to add the time difference
+    //     const timestamp = new Date().getTime()
+    //     this.absoluteStart = timestamp - this.absoluteStart
+    // }
 
-    public resume = () => {
-        this.isRecording = true
-        // set absoluteStart to the difference in time
-        this.absoluteStart = new Date().getTime() - this.absoluteStart
-    }
+    // public resume = () => {
+    //     this.isRecording = true
+    //     // set absoluteStart to the difference in time
+    //     this.absoluteStart = new Date().getTime() - this.absoluteStart
+    // }
 
     private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
         // ensure we are recording
@@ -134,6 +146,8 @@ export class RecordingApi {
         if (this.isInitPresenation) {
             return new Error('[recordingApi.ts] trackMovements(): no presentation')
         }
+      
+      console.log('track movment')
 
         // get the time
         const time = new Date().getTime() - this.absoluteStart
@@ -164,12 +178,6 @@ export class RecordingApi {
         this.recordingFFView = view;
     }
 
-    // call on dispose function to stop tracking movements
-    public removeRecordingFFView = (): void => {
-        this.disposeFunc?.();
-        this.disposeFunc = null;
-    }
-
     // TODO: extract this into different class with pause and resume recording
     // TODO: store the FFview with the movements
     private playFFView: CollectionFreeFormView | null;
@@ -250,6 +258,37 @@ export class RecordingApi {
             }, timeDiff)
         })
     }
+  
+  // make a public method that concatenates the movements of the an array of presentations into one array
+  // TODO: consider the meta data of the presentations
+  public concatPresentations = (presentations: Presentation[]): Presentation => {
+    console.info(presentations);
+    if (presentations.length === 0) return RecordingApi.NULL_PRESENTATION;
+    const firstPresentation = presentations[0];
+
+    let sumTime = presentations[0].totalTime;
+    let combinedPresentations = { ...firstPresentation }
+    presentations.forEach((presentation, i) => {
+      // already consider the first presentation
+      if (i === 0) return;
+
+      const { movements, totalTime } = presentation;
+      if (movements === null) return;
+
+      // add the summed time to the movements
+      const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
+      // concat the movements already in the combined presentation with these new ones
+      const newMovements = [...combinedPresentations.movements || [], ...addedTimeMovements];
+
+      combinedPresentations = { ...combinedPresentations, movements: newMovements }
+
+      // update the totalTime
+      sumTime += totalTime;
+    });
+
+    // return the combined presentation with the updated total summed time
+    return { ...combinedPresentations, totalTime: sumTime };
+  }
 
     // Unfinished code for tracing multiple free form views
     // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 493069394..1bb2b7c84 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -161,12 +161,12 @@ export function ProgressBar(props: ProgressBarProps) {
     }
 
     // pointerdown event for the progress bar
-    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
-        // don't move the videobox element
-        e.stopPropagation()
+  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+           // don't move the videobox element
+    e.stopPropagation();  
 
         // if recording, do nothing
-        if (props.recording) return;
+      if (props.recording) return;
 
         // get the segment the user clicked on to be dragged
         const clickedSegment = e.target as HTMLDivElement & EventTarget
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 5e97e3eb5..6fe67b6db 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -8,8 +8,8 @@ import { FieldView } from "../FieldView";
 import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 import { DocumentType } from "../../../documents/DocumentTypes";
-import { RecordingApi } from "../../../util/RecordingApi";
-import { Doc, FieldsSym } from "../../../../fields/Doc";
+import { Presentation } from "../../../util/RecordingApi";
+import { Doc } from "../../../../fields/Doc";
 import { Id } from "../../../../fields/FieldSymbols";
 
 
@@ -21,7 +21,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       private _ref: React.RefObject<HTMLDivElement> = React.createRef();
 
       constructor(props: any) {
-            super(props);
+        super(props);
       }
 
       componentDidMount() {
@@ -38,7 +38,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       @action
-      setResult = (info: Upload.AccessPathInfo, trackScreen: boolean) => {
+      setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
             this.result = info
             this.dataDoc.type = DocumentType.VID;
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
@@ -47,9 +47,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
             // stringify the presenation and store it
-            if (trackScreen) {
-                  this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
-            }
+            presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
       }
 
       render() {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 138e72274..208eaf45a 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -8,16 +8,17 @@ import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
 import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-import { RecordingApi } from '../../../util/RecordingApi';
+import { Presentation, RecordingApi } from '../../../util/RecordingApi';
 
 export interface MediaSegment {
     videoChunks: any[],
     endTime: number,
-    startTime: number
+  startTime: number,
+  presenation?: Presentation,
 }
 
 interface IRecordingViewProps {
-    setResult: (info: Upload.AccessPathInfo, trackScreen: boolean) => void
+    setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
     setDuration: (seconds: number) => void
     id: string
 }
@@ -58,13 +59,18 @@ export function RecordingView(props: IRecordingViewProps) {
             sampleRate: 44100
         }
     }
+  
+  useEffect(() => console.info('progess', progress), [progress])
 
 
     useEffect(() => {
-        if (finished) {
+      if (finished) {
+        // make the total presentation that'll match the concatted video
+        const concatPres = trackScreen ? RecordingApi.Instance.concatPresentations(videos.map(v => v.presenation as Presentation)) : undefined;
+
             // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
             (async () => {
-                const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
+              const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
 
                 // upload the segments to the server and get their server access paths
                 const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
@@ -72,7 +78,7 @@ export function RecordingView(props: IRecordingViewProps) {
 
                 // concat the segments together using post call
                 const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
-                !(result instanceof Error) ? props.setResult(result, trackScreen) : console.error("video conversion failed");
+                !(result instanceof Error) ? props.setResult(result, concatPres) : console.error("video conversion failed");
             })();
         }
     }, [videos])
@@ -133,53 +139,70 @@ export function RecordingView(props: IRecordingViewProps) {
             trackScreen && RecordingApi.Instance.start();
         }
 
-        videoRecorder.current.onstop = () => {
+      videoRecorder.current.onstop = () => {
+          RecordingApi.Instance.stop();
             // if we have a last portion
-            if (videoChunks.length > 1) {
+          if (videoChunks.length > 1) {
                 // append the current portion to the video pieces
-                setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }])
+                  setVideos(videos => [...videos, {
+                    videoChunks: videoChunks,
+                    endTime: recordingTimerRef.current,
+                    startTime: videos?.lastElement()?.endTime || 0,
+                    // RecordingApi.stop() will return undefined if no track screen
+                    presenation: RecordingApi.Instance.getPresentation()
+                  }])
+                  // now that we got the presentation data, we can clear for the next segment to be recorded
+                  RecordingApi.Instance.clear();
             }
 
-            // reset the temporary chunks
-            videoChunks = []
-            setRecording(false);
-            trackScreen && RecordingApi.Instance.pause();
+          // reset the temporary chunks
+          videoChunks = []
+          setRecording(false);
         }
 
         videoRecorder.current.start(200)
     }
 
 
-    const stop = (e: React.PointerEvent) => {
-        e.stopPropagation();
-        if (videoRecorder.current) {
-            if (videoRecorder.current.state !== "inactive") {
-                videoRecorder.current.stop();
-            }
-
-            // this will call upon progessbar to update videos to be in the correct order
-            setFinished(true);
-
-            // end the streams (audio/video) to remove recording icon
-            const stream = videoElementRef.current!.srcObject;
-            stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
-        }
+  const finish = (e: React.PointerEvent) => {
+    e.stopPropagation();
+    console.log('finish', videoRecorder.current)
+      // if inactive, then we're done recording all the segments
+    if (videoRecorder.current && videoRecorder.current.state !== "inactive") {
+        console.log('stopping recorder', videoRecorder.current?.state)
+        
+        
+        
+        // call stop on the video recorder
+        videoRecorder.current?.stop();
+
+        // end the streams (audio/video) to remove recording icon
+        const stream = videoElementRef.current!.srcObject;
+        stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
+          
+        // clear the recoringApi - this is done in the stop method
+        // RecordingApi.Instance.clear();
+
+        // this will call upon progessbar to update videos to be in the correct order
+        console.log('setFinished to true', finished)
+        setFinished(true);
+      }
     }
 
     const pause = (e: React.PointerEvent) => {
-        e.stopPropagation()
-        if (videoRecorder.current) {
-            if (videoRecorder.current.state === "recording") {
-                videoRecorder.current.stop();
-            }
-        }
+      e.stopPropagation()
+      // if recording, then this is just a new segment
+      videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
     }
 
-    const startOrResume = (e: React.PointerEvent) => {
+    const start = (e: React.PointerEvent) => {
         // the code to start or resume does not get triggered if we start dragging the button
         setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-            (!videoRecorder.current || videoRecorder.current.state === "inactive") && record();
-            return true; // cancels propagation to documentView to avoid selecting it.
+          if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+            record();
+            // trackScreen && RecordingApi.Instance.start();
+          }
+          return true; // cancels propagation to documentView to avoid selecting it.
         }, false, false);
     }
 
@@ -218,7 +241,7 @@ export function RecordingView(props: IRecordingViewProps) {
                         <div className="record-button-wrapper">
                             {recording ?
                                 <button className="stop-button" onPointerDown={pause} /> :
-                                <button className="record-button" onPointerDown={startOrResume} />
+                                <button className="record-button" onPointerDown={start} />
                             }
                         </div>
 
@@ -229,7 +252,7 @@ export function RecordingView(props: IRecordingViewProps) {
                                     <MdBackspace onPointerDown={undoPrevious} />
                                 </IconContext.Provider>
                                 <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
-                                    <FaCheckCircle onPointerDown={stop} />
+                                    <FaCheckCircle onPointerDown={finish} />
                                 </IconContext.Provider>
                             </div>
 
-- 
cgit v1.2.3-70-g09d2


From f95ea8c5ea8212fe56125822e509f1c7d999133c Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Sun, 12 Jun 2022 10:01:14 -0400
Subject: fix small bug with video recorder. need to work on recording api bug

---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 23 ++++++++--------------
 1 file changed, 8 insertions(+), 15 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 208eaf45a..380ac050d 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -164,29 +164,22 @@ export function RecordingView(props: IRecordingViewProps) {
     }
 
 
-  const finish = (e: React.PointerEvent) => {
-    e.stopPropagation();
-    console.log('finish', videoRecorder.current)
-      // if inactive, then we're done recording all the segments
-    if (videoRecorder.current && videoRecorder.current.state !== "inactive") {
-        console.log('stopping recorder', videoRecorder.current?.state)
-        
-        
-        
-        // call stop on the video recorder
-        videoRecorder.current?.stop();
+    // if this is called, then we're done recording all the segments
+    const finish = (e: React.PointerEvent) => {
+        e.stopPropagation();
+
+        // call stop on the video recorder if active
+        videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
 
         // end the streams (audio/video) to remove recording icon
         const stream = videoElementRef.current!.srcObject;
         stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
           
-        // clear the recoringApi - this is done in the stop method
-        // RecordingApi.Instance.clear();
+        // clear the recoringApi
+        RecordingApi.Instance.clear();
 
         // this will call upon progessbar to update videos to be in the correct order
-        console.log('setFinished to true', finished)
         setFinished(true);
-      }
     }
 
     const pause = (e: React.PointerEvent) => {
-- 
cgit v1.2.3-70-g09d2


From daeed5592522c01ea2c38a438c24d914f117530b Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Mon, 13 Jun 2022 12:18:55 -0400
Subject: small fix on recordingApi to work with segments. now tracks
 accurately

---
 src/client/util/RecordingApi.ts                    | 53 ++++++++++++----------
 .../views/nodes/RecordingBox/RecordingView.tsx     | 27 ++++++-----
 2 files changed, 44 insertions(+), 36 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index ab6935e3b..009652f6e 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -25,7 +25,7 @@ export class RecordingApi {
 
     // instance variables
     private currentPresentation: Presentation;
-    private isRecording: boolean;
+    private tracking: boolean;
     private absoluteStart: number;
 
 
@@ -38,7 +38,7 @@ export class RecordingApi {
 
         // init the instance variables
         this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        this.isRecording = false;
+        this.tracking = false;
         this.absoluteStart = -1;
 
         // used for tracking movements in the view frame
@@ -57,58 +57,61 @@ export class RecordingApi {
         return this.currentPresentation.movements === null
     }
 
-    public start = (meta?: Object): Error | undefined => {
+    public start = (meta?: Object) => {
         // check if already init a presentation
       if (!this.isInitPresenation) {
           console.log(this.currentPresentation)
-            console.trace('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
-            return new Error('[recordingApi.ts] start()')
+          console.trace('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
         }
 
         // update the presentation mode
-        Doc.UserDoc().presentationMode = 'recording'
+      Doc.UserDoc().presentationMode = 'recording';
 
         // (1a) get start date for presenation
-        const startDate = new Date()
+      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.movements = []
-        // (4) set isRecording true to allow trackMovements
-        this.isRecording = true
+        // (4) set tracking true to allow trackMovements
+        this.tracking = true
     }
 
   /* stops the video and returns the presentatation; if no presentation, returns undefined */  
-  public getPresentation = (): undefined | Presentation => {
+  public* yieldPresentation(clearData: boolean = true): Generator<Presentation | null> {
         // TODO: maybe archive the data?
-    if (this.isRecording) console.warn('[recordingApi.ts] getPresentation() : currently recording presentation.');
+    // if (this.tracking) console.warn('[recordingApi.ts] getPresentation() : currently recording presentation.');
 
     // update the presentation mode
-    Doc.UserDoc().presentationMode = 'none';
+    // Doc.UserDoc().presentationMode = 'none';
     // set the previus recording view to the play view
     this.playFFView = this.recordingFFView;
 
     // ensure we add the endTime now that they are done recording    
-    return { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+    yield { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+
+    // reset the current presentation
+    clearData && this.clear();
   }
 
   public stop = (): void => {
-    // make is recording false
-    this.isRecording = false
+    // make is tracking false
+    this.tracking = false
   }
 
   public clear = (): void => {
+    // clear the disposeFunc if we are done (not recording)
+    if (!this.tracking)
+      this.removeRecordingFFView()
     // clear presenation data
     this.currentPresentation = RecordingApi.NULL_PRESENTATION
     // clear isRecording
-    this.isRecording = false
+    // this.tracking = false
     // clear absoluteStart
     this.absoluteStart = -1
-    // clear the disposeFunc
-    this.removeRecordingFFView()
   }
   
   
@@ -124,7 +127,7 @@ export class RecordingApi {
     //         return new Error('[recordingApi.ts] pause(): no presentation')
     //     }
     //     // don't allow track movments
-    //     this.isRecording = false
+    //     this.tracking = false
 
     //     // set adjust absoluteStart to add the time difference
     //     const timestamp = new Date().getTime()
@@ -132,14 +135,14 @@ export class RecordingApi {
     // }
 
     // public resume = () => {
-    //     this.isRecording = true
+    //     this.tracking = true
     //     // set absoluteStart to the difference in time
     //     this.absoluteStart = new Date().getTime() - this.absoluteStart
     // }
 
     private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
         // ensure we are recording
-        if (!this.isRecording) {
+        if (!this.tracking) {
             return new Error('[recordingApi.ts] trackMovements()')
         }
         // check to see if the presetation is init
@@ -171,7 +174,7 @@ export class RecordingApi {
         // set the reaction to track the movements
         this.disposeFunc = reaction(
             () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
-            (res) => (res.x !== -1 && res.y !== -1 && this.isRecording) && this.trackMovements(res.x, res.y, res.scale)
+            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
         )
 
         // for now, set the most recent recordingFFView to the playFFView
@@ -262,11 +265,11 @@ export class RecordingApi {
   // make a public method that concatenates the movements of the an array of presentations into one array
   // TODO: consider the meta data of the presentations
   public concatPresentations = (presentations: Presentation[]): Presentation => {
-    console.info(presentations);
+    console.table(presentations);
     if (presentations.length === 0) return RecordingApi.NULL_PRESENTATION;
     const firstPresentation = presentations[0];
 
-    let sumTime = presentations[0].totalTime;
+    let sumTime = firstPresentation.totalTime;
     let combinedPresentations = { ...firstPresentation }
     presentations.forEach((presentation, i) => {
       // already consider the first presentation
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 380ac050d..be9f342bb 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -14,7 +14,7 @@ export interface MediaSegment {
     videoChunks: any[],
     endTime: number,
   startTime: number,
-  presenation?: Presentation,
+  presentation?: Presentation,
 }
 
 interface IRecordingViewProps {
@@ -60,13 +60,14 @@ export function RecordingView(props: IRecordingViewProps) {
         }
     }
   
-  useEffect(() => console.info('progess', progress), [progress])
+  // useEffect(() => console.info('progress', progress), [progress])
 
 
     useEffect(() => {
       if (finished) {
         // make the total presentation that'll match the concatted video
-        const concatPres = trackScreen ? RecordingApi.Instance.concatPresentations(videos.map(v => v.presenation as Presentation)) : undefined;
+        const concatPres = RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+        console.log('concatPres', concatPres);
 
             // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
             (async () => {
@@ -91,7 +92,8 @@ export function RecordingView(props: IRecordingViewProps) {
     // check if the browser supports media devices on first load
     useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, [])
 
-    useEffect(() => {
+  useEffect(() => {
+      console.log('recording useEffect', recording)
         let interval: any = null;
         if (recording) {
             interval = setInterval(() => {
@@ -136,23 +138,26 @@ export function RecordingView(props: IRecordingViewProps) {
 
         videoRecorder.current.onstart = (event: any) => {
             setRecording(true);
+            // trackScreen && RecordingApi.Instance.start();
             trackScreen && RecordingApi.Instance.start();
         }
 
       videoRecorder.current.onstop = () => {
-          RecordingApi.Instance.stop();
+          // RecordingApi.Instance.stop();
             // if we have a last portion
-          if (videoChunks.length > 1) {
+        if (videoChunks.length > 1) {
+          const presentation = RecordingApi.Instance.yieldPresentation().next().value || undefined
+          console.log('presenation yield', JSON.parse(JSON.stringify(presentation)))
                 // append the current portion to the video pieces
                   setVideos(videos => [...videos, {
                     videoChunks: videoChunks,
                     endTime: recordingTimerRef.current,
                     startTime: videos?.lastElement()?.endTime || 0,
                     // RecordingApi.stop() will return undefined if no track screen
-                    presenation: RecordingApi.Instance.getPresentation()
+                    presentation
                   }])
                   // now that we got the presentation data, we can clear for the next segment to be recorded
-                  RecordingApi.Instance.clear();
+                  // RecordingApi.Instance.clear();
             }
 
           // reset the temporary chunks
@@ -193,7 +198,7 @@ export function RecordingView(props: IRecordingViewProps) {
         setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
           if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
             record();
-            // trackScreen && RecordingApi.Instance.start();
+            // trackScreen &&
           }
           return true; // cancels propagation to documentView to avoid selecting it.
         }, false, false);
@@ -204,7 +209,7 @@ export function RecordingView(props: IRecordingViewProps) {
         setDoUndo(prev => !prev);
     }
 
-    const handleOnTimeUpdate = () => playing && setVideoProgressHelper(videoElementRef.current!.currentTime);
+  const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
 
     const millisecondToMinuteSecond = (milliseconds: number) => {
         const toTwoDigit = (digit: number) => {
@@ -221,7 +226,7 @@ export function RecordingView(props: IRecordingViewProps) {
                 <video id={`video-${props.id}`}
                     autoPlay
                     muted
-                    onTimeUpdate={handleOnTimeUpdate}
+                    onTimeUpdate={() => handleOnTimeUpdate()}
                     ref={videoElementRef}
                 />
                 <div className="recording-sign">
-- 
cgit v1.2.3-70-g09d2


From a3faf09f5a59f1500894c629cb0052e2cf7413e4 Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Mon, 13 Jun 2022 13:00:33 -0400
Subject: support for not checking the track screen movements

---
 src/client/util/RecordingApi.ts                    |  70 ++--
 .../views/nodes/RecordingBox/RecordingView.tsx     | 442 ++++++++++-----------
 2 files changed, 248 insertions(+), 264 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index 009652f6e..12c1654a2 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -58,13 +58,7 @@ export class RecordingApi {
     }
 
     public start = (meta?: Object) => {
-        // check if already init a presentation
-      if (!this.isInitPresenation) {
-          console.log(this.currentPresentation)
-          console.trace('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
-        }
-
-        // update the presentation mode
+      // update the presentation mode
       Doc.UserDoc().presentationMode = 'recording';
 
         // (1a) get start date for presenation
@@ -81,12 +75,10 @@ export class RecordingApi {
     }
 
   /* stops the video and returns the presentatation; if no presentation, returns undefined */  
-  public* yieldPresentation(clearData: boolean = true): Generator<Presentation | null> {
-        // TODO: maybe archive the data?
-    // if (this.tracking) console.warn('[recordingApi.ts] getPresentation() : currently recording presentation.');
+  public * yieldPresentation(clearData: boolean = true): Generator<Presentation | null> {
+    // if no presentation or done tracking, return null
+    if (!this.isInitPresenation || !this.tracking) return null;
 
-    // update the presentation mode
-    // Doc.UserDoc().presentationMode = 'none';
     // set the previus recording view to the play view
     this.playFFView = this.recordingFFView;
 
@@ -103,9 +95,12 @@ export class RecordingApi {
   }
 
   public clear = (): void => {
-    // clear the disposeFunc if we are done (not recording)
-    if (!this.tracking)
-      this.removeRecordingFFView()
+    // clear the disposeFunc if we are done (not tracking)
+    if (!this.tracking) {
+      this.removeRecordingFFView();
+      // update the presentation mode now that we are done tracking
+      Doc.UserDoc().presentationMode = 'none';
+    }
     // clear presenation data
     this.currentPresentation = RecordingApi.NULL_PRESENTATION
     // clear isRecording
@@ -114,7 +109,6 @@ export class RecordingApi {
     this.absoluteStart = -1
   }
   
-  
     // call on dispose function to stop tracking movements
     public removeRecordingFFView = (): void => {
         this.disposeFunc?.();
@@ -150,7 +144,8 @@ export class RecordingApi {
             return new Error('[recordingApi.ts] trackMovements(): no presentation')
         }
       
-      console.log('track movment')
+      // TO FIX: bob
+      // console.debug('track movment')
 
         // get the time
         const time = new Date().getTime() - this.absoluteStart
@@ -262,35 +257,30 @@ export class RecordingApi {
         })
     }
   
-  // make a public method that concatenates the movements of the an array of presentations into one array
-  // TODO: consider the meta data of the presentations
+  // method that concatenates an array of presentatations into one
   public concatPresentations = (presentations: Presentation[]): Presentation => {
-    console.table(presentations);
-    if (presentations.length === 0) return RecordingApi.NULL_PRESENTATION;
-    const firstPresentation = presentations[0];
-
-    let sumTime = firstPresentation.totalTime;
-    let combinedPresentations = { ...firstPresentation }
-    presentations.forEach((presentation, i) => {
-      // already consider the first presentation
-      if (i === 0) return;
-
-      const { movements, totalTime } = presentation;
-      if (movements === null) return;
-
-      // add the summed time to the movements
-      const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
-      // concat the movements already in the combined presentation with these new ones
-      const newMovements = [...combinedPresentations.movements || [], ...addedTimeMovements];
-
-      combinedPresentations = { ...combinedPresentations, movements: newMovements }
-
+    // these three will lead to the combined presentation
+    let combinedMovements: Movement[] = [];
+    let sumTime = 0;
+    let combinedMetas: any[] = [];
+
+    presentations.forEach((presentation) => {
+      const { movements, totalTime, meta } = presentation;
+      // update movements if they had one
+      if (movements) {
+        // add the summed time to the movements
+        const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
+        // concat the movements already in the combined presentation with these new ones
+        combinedMovements.push(...addedTimeMovements);
+      }
       // update the totalTime
       sumTime += totalTime;
+      // concatenate the metas
+      combinedMetas.push(...meta);
     });
 
     // return the combined presentation with the updated total summed time
-    return { ...combinedPresentations, totalTime: sumTime };
+    return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
   }
 
     // Unfinished code for tracing multiple free form views
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index be9f342bb..35a6aa07e 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -11,270 +11,264 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
 import { Presentation, RecordingApi } from '../../../util/RecordingApi';
 
 export interface MediaSegment {
-    videoChunks: any[],
-    endTime: number,
+  videoChunks: any[],
+  endTime: number,
   startTime: number,
   presentation?: Presentation,
 }
 
 interface IRecordingViewProps {
-    setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
-    setDuration: (seconds: number) => void
-    id: string
+  setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
+  setDuration: (seconds: number) => void
+  id: string
 }
 
 const MAXTIME = 100000;
 
 export function RecordingView(props: IRecordingViewProps) {
 
-    const [recording, setRecording] = useState(false);
-    const recordingTimerRef = useRef<number>(0);
-    const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
-    const [playing, setPlaying] = useState(false);
-    const [progress, setProgress] = useState(0);
-
-    // acts as a "refresh state" to tell progressBar when to undo
-    const [doUndo, setDoUndo] = useState(false);
-    // whether an undo can occur or not
-    const [canUndo, setCanUndo] = useState(false);
-
-    const [videos, setVideos] = useState<MediaSegment[]>([]);
-    const [orderVideos, setOrderVideos] = useState<boolean>(false);
-    const videoRecorder = useRef<MediaRecorder | null>(null);
-    const videoElementRef = useRef<HTMLVideoElement | null>(null);
-
-    const [finished, setFinished] = useState<boolean>(false)
-    const [trackScreen, setTrackScreen] = useState<boolean>(true)
-
-
-
-    const DEFAULT_MEDIA_CONSTRAINTS = {
-        video: {
-            width: 1280,
-            height: 720,
-        },
-        audio: {
-            echoCancellation: true,
-            noiseSuppression: true,
-            sampleRate: 44100
-        }
-    }
-  
-  // useEffect(() => console.info('progress', progress), [progress])
+  const [recording, setRecording] = useState(false);
+  const recordingTimerRef = useRef<number>(0);
+  const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
+  const [playing, setPlaying] = useState(false);
+  const [progress, setProgress] = useState(0);
 
+  // acts as a "refresh state" to tell progressBar when to undo
+  const [doUndo, setDoUndo] = useState(false);
+  // whether an undo can occur or not
+  const [canUndo, setCanUndo] = useState(false);
 
-    useEffect(() => {
-      if (finished) {
-        // make the total presentation that'll match the concatted video
-        const concatPres = RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
-        console.log('concatPres', concatPres);
+  const [videos, setVideos] = useState<MediaSegment[]>([]);
+  const [orderVideos, setOrderVideos] = useState<boolean>(false);
+  const videoRecorder = useRef<MediaRecorder | null>(null);
+  const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
-            // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
-            (async () => {
-              const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
+  const [finished, setFinished] = useState<boolean>(false)
+  const [trackScreen, setTrackScreen] = useState<boolean>(true)
 
-                // upload the segments to the server and get their server access paths
-                const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
-                    .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
 
-                // concat the segments together using post call
-                const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
-                !(result instanceof Error) ? props.setResult(result, concatPres) : console.error("video conversion failed");
-            })();
-        }
-    }, [videos])
 
-    // this will call upon the progress bar to edit videos to be in the correct order
-    useEffect(() => {
-        finished && setOrderVideos(true);
-    }, [finished])
+  const DEFAULT_MEDIA_CONSTRAINTS = {
+    video: {
+      width: 1280,
+      height: 720,
+    },
+    audio: {
+      echoCancellation: true,
+      noiseSuppression: true,
+      sampleRate: 44100
+    }
+  }
 
-    // check if the browser supports media devices on first load
-    useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, [])
+  // useEffect(() => console.debug('progress', progress), [progress])
 
   useEffect(() => {
-      console.log('recording useEffect', recording)
-        let interval: any = null;
-        if (recording) {
-            interval = setInterval(() => {
-                setRecordingTimer(unit => unit + 1);
-            }, 10);
-        } else if (!recording && recordingTimer !== 0) {
-            clearInterval(interval);
-        }
-        return () => clearInterval(interval);
-    }, [recording])
-
-    useEffect(() => {
-        setVideoProgressHelper(recordingTimer)
-        recordingTimerRef.current = recordingTimer;
-    }, [recordingTimer])
-
-    const setVideoProgressHelper = (progress: number) => {
-        const newProgress = (progress / MAXTIME) * 100;
-        setProgress(newProgress)
+    if (finished) {
+      // make the total presentation that'll match the concatted video
+      let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+
+      // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
+      (async () => {
+        const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
+
+        // upload the segments to the server and get their server access paths
+        const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
+          .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+
+        // concat the segments together using post call
+        const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
+        !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+      })();
     }
+  }, [videos])
 
-    const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
-        const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
+  // this will call upon the progress bar to edit videos to be in the correct order
+  useEffect(() => {
+    finished && setOrderVideos(true);
+  }, [finished])
 
-        videoElementRef.current!.src = ""
-        videoElementRef.current!.srcObject = stream
-        videoElementRef.current!.muted = true
+  // check if the browser supports media devices on first load
+  useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, [])
 
-        return stream
+  useEffect(() => {
+    let interval: any = null;
+    if (recording) {
+      interval = setInterval(() => {
+        setRecordingTimer(unit => unit + 1);
+      }, 10);
+    } else if (!recording && recordingTimer !== 0) {
+      clearInterval(interval);
     }
+    return () => clearInterval(interval);
+  }, [recording])
 
-    const record = async () => {
-        // don't need to start a new stream every time we start recording a new segment
-        if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream())
-
-        // temporary chunks of video
-        let videoChunks: any = []
-
-        videoRecorder.current.ondataavailable = (event: any) => {
-            if (event.data.size > 0) videoChunks.push(event.data)
-        }
-
-        videoRecorder.current.onstart = (event: any) => {
-            setRecording(true);
-            // trackScreen && RecordingApi.Instance.start();
-            trackScreen && RecordingApi.Instance.start();
-        }
-
-      videoRecorder.current.onstop = () => {
-          // RecordingApi.Instance.stop();
-            // if we have a last portion
-        if (videoChunks.length > 1) {
-          const presentation = RecordingApi.Instance.yieldPresentation().next().value || undefined
-          console.log('presenation yield', JSON.parse(JSON.stringify(presentation)))
-                // append the current portion to the video pieces
-                  setVideos(videos => [...videos, {
-                    videoChunks: videoChunks,
-                    endTime: recordingTimerRef.current,
-                    startTime: videos?.lastElement()?.endTime || 0,
-                    // RecordingApi.stop() will return undefined if no track screen
-                    presentation
-                  }])
-                  // now that we got the presentation data, we can clear for the next segment to be recorded
-                  // RecordingApi.Instance.clear();
-            }
-
-          // reset the temporary chunks
-          videoChunks = []
-          setRecording(false);
-        }
-
-        videoRecorder.current.start(200)
-    }
+  useEffect(() => {
+    setVideoProgressHelper(recordingTimer)
+    recordingTimerRef.current = recordingTimer;
+  }, [recordingTimer])
 
+  const setVideoProgressHelper = (progress: number) => {
+    const newProgress = (progress / MAXTIME) * 100;
+    setProgress(newProgress)
+  }
 
-    // if this is called, then we're done recording all the segments
-    const finish = (e: React.PointerEvent) => {
-        e.stopPropagation();
+  const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
+    const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
 
-        // call stop on the video recorder if active
-        videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+    videoElementRef.current!.src = ""
+    videoElementRef.current!.srcObject = stream
+    videoElementRef.current!.muted = true
 
-        // end the streams (audio/video) to remove recording icon
-        const stream = videoElementRef.current!.srcObject;
-        stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
-          
-        // clear the recoringApi
-        RecordingApi.Instance.clear();
+    return stream
+  }
 
-        // this will call upon progessbar to update videos to be in the correct order
-        setFinished(true);
-    }
+  const record = async () => {
+    // don't need to start a new stream every time we start recording a new segment
+    if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream())
+
+    // temporary chunks of video
+    let videoChunks: any = []
 
-    const pause = (e: React.PointerEvent) => {
-      e.stopPropagation()
-      // if recording, then this is just a new segment
-      videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
+    videoRecorder.current.ondataavailable = (event: any) => {
+      if (event.data.size > 0) videoChunks.push(event.data)
     }
 
-    const start = (e: React.PointerEvent) => {
-        // the code to start or resume does not get triggered if we start dragging the button
-        setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-          if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
-            record();
-            // trackScreen &&
-          }
-          return true; // cancels propagation to documentView to avoid selecting it.
-        }, false, false);
+    videoRecorder.current.onstart = (event: any) => {
+      setRecording(true);
+      // start the recording api when the video recorder starts
+      trackScreen && RecordingApi.Instance.start();
     }
 
-    const undoPrevious = (e: React.PointerEvent) => {
-        e.stopPropagation();
-        setDoUndo(prev => !prev);
+    videoRecorder.current.onstop = () => {
+      // RecordingApi.Instance.stop();
+      // if we have a last portion
+      if (videoChunks.length > 1) {
+        // append the current portion to the video pieces
+        const nextVideo = {
+          videoChunks,
+          endTime: recordingTimerRef.current,
+          startTime: videos?.lastElement()?.endTime || 0
+        };
+        // depending on if a presenation exists, add it to the video
+        const { done: presError, value: presentation } = RecordingApi.Instance.yieldPresentation().next();
+        setVideos(videos => [...videos, (!presError && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+      }
+
+      // reset the temporary chunks
+      videoChunks = []
+      setRecording(false);
     }
 
+    videoRecorder.current.start(200)
+  }
+
+
+  // if this is called, then we're done recording all the segments
+  const finish = (e: React.PointerEvent) => {
+    e.stopPropagation();
+
+    // call stop on the video recorder if active
+    videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+
+    // end the streams (audio/video) to remove recording icon
+    const stream = videoElementRef.current!.srcObject;
+    stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
+
+    // clear the recoringApi
+    RecordingApi.Instance.clear();
+
+    // this will call upon progessbar to update videos to be in the correct order
+    setFinished(true);
+  }
+
+  const pause = (e: React.PointerEvent) => {
+    e.stopPropagation()
+    // if recording, then this is just a new segment
+    videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
+  }
+
+  const start = (e: React.PointerEvent) => {
+    // the code to start or resume does not get triggered if we start dragging the button
+    setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
+      if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+        record();
+        // trackScreen &&
+      }
+      return true; // cancels propagation to documentView to avoid selecting it.
+    }, false, false);
+  }
+
+  const undoPrevious = (e: React.PointerEvent) => {
+    e.stopPropagation();
+    setDoUndo(prev => !prev);
+  }
+
   const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
 
-    const millisecondToMinuteSecond = (milliseconds: number) => {
-        const toTwoDigit = (digit: number) => {
-            return String(digit).length == 1 ? "0" + digit : digit
-        }
-        const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
-        const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
-        return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
+  const millisecondToMinuteSecond = (milliseconds: number) => {
+    const toTwoDigit = (digit: number) => {
+      return String(digit).length == 1 ? "0" + digit : digit
     }
-
-    return (
-        <div className="recording-container">
-            <div className="video-wrapper">
-                <video id={`video-${props.id}`}
-                    autoPlay
-                    muted
-                    onTimeUpdate={() => handleOnTimeUpdate()}
-                    ref={videoElementRef}
-                />
-                <div className="recording-sign">
-                    <span className="dot" />
-                    <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
-                </div>
-                <div className="controls">
-
-                    <div className="controls-inner-container">
-                        <div className="record-button-wrapper">
-                            {recording ?
-                                <button className="stop-button" onPointerDown={pause} /> :
-                                <button className="record-button" onPointerDown={start} />
-                            }
-                        </div>
-
-                        {!recording && (videos.length > 0 ?
-
-                            <div className="options-wrapper video-edit-wrapper">
-                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: {display: canUndo ? 'inherit' : 'none'} }}>
-                                    <MdBackspace onPointerDown={undoPrevious} />
-                                </IconContext.Provider>
-                                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
-                                    <FaCheckCircle onPointerDown={finish} />
-                                </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>
-
-                </div>
-
-                <ProgressBar
-                    videos={videos}
-                    setVideos={setVideos}
-                    orderVideos={orderVideos}
-                    progress={progress}
-                    recording={recording}
-                    doUndo={doUndo}
-                    setCanUndo={setCanUndo}
-                />
+    const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
+    const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
+    return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
+  }
+
+  return (
+    <div className="recording-container">
+      <div className="video-wrapper">
+        <video id={`video-${props.id}`}
+          autoPlay
+          muted
+          onTimeUpdate={() => handleOnTimeUpdate()}
+          ref={videoElementRef}
+        />
+        <div className="recording-sign">
+          <span className="dot" />
+          <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+        </div>
+        <div className="controls">
+
+          <div className="controls-inner-container">
+            <div className="record-button-wrapper">
+              {recording ?
+                <button className="stop-button" onPointerDown={pause} /> :
+                <button className="record-button" onPointerDown={start} />
+              }
             </div>
-        </div>)
+
+            {!recording && (videos.length > 0 ?
+
+              <div className="options-wrapper video-edit-wrapper">
+                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
+                  <MdBackspace onPointerDown={undoPrevious} />
+                </IconContext.Provider>
+                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+                  <FaCheckCircle onPointerDown={finish} />
+                </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>
+
+        </div>
+
+        <ProgressBar
+          videos={videos}
+          setVideos={setVideos}
+          orderVideos={orderVideos}
+          progress={progress}
+          recording={recording}
+          doUndo={doUndo}
+          setCanUndo={setCanUndo}
+        />
+      </div>
+    </div>)
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From d91ff1f937e9588b88258b07738eea56bac5fcca Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Mon, 13 Jun 2022 13:21:38 -0400
Subject: fix bugs with selecting tracking. very stable now.

---
 src/client/util/RecordingApi.ts                    | 408 ++++++++++-----------
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   2 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  18 +-
 3 files changed, 208 insertions(+), 220 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index 12c1654a2..d56093cee 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -5,10 +5,10 @@ import { Doc } from "../../fields/Doc";
 import { VideoBox } from "../views/nodes/VideoBox";
 
 type Movement = {
-    time: number,
-    panX: number,
-    panY: number,
-    scale: number,
+  time: number,
+  panX: number,
+  panY: number,
+  scale: number,
 }
 
 export type Presentation = {
@@ -20,78 +20,82 @@ export type Presentation = {
 export class RecordingApi {
 
   private static get NULL_PRESENTATION(): Presentation {
-    return { movements: null, meta: {}, totalTime: -1,}
+    return { movements: null, meta: {}, totalTime: -1, }
   }
 
-    // instance variables
-    private currentPresentation: Presentation;
-    private tracking: boolean;
-    private absoluteStart: number;
-
-
-    // create static instance and getter for global use
-    @observable static _instance: RecordingApi;
-    public static get Instance(): RecordingApi { return RecordingApi._instance }
-    public constructor() {
-        // init the global instance
-        RecordingApi._instance = this;
-
-        // init the instance variables
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        this.tracking = false;
-        this.absoluteStart = -1;
-
-        // used for tracking movements in the view frame
-        this.disposeFunc = null;
-        this.recordingFFView = null;
-
-        // for now, set playFFView
-        this.playFFView = null;
-        this.timers = null;
-      
-      // put a pointerdown event on the doucment to see what the target
-    }
+  // instance variables
+  private currentPresentation: Presentation;
+  private tracking: boolean;
+  private absoluteStart: number;
 
-    // little helper :)
-    private get isInitPresenation(): boolean {
-        return this.currentPresentation.movements === null
-    }
 
-    public start = (meta?: Object) => {
-      // update the presentation mode
-      Doc.UserDoc().presentationMode = 'recording';
-
-        // (1a) get start date for presenation
-      const startDate = new Date();
-        // (1b) set start timestamp to absolute timestamp
-      this.absoluteStart = startDate.getTime();
-
-        // (2) assign meta content if it exists
-        this.currentPresentation.meta = meta || {}
-        // (3) assign start date to currentPresenation
-        this.currentPresentation.movements = []
-        // (4) set tracking true to allow trackMovements
-        this.tracking = true
-    }
+  // create static instance and getter for global use
+  @observable static _instance: RecordingApi;
+  public static get Instance(): RecordingApi { return RecordingApi._instance }
+  public constructor() {
+    // init the global instance
+    RecordingApi._instance = this;
+
+    // init the instance variables
+    this.currentPresentation = RecordingApi.NULL_PRESENTATION
+    this.tracking = false;
+    this.absoluteStart = -1;
+
+    // used for tracking movements in the view frame
+    this.disposeFunc = null;
+    this.recordingFFView = null;
 
-  /* stops the video and returns the presentatation; if no presentation, returns undefined */  
-  public * yieldPresentation(clearData: boolean = true): Generator<Presentation | null> {
+    // for now, set playFFView
+    this.playFFView = null;
+    this.timers = null;
+
+    // put a pointerdown event on the doucment to see what the target
+  }
+
+  // little helper :)
+  private get nullPresentation(): boolean {
+    return this.currentPresentation.movements === null
+  }
+
+  public start = (meta?: Object) => {
+    // update the presentation mode
+    Doc.UserDoc().presentationMode = 'recording';
+
+    // (1a) get start date for presenation
+    const startDate = new Date();
+    // (1b) set start timestamp to absolute timestamp
+    this.absoluteStart = startDate.getTime();
+
+    // (2) assign meta content if it exists
+    this.currentPresentation.meta = meta || {}
+    // (3) assign start date to currentPresenation
+    this.currentPresentation.movements = []
+    // (4) set tracking true to allow trackMovements
+    this.tracking = true
+  }
+
+  /* stops the video and returns the presentatation; if no presentation, returns undefined */
+  public yieldPresentation(clearData: boolean = true): Presentation | null {
     // if no presentation or done tracking, return null
-    if (!this.isInitPresenation || !this.tracking) return null;
+    if (this.nullPresentation || !this.tracking) return null;
 
     // set the previus recording view to the play view
     this.playFFView = this.recordingFFView;
 
     // ensure we add the endTime now that they are done recording    
-    yield { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+    const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
 
     // reset the current presentation
     clearData && this.clear();
+
+    return cpy;
   }
 
-  public stop = (): void => {
+  public finish = (): void => {
     // make is tracking false
     this.tracking = false
+    // reset the RecordingApi instance
+    this.clear();
   }
 
   public clear = (): void => {
@@ -108,155 +112,138 @@ export class RecordingApi {
     // clear absoluteStart
     this.absoluteStart = -1
   }
-  
-    // call on dispose function to stop tracking movements
-    public removeRecordingFFView = (): void => {
-        this.disposeFunc?.();
-        this.disposeFunc = null;
-    }
 
-    // public pause = (): Error | undefined => {
-    //     if (this.isInitPresenation) {
-    //         console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
-    //         return new Error('[recordingApi.ts] pause(): no presentation')
-    //     }
-    //     // don't allow track movments
-    //     this.tracking = false
-
-    //     // set adjust absoluteStart to add the time difference
-    //     const timestamp = new Date().getTime()
-    //     this.absoluteStart = timestamp - this.absoluteStart
-    // }
-
-    // public resume = () => {
-    //     this.tracking = true
-    //     // set absoluteStart to the difference in time
-    //     this.absoluteStart = new Date().getTime() - this.absoluteStart
-    // }
-
-    private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
-        // ensure we are recording
-        if (!this.tracking) {
-            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')
-        }
-      
-      // TO FIX: bob
-      // console.debug('track movment')
-
-        // get the time
-        const time = new Date().getTime() - this.absoluteStart
-        // make new movement object
-        const movement: Movement = { time, panX, panY, scale }
-        
-        // add that movement to the current presentation data's movement array
-        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
-    }
+  // call on dispose function to stop tracking movements
+  public removeRecordingFFView = (): void => {
+    this.disposeFunc?.();
+    this.disposeFunc = null;
+  }
 
-    // instance variable for the FFView
-    private disposeFunc: IReactionDisposer | null;
-    private recordingFFView: CollectionFreeFormView | null;
-    
-    // set the FFView that will be used in a reaction to track the movements
-    public setRecordingFFView = (view: CollectionFreeFormView): void => {
-        // set the view to the current view
-        if (view === this.recordingFFView || view == null) return;
-        
-        // this.recordingFFView = view;
-        // set the reaction to track the movements
-        this.disposeFunc = reaction(
-            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
-            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
-        )
-
-        // for now, set the most recent recordingFFView to the playFFView
-        this.recordingFFView = view;
+  private trackMovements = (panX: number, panY: number, scale: number = 0) => {
+    // ensure we are recording
+    if (!this.tracking) {
+      console.error('[recordingApi.ts] trackMovements(): tracking is false')
+      return;
     }
+    // check to see if the presetation is init
+    if (this.nullPresentation) {
+      console.error('[recordingApi.ts] trackMovements(): no presentation')
+      return;
+    }
+
+    // TO FIX: bob
+    // console.debug('track movment')
+
+    // get the time
+    const time = new Date().getTime() - this.absoluteStart
+    // make new movement object
+    const movement: Movement = { time, panX, panY, scale }
+
+    // add that movement to the current presentation data's movement array
+    this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
+  }
 
-    // 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;
+  // instance variable for the FFView
+  private disposeFunc: IReactionDisposer | null;
+  private recordingFFView: CollectionFreeFormView | null;
 
-    public setPlayFFView = (view: CollectionFreeFormView): void => {
-        this.playFFView = view
+  // set the FFView that will be used in a reaction to track the movements
+  public setRecordingFFView = (view: CollectionFreeFormView): void => {
+    // set the view to the current view
+    if (view === this.recordingFFView || view == null) return;
+
+    // this.recordingFFView = view;
+    // set the reaction to track the movements
+    this.disposeFunc = reaction(
+      () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
+      (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
+    )
+
+    // for now, set the most recent recordingFFView to the playFFView
+    this.recordingFFView = view;
+  }
+
+  // TODO: extract this into different class with pause and resume recording
+  // TODO: store the FFview with the movements
+  private playFFView: CollectionFreeFormView | null;
+  private timers: NodeJS.Timeout[] | null;
+
+  public setPlayFFView = (view: CollectionFreeFormView): void => {
+    this.playFFView = view
+  }
+
+  // pausing movements will dispose all timers that are planned to replay the movements
+  // play movemvents will recreate them when the user resumes the presentation
+  public pauseMovements = (): undefined | Error => {
+    if (this.playFFView === null) {
+      return new Error('[recordingApi.ts] pauseMovements() failed: no view')
     }
 
-    // pausing movements will dispose all timers that are planned to replay the movements
-    // play movemvents will recreate them when the user resumes the presentation
-    public pauseMovements = (): undefined | Error => {
-        if (this.playFFView === null) {
-            return new Error('[recordingApi.ts] pauseMovements() failed: no view')
-        }
-
-        if (!this._isPlaying) {
-            //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
-            return
-        }
-        this._isPlaying = false
-        // TODO: set userdoc presentMode to browsing
-        this.timers?.map(timer => clearTimeout(timer))
-
-        // this.videoBox = null;
+    if (!this._isPlaying) {
+      //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+      return
     }
-    
-    private videoBox: VideoBox | null = null;
+    this._isPlaying = false
+    // TODO: set userdoc presentMode to browsing
+    this.timers?.map(timer => clearTimeout(timer))
+
+    // this.videoBox = null;
+  }
 
-    // by calling pause on the VideoBox, the pauseMovements will be called
-    public pauseVideoAndMovements = (): boolean => {
-        this.videoBox?.Pause()
+  private videoBox: VideoBox | null = null;
+
+  // by calling pause on the VideoBox, the pauseMovements will be called
+  public pauseVideoAndMovements = (): boolean => {
+    this.videoBox?.Pause()
+
+    this.pauseMovements()
+    return this.videoBox == null
+  }
 
-        this.pauseMovements()
-        return this.videoBox == null
+  public _isPlaying = false;
+
+  public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
+    if (presentation.movements === null || this.playFFView === null) {
+      return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
     }
+    if (this._isPlaying) return;
+
+    this._isPlaying = true;
+    Doc.UserDoc().presentationMode = 'watching';
+
+    // TODO: consider this bug at the end of the clip on seek
+    this.videoBox = videoBox || null;
 
-    public _isPlaying = false;
-
-    public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
-        if (presentation.movements === null || this.playFFView === null) {
-            return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
-        }
-        if(this._isPlaying) return;
-
-        this._isPlaying = true;
-        Doc.UserDoc().presentationMode = 'watching';
-
-        // TODO: consider this bug at the end of the clip on seek
-        this.videoBox = videoBox || null;
-
-        // only get the movements that are remaining in the video time left
-        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
-
-        // helper to replay a movement
-        const document = this.playFFView
-        let preScale = -1;
-        const zoomAndPan = (movement: Movement) => {
-            const { panX, panY, scale } = movement;
-            (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
-            document.Document._panX = panX;
-            document.Document._panY = panY;
-
-            preScale = scale;
-        }
-
-        // set the first frame to be at the start of the pres
-        zoomAndPan(filteredMovements[0]);
-
-        // make timers that will execute each movement at the correct replay time
-        this.timers = filteredMovements.map(movement => {
-            const timeDiff = movement.time - timeViewed*1000
-            return setTimeout(() => {
-                // replay the movement
-                zoomAndPan(movement)
-                // if last movement, presentation is done -> set the instance var
-                if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
-            }, timeDiff)
-        })
+    // only get the movements that are remaining in the video time left
+    const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+    // helper to replay a movement
+    const document = this.playFFView
+    let preScale = -1;
+    const zoomAndPan = (movement: Movement) => {
+      const { panX, panY, scale } = movement;
+      (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+      document.Document._panX = panX;
+      document.Document._panY = panY;
+
+      preScale = scale;
     }
-  
+
+    // set the first frame to be at the start of the pres
+    zoomAndPan(filteredMovements[0]);
+
+    // make timers that will execute each movement at the correct replay time
+    this.timers = filteredMovements.map(movement => {
+      const timeDiff = movement.time - timeViewed * 1000
+      return setTimeout(() => {
+        // replay the movement
+        zoomAndPan(movement)
+        // if last movement, presentation is done -> set the instance var
+        if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
+      }, timeDiff)
+    })
+  }
+
   // method that concatenates an array of presentatations into one
   public concatPresentations = (presentations: Presentation[]): Presentation => {
     // these three will lead to the combined presentation
@@ -266,6 +253,7 @@ export class RecordingApi {
 
     presentations.forEach((presentation) => {
       const { movements, totalTime, meta } = presentation;
+
       // update movements if they had one
       if (movements) {
         // add the summed time to the movements
@@ -273,8 +261,10 @@ export class RecordingApi {
         // concat the movements already in the combined presentation with these new ones
         combinedMovements.push(...addedTimeMovements);
       }
+
       // update the totalTime
       sumTime += totalTime;
+
       // concatenate the metas
       combinedMetas.push(...meta);
     });
@@ -283,19 +273,19 @@ export class RecordingApi {
     return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
   }
 
-    // Unfinished code for tracing multiple free form views
-    // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
-
-    // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
-    //     pres.set(ffView,
-    //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
-    //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
-    //     )
-    // }
-
-    // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
-    //     const disposer = pres.get(ffView);
-    //     disposer?.();
-    //     pres.delete(ffView)
-    // }
+  // Unfinished code for tracing multiple free form views
+  // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
+
+  // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
+  //     pres.set(ffView,
+  //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
+  //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
+  //     )
+  // }
+
+  // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
+  //     const disposer = pres.get(ffView);
+  //     disposer?.();
+  //     pres.delete(ffView)
+  // }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 6fe67b6db..a28677525 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -46,7 +46,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
-            // stringify the presenation and store it
+            // stringify the presentation and store it
             presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
       }
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 35a6aa07e..15883632a 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -140,7 +140,6 @@ export function RecordingView(props: IRecordingViewProps) {
     }
 
     videoRecorder.current.onstop = () => {
-      // RecordingApi.Instance.stop();
       // if we have a last portion
       if (videoChunks.length > 1) {
         // append the current portion to the video pieces
@@ -149,9 +148,10 @@ export function RecordingView(props: IRecordingViewProps) {
           endTime: recordingTimerRef.current,
           startTime: videos?.lastElement()?.endTime || 0
         };
+
         // depending on if a presenation exists, add it to the video
-        const { done: presError, value: presentation } = RecordingApi.Instance.yieldPresentation().next();
-        setVideos(videos => [...videos, (!presError && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+        const presentation = RecordingApi.Instance.yieldPresentation();
+        setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
       }
 
       // reset the temporary chunks
@@ -174,8 +174,8 @@ export function RecordingView(props: IRecordingViewProps) {
     const stream = videoElementRef.current!.srcObject;
     stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
 
-    // clear the recoringApi
-    RecordingApi.Instance.clear();
+    // finish/clear the recoringApi
+    RecordingApi.Instance.finish();
 
     // this will call upon progessbar to update videos to be in the correct order
     setFinished(true);
@@ -188,12 +188,10 @@ export function RecordingView(props: IRecordingViewProps) {
   }
 
   const start = (e: React.PointerEvent) => {
-    // the code to start or resume does not get triggered if we start dragging the button
     setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-      if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
-        record();
-        // trackScreen &&
-      }
+      // start recording if not already recording
+      if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
+
       return true; // cancels propagation to documentView to avoid selecting it.
     }, false, false);
   }
-- 
cgit v1.2.3-70-g09d2


From 1dfb694ffa8f34a834387eb073ffb9bd3a678039 Mon Sep 17 00:00:00 2001
From: Michael <michael.foiani@gmail.com>
Date: Mon, 13 Jun 2022 14:09:46 -0400
Subject: debug w bob

---
 src/client/util/RecordingApi.ts                       |  2 --
 src/client/views/nodes/RecordingBox/RecordingView.tsx | 11 +++++++----
 2 files changed, 7 insertions(+), 6 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index d56093cee..bcd21a48a 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -48,8 +48,6 @@ export class RecordingApi {
     // for now, set playFFView
     this.playFFView = null;
     this.timers = null;
-
-    // put a pointerdown event on the doucment to see what the target
   }
 
   // little helper :)
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 15883632a..d4d19f3d8 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -60,7 +60,7 @@ export function RecordingView(props: IRecordingViewProps) {
     }
   }
 
-  // useEffect(() => console.debug('progress', progress), [progress])
+  useEffect(() => console.log('progress', progress), [progress])
 
   useEffect(() => {
     if (finished) {
@@ -93,7 +93,8 @@ export function RecordingView(props: IRecordingViewProps) {
   useEffect(() => {
     let interval: any = null;
     if (recording) {
-      interval = setInterval(() => {
+      interval = setTimeout(() => {
+        console.log('update interval')
         setRecordingTimer(unit => unit + 1);
       }, 10);
     } else if (!recording && recordingTimer !== 0) {
@@ -201,7 +202,9 @@ export function RecordingView(props: IRecordingViewProps) {
     setDoUndo(prev => !prev);
   }
 
-  const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+  const handleOnTimeUpdate = () => {
+    playing && setVideoProgressHelper(videoElementRef.current!.currentTime);
+  };
 
   const millisecondToMinuteSecond = (milliseconds: number) => {
     const toTwoDigit = (digit: number) => {
@@ -218,7 +221,7 @@ export function RecordingView(props: IRecordingViewProps) {
         <video id={`video-${props.id}`}
           autoPlay
           muted
-          onTimeUpdate={() => handleOnTimeUpdate()}
+          onTimeUpdate={handleOnTimeUpdate}
           ref={videoElementRef}
         />
         <div className="recording-sign">
-- 
cgit v1.2.3-70-g09d2


From f8bfda0b0139db5f8bf1de602ce211a091d5d9ef Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Mon, 13 Jun 2022 14:56:03 -0400
Subject: revert to previous stable commit. lmao get ratioed by a mouse

---
 src/client/util/RecordingApi.ts                    | 383 ++++++++++++---------
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   2 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  18 +-
 3 files changed, 223 insertions(+), 180 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index bcd21a48a..4c8f55ce5 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -5,10 +5,10 @@ import { Doc } from "../../fields/Doc";
 import { VideoBox } from "../views/nodes/VideoBox";
 
 type Movement = {
-  time: number,
-  panX: number,
-  panY: number,
-  scale: number,
+    time: number,
+    panX: number,
+    panY: number,
+    scale: number,
 }
 
 export type Presentation = {
@@ -20,9 +20,10 @@ export type Presentation = {
 export class RecordingApi {
 
   private static get NULL_PRESENTATION(): Presentation {
-    return { movements: null, meta: {}, totalTime: -1, }
+    return { movements: null, meta: {}, totalTime: -1,}
   }
 
+<<<<<<< HEAD
   // instance variables
   private currentPresentation: Presentation;
   private tracking: boolean;
@@ -49,51 +50,77 @@ export class RecordingApi {
     this.playFFView = null;
     this.timers = null;
   }
+=======
+    // instance variables
+    private currentPresentation: Presentation;
+    private tracking: boolean;
+    private absoluteStart: number;
+
+
+    // create static instance and getter for global use
+    @observable static _instance: RecordingApi;
+    public static get Instance(): RecordingApi { return RecordingApi._instance }
+    public constructor() {
+        // init the global instance
+        RecordingApi._instance = this;
+
+        // init the instance variables
+        this.currentPresentation = RecordingApi.NULL_PRESENTATION
+        this.tracking = false;
+        this.absoluteStart = -1;
+
+        // used for tracking movements in the view frame
+        this.disposeFunc = null;
+        this.recordingFFView = null;
+
+        // for now, set playFFView
+        this.playFFView = null;
+        this.timers = null;
+      
+      // put a pointerdown event on the doucment to see what the target
+    }
+>>>>>>> parent of d91ff1f93 (fix bugs with selecting tracking. very stable now.)
 
-  // little helper :)
-  private get nullPresentation(): boolean {
-    return this.currentPresentation.movements === null
-  }
+    // little helper :)
+    private get isInitPresenation(): boolean {
+        return this.currentPresentation.movements === null
+    }
 
-  public start = (meta?: Object) => {
-    // update the presentation mode
-    Doc.UserDoc().presentationMode = 'recording';
-
-    // (1a) get start date for presenation
-    const startDate = new Date();
-    // (1b) set start timestamp to absolute timestamp
-    this.absoluteStart = startDate.getTime();
-
-    // (2) assign meta content if it exists
-    this.currentPresentation.meta = meta || {}
-    // (3) assign start date to currentPresenation
-    this.currentPresentation.movements = []
-    // (4) set tracking true to allow trackMovements
-    this.tracking = true
-  }
+    public start = (meta?: Object) => {
+      // update the presentation mode
+      Doc.UserDoc().presentationMode = 'recording';
+
+        // (1a) get start date for presenation
+      const startDate = new Date();
+        // (1b) set start timestamp to absolute timestamp
+      this.absoluteStart = startDate.getTime();
+
+        // (2) assign meta content if it exists
+        this.currentPresentation.meta = meta || {}
+        // (3) assign start date to currentPresenation
+        this.currentPresentation.movements = []
+        // (4) set tracking true to allow trackMovements
+        this.tracking = true
+    }
 
-  /* stops the video and returns the presentatation; if no presentation, returns undefined */
-  public yieldPresentation(clearData: boolean = true): Presentation | null {
+  /* stops the video and returns the presentatation; if no presentation, returns undefined */  
+  public * yieldPresentation(clearData: boolean = true): Generator<Presentation | null> {
     // if no presentation or done tracking, return null
-    if (this.nullPresentation || !this.tracking) return null;
+    if (!this.isInitPresenation || !this.tracking) return null;
 
     // set the previus recording view to the play view
     this.playFFView = this.recordingFFView;
 
     // ensure we add the endTime now that they are done recording    
-    const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+    yield { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
 
     // reset the current presentation
     clearData && this.clear();
-
-    return cpy;
   }
 
-  public finish = (): void => {
+  public stop = (): void => {
     // make is tracking false
     this.tracking = false
-    // reset the RecordingApi instance
-    this.clear();
   }
 
   public clear = (): void => {
@@ -110,138 +137,155 @@ export class RecordingApi {
     // clear absoluteStart
     this.absoluteStart = -1
   }
-
-  // call on dispose function to stop tracking movements
-  public removeRecordingFFView = (): void => {
-    this.disposeFunc?.();
-    this.disposeFunc = null;
-  }
-
-  private trackMovements = (panX: number, panY: number, scale: number = 0) => {
-    // ensure we are recording
-    if (!this.tracking) {
-      console.error('[recordingApi.ts] trackMovements(): tracking is false')
-      return;
-    }
-    // check to see if the presetation is init
-    if (this.nullPresentation) {
-      console.error('[recordingApi.ts] trackMovements(): no presentation')
-      return;
+  
+    // call on dispose function to stop tracking movements
+    public removeRecordingFFView = (): void => {
+        this.disposeFunc?.();
+        this.disposeFunc = null;
     }
 
-    // TO FIX: bob
-    // console.debug('track movment')
-
-    // get the time
-    const time = new Date().getTime() - this.absoluteStart
-    // make new movement object
-    const movement: Movement = { time, panX, panY, scale }
-
-    // add that movement to the current presentation data's movement array
-    this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
-  }
-
-  // instance variable for the FFView
-  private disposeFunc: IReactionDisposer | null;
-  private recordingFFView: CollectionFreeFormView | null;
-
-  // set the FFView that will be used in a reaction to track the movements
-  public setRecordingFFView = (view: CollectionFreeFormView): void => {
-    // set the view to the current view
-    if (view === this.recordingFFView || view == null) return;
-
-    // this.recordingFFView = view;
-    // set the reaction to track the movements
-    this.disposeFunc = reaction(
-      () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
-      (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
-    )
-
-    // for now, set the most recent recordingFFView to the playFFView
-    this.recordingFFView = view;
-  }
-
-  // TODO: extract this into different class with pause and resume recording
-  // TODO: store the FFview with the movements
-  private playFFView: CollectionFreeFormView | null;
-  private timers: NodeJS.Timeout[] | null;
-
-  public setPlayFFView = (view: CollectionFreeFormView): void => {
-    this.playFFView = view
-  }
-
-  // pausing movements will dispose all timers that are planned to replay the movements
-  // play movemvents will recreate them when the user resumes the presentation
-  public pauseMovements = (): undefined | Error => {
-    if (this.playFFView === null) {
-      return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+    // public pause = (): Error | undefined => {
+    //     if (this.isInitPresenation) {
+    //         console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
+    //         return new Error('[recordingApi.ts] pause(): no presentation')
+    //     }
+    //     // don't allow track movments
+    //     this.tracking = false
+
+    //     // set adjust absoluteStart to add the time difference
+    //     const timestamp = new Date().getTime()
+    //     this.absoluteStart = timestamp - this.absoluteStart
+    // }
+
+    // public resume = () => {
+    //     this.tracking = true
+    //     // set absoluteStart to the difference in time
+    //     this.absoluteStart = new Date().getTime() - this.absoluteStart
+    // }
+
+    private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
+        // ensure we are recording
+        if (!this.tracking) {
+            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')
+        }
+      
+      // TO FIX: bob
+      // console.debug('track movment')
+
+        // get the time
+        const time = new Date().getTime() - this.absoluteStart
+        // make new movement object
+        const movement: Movement = { time, panX, panY, scale }
+        
+        // add that movement to the current presentation data's movement array
+        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
     }
 
-    if (!this._isPlaying) {
-      //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
-      return
+    // instance variable for the FFView
+    private disposeFunc: IReactionDisposer | null;
+    private recordingFFView: CollectionFreeFormView | null;
+    
+    // set the FFView that will be used in a reaction to track the movements
+    public setRecordingFFView = (view: CollectionFreeFormView): void => {
+        // set the view to the current view
+        if (view === this.recordingFFView || view == null) return;
+        
+        // this.recordingFFView = view;
+        // set the reaction to track the movements
+        this.disposeFunc = reaction(
+            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
+            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
+        )
+
+        // for now, set the most recent recordingFFView to the playFFView
+        this.recordingFFView = view;
     }
-    this._isPlaying = false
-    // TODO: set userdoc presentMode to browsing
-    this.timers?.map(timer => clearTimeout(timer))
-
-    // this.videoBox = null;
-  }
-
-  private videoBox: VideoBox | null = null;
-
-  // by calling pause on the VideoBox, the pauseMovements will be called
-  public pauseVideoAndMovements = (): boolean => {
-    this.videoBox?.Pause()
-
-    this.pauseMovements()
-    return this.videoBox == null
-  }
 
-  public _isPlaying = false;
+    // TODO: extract this into different class with pause and resume recording
+    // TODO: store the FFview with the movements
+    private playFFView: CollectionFreeFormView | null;
+    private timers: NodeJS.Timeout[] | null;
 
-  public 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')
+    public setPlayFFView = (view: CollectionFreeFormView): void => {
+        this.playFFView = view
     }
-    if (this._isPlaying) return;
 
-    this._isPlaying = true;
-    Doc.UserDoc().presentationMode = 'watching';
-
-    // TODO: consider this bug at the end of the clip on seek
-    this.videoBox = videoBox || null;
-
-    // only get the movements that are remaining in the video time left
-    const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+    // pausing movements will dispose all timers that are planned to replay the movements
+    // play movemvents will recreate them when the user resumes the presentation
+    public pauseMovements = (): undefined | Error => {
+        if (this.playFFView === null) {
+            return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+        }
+
+        if (!this._isPlaying) {
+            //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+            return
+        }
+        this._isPlaying = false
+        // TODO: set userdoc presentMode to browsing
+        this.timers?.map(timer => clearTimeout(timer))
+
+        // this.videoBox = null;
+    }
+    
+    private videoBox: VideoBox | null = null;
 
-    // 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;
+    // by calling pause on the VideoBox, the pauseMovements will be called
+    public pauseVideoAndMovements = (): boolean => {
+        this.videoBox?.Pause()
 
-      preScale = scale;
+        this.pauseMovements()
+        return this.videoBox == null
     }
 
-    // set the first frame to be at the start of the pres
-    zoomAndPan(filteredMovements[0]);
-
-    // make timers that will execute each movement at the correct replay time
-    this.timers = filteredMovements.map(movement => {
-      const timeDiff = movement.time - timeViewed * 1000
-      return setTimeout(() => {
-        // replay the movement
-        zoomAndPan(movement)
-        // if last movement, presentation is done -> set the instance var
-        if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
-      }, timeDiff)
-    })
-  }
-
+    public _isPlaying = false;
+
+    public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
+        if (presentation.movements === null || this.playFFView === null) {
+            return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+        }
+        if(this._isPlaying) return;
+
+        this._isPlaying = true;
+        Doc.UserDoc().presentationMode = 'watching';
+
+        // TODO: consider this bug at the end of the clip on seek
+        this.videoBox = videoBox || null;
+
+        // only get the movements that are remaining in the video time left
+        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+        // helper to replay a movement
+        const document = this.playFFView
+        let preScale = -1;
+        const zoomAndPan = (movement: Movement) => {
+            const { panX, panY, scale } = movement;
+            (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+            document.Document._panX = panX;
+            document.Document._panY = panY;
+
+            preScale = scale;
+        }
+
+        // set the first frame to be at the start of the pres
+        zoomAndPan(filteredMovements[0]);
+
+        // make timers that will execute each movement at the correct replay time
+        this.timers = filteredMovements.map(movement => {
+            const timeDiff = movement.time - timeViewed*1000
+            return setTimeout(() => {
+                // replay the movement
+                zoomAndPan(movement)
+                // if last movement, presentation is done -> set the instance var
+                if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
+            }, timeDiff)
+        })
+    }
+  
   // method that concatenates an array of presentatations into one
   public concatPresentations = (presentations: Presentation[]): Presentation => {
     // these three will lead to the combined presentation
@@ -251,7 +295,6 @@ export class RecordingApi {
 
     presentations.forEach((presentation) => {
       const { movements, totalTime, meta } = presentation;
-
       // update movements if they had one
       if (movements) {
         // add the summed time to the movements
@@ -259,10 +302,8 @@ export class RecordingApi {
         // concat the movements already in the combined presentation with these new ones
         combinedMovements.push(...addedTimeMovements);
       }
-
       // update the totalTime
       sumTime += totalTime;
-
       // concatenate the metas
       combinedMetas.push(...meta);
     });
@@ -271,19 +312,19 @@ export class RecordingApi {
     return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
   }
 
-  // Unfinished code for tracing multiple free form views
-  // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
-
-  // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
-  //     pres.set(ffView,
-  //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
-  //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
-  //     )
-  // }
-
-  // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
-  //     const disposer = pres.get(ffView);
-  //     disposer?.();
-  //     pres.delete(ffView)
-  // }
+    // Unfinished code for tracing multiple free form views
+    // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
+
+    // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
+    //     pres.set(ffView,
+    //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
+    //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
+    //     )
+    // }
+
+    // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
+    //     const disposer = pres.get(ffView);
+    //     disposer?.();
+    //     pres.delete(ffView)
+    // }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index a28677525..6fe67b6db 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -46,7 +46,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
-            // stringify the presentation and store it
+            // stringify the presenation and store it
             presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
       }
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index d4d19f3d8..ba9479f41 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -141,6 +141,7 @@ export function RecordingView(props: IRecordingViewProps) {
     }
 
     videoRecorder.current.onstop = () => {
+      // RecordingApi.Instance.stop();
       // if we have a last portion
       if (videoChunks.length > 1) {
         // append the current portion to the video pieces
@@ -149,10 +150,9 @@ export function RecordingView(props: IRecordingViewProps) {
           endTime: recordingTimerRef.current,
           startTime: videos?.lastElement()?.endTime || 0
         };
-
         // depending on if a presenation exists, add it to the video
-        const presentation = RecordingApi.Instance.yieldPresentation();
-        setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+        const { done: presError, value: presentation } = RecordingApi.Instance.yieldPresentation().next();
+        setVideos(videos => [...videos, (!presError && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
       }
 
       // reset the temporary chunks
@@ -175,8 +175,8 @@ export function RecordingView(props: IRecordingViewProps) {
     const stream = videoElementRef.current!.srcObject;
     stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
 
-    // finish/clear the recoringApi
-    RecordingApi.Instance.finish();
+    // clear the recoringApi
+    RecordingApi.Instance.clear();
 
     // this will call upon progessbar to update videos to be in the correct order
     setFinished(true);
@@ -189,10 +189,12 @@ export function RecordingView(props: IRecordingViewProps) {
   }
 
   const start = (e: React.PointerEvent) => {
+    // the code to start or resume does not get triggered if we start dragging the button
     setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-      // start recording if not already recording
-      if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
-
+      if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+        record();
+        // trackScreen &&
+      }
       return true; // cancels propagation to documentView to avoid selecting it.
     }, false, false);
   }
-- 
cgit v1.2.3-70-g09d2


From c7e4bf8c4e19669666cce610020969cf0d865e49 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Mon, 13 Jun 2022 14:57:34 -0400
Subject: Revert "debug w bob"

This reverts commit 1dfb694ffa8f34a834387eb073ffb9bd3a678039.

return to stable version without printing the logs.
---
 src/client/util/RecordingApi.ts                    | 385 +++++++++------------
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   2 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  29 +-
 3 files changed, 186 insertions(+), 230 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index 4c8f55ce5..d56093cee 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -5,10 +5,10 @@ import { Doc } from "../../fields/Doc";
 import { VideoBox } from "../views/nodes/VideoBox";
 
 type Movement = {
-    time: number,
-    panX: number,
-    panY: number,
-    scale: number,
+  time: number,
+  panX: number,
+  panY: number,
+  scale: number,
 }
 
 export type Presentation = {
@@ -20,10 +20,9 @@ export type Presentation = {
 export class RecordingApi {
 
   private static get NULL_PRESENTATION(): Presentation {
-    return { movements: null, meta: {}, totalTime: -1,}
+    return { movements: null, meta: {}, totalTime: -1, }
   }
 
-<<<<<<< HEAD
   // instance variables
   private currentPresentation: Presentation;
   private tracking: boolean;
@@ -49,78 +48,54 @@ export class RecordingApi {
     // for now, set playFFView
     this.playFFView = null;
     this.timers = null;
+
+    // put a pointerdown event on the doucment to see what the target
   }
-=======
-    // instance variables
-    private currentPresentation: Presentation;
-    private tracking: boolean;
-    private absoluteStart: number;
-
-
-    // create static instance and getter for global use
-    @observable static _instance: RecordingApi;
-    public static get Instance(): RecordingApi { return RecordingApi._instance }
-    public constructor() {
-        // init the global instance
-        RecordingApi._instance = this;
-
-        // init the instance variables
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        this.tracking = false;
-        this.absoluteStart = -1;
-
-        // used for tracking movements in the view frame
-        this.disposeFunc = null;
-        this.recordingFFView = null;
-
-        // for now, set playFFView
-        this.playFFView = null;
-        this.timers = null;
-      
-      // put a pointerdown event on the doucment to see what the target
-    }
->>>>>>> parent of d91ff1f93 (fix bugs with selecting tracking. very stable now.)
 
-    // little helper :)
-    private get isInitPresenation(): boolean {
-        return this.currentPresentation.movements === null
-    }
+  // little helper :)
+  private get nullPresentation(): boolean {
+    return this.currentPresentation.movements === null
+  }
 
-    public start = (meta?: Object) => {
-      // update the presentation mode
-      Doc.UserDoc().presentationMode = 'recording';
-
-        // (1a) get start date for presenation
-      const startDate = new Date();
-        // (1b) set start timestamp to absolute timestamp
-      this.absoluteStart = startDate.getTime();
-
-        // (2) assign meta content if it exists
-        this.currentPresentation.meta = meta || {}
-        // (3) assign start date to currentPresenation
-        this.currentPresentation.movements = []
-        // (4) set tracking true to allow trackMovements
-        this.tracking = true
-    }
+  public start = (meta?: Object) => {
+    // update the presentation mode
+    Doc.UserDoc().presentationMode = 'recording';
+
+    // (1a) get start date for presenation
+    const startDate = new Date();
+    // (1b) set start timestamp to absolute timestamp
+    this.absoluteStart = startDate.getTime();
+
+    // (2) assign meta content if it exists
+    this.currentPresentation.meta = meta || {}
+    // (3) assign start date to currentPresenation
+    this.currentPresentation.movements = []
+    // (4) set tracking true to allow trackMovements
+    this.tracking = true
+  }
 
-  /* stops the video and returns the presentatation; if no presentation, returns undefined */  
-  public * yieldPresentation(clearData: boolean = true): Generator<Presentation | null> {
+  /* stops the video and returns the presentatation; if no presentation, returns undefined */
+  public yieldPresentation(clearData: boolean = true): Presentation | null {
     // if no presentation or done tracking, return null
-    if (!this.isInitPresenation || !this.tracking) return null;
+    if (this.nullPresentation || !this.tracking) return null;
 
     // set the previus recording view to the play view
     this.playFFView = this.recordingFFView;
 
     // ensure we add the endTime now that they are done recording    
-    yield { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+    const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
 
     // reset the current presentation
     clearData && this.clear();
+
+    return cpy;
   }
 
-  public stop = (): void => {
+  public finish = (): void => {
     // make is tracking false
     this.tracking = false
+    // reset the RecordingApi instance
+    this.clear();
   }
 
   public clear = (): void => {
@@ -137,155 +112,138 @@ export class RecordingApi {
     // clear absoluteStart
     this.absoluteStart = -1
   }
-  
-    // call on dispose function to stop tracking movements
-    public removeRecordingFFView = (): void => {
-        this.disposeFunc?.();
-        this.disposeFunc = null;
-    }
 
-    // public pause = (): Error | undefined => {
-    //     if (this.isInitPresenation) {
-    //         console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
-    //         return new Error('[recordingApi.ts] pause(): no presentation')
-    //     }
-    //     // don't allow track movments
-    //     this.tracking = false
-
-    //     // set adjust absoluteStart to add the time difference
-    //     const timestamp = new Date().getTime()
-    //     this.absoluteStart = timestamp - this.absoluteStart
-    // }
-
-    // public resume = () => {
-    //     this.tracking = true
-    //     // set absoluteStart to the difference in time
-    //     this.absoluteStart = new Date().getTime() - this.absoluteStart
-    // }
-
-    private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
-        // ensure we are recording
-        if (!this.tracking) {
-            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')
-        }
-      
-      // TO FIX: bob
-      // console.debug('track movment')
-
-        // get the time
-        const time = new Date().getTime() - this.absoluteStart
-        // make new movement object
-        const movement: Movement = { time, panX, panY, scale }
-        
-        // add that movement to the current presentation data's movement array
-        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
-    }
+  // call on dispose function to stop tracking movements
+  public removeRecordingFFView = (): void => {
+    this.disposeFunc?.();
+    this.disposeFunc = null;
+  }
 
-    // instance variable for the FFView
-    private disposeFunc: IReactionDisposer | null;
-    private recordingFFView: CollectionFreeFormView | null;
-    
-    // set the FFView that will be used in a reaction to track the movements
-    public setRecordingFFView = (view: CollectionFreeFormView): void => {
-        // set the view to the current view
-        if (view === this.recordingFFView || view == null) return;
-        
-        // this.recordingFFView = view;
-        // set the reaction to track the movements
-        this.disposeFunc = reaction(
-            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
-            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
-        )
-
-        // for now, set the most recent recordingFFView to the playFFView
-        this.recordingFFView = view;
+  private trackMovements = (panX: number, panY: number, scale: number = 0) => {
+    // ensure we are recording
+    if (!this.tracking) {
+      console.error('[recordingApi.ts] trackMovements(): tracking is false')
+      return;
+    }
+    // check to see if the presetation is init
+    if (this.nullPresentation) {
+      console.error('[recordingApi.ts] trackMovements(): no presentation')
+      return;
     }
 
-    // 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;
+    // TO FIX: bob
+    // console.debug('track movment')
+
+    // get the time
+    const time = new Date().getTime() - this.absoluteStart
+    // make new movement object
+    const movement: Movement = { time, panX, panY, scale }
+
+    // add that movement to the current presentation data's movement array
+    this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
+  }
+
+  // instance variable for the FFView
+  private disposeFunc: IReactionDisposer | null;
+  private recordingFFView: CollectionFreeFormView | null;
+
+  // set the FFView that will be used in a reaction to track the movements
+  public setRecordingFFView = (view: CollectionFreeFormView): void => {
+    // set the view to the current view
+    if (view === this.recordingFFView || view == null) return;
+
+    // this.recordingFFView = view;
+    // set the reaction to track the movements
+    this.disposeFunc = reaction(
+      () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
+      (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
+    )
+
+    // for now, set the most recent recordingFFView to the playFFView
+    this.recordingFFView = view;
+  }
+
+  // TODO: extract this into different class with pause and resume recording
+  // TODO: store the FFview with the movements
+  private playFFView: CollectionFreeFormView | null;
+  private timers: NodeJS.Timeout[] | null;
 
-    public setPlayFFView = (view: CollectionFreeFormView): void => {
-        this.playFFView = view
+  public setPlayFFView = (view: CollectionFreeFormView): void => {
+    this.playFFView = view
+  }
+
+  // pausing movements will dispose all timers that are planned to replay the movements
+  // play movemvents will recreate them when the user resumes the presentation
+  public pauseMovements = (): undefined | Error => {
+    if (this.playFFView === null) {
+      return new Error('[recordingApi.ts] pauseMovements() failed: no view')
     }
 
-    // pausing movements will dispose all timers that are planned to replay the movements
-    // play movemvents will recreate them when the user resumes the presentation
-    public pauseMovements = (): undefined | Error => {
-        if (this.playFFView === null) {
-            return new Error('[recordingApi.ts] pauseMovements() failed: no view')
-        }
-
-        if (!this._isPlaying) {
-            //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
-            return
-        }
-        this._isPlaying = false
-        // TODO: set userdoc presentMode to browsing
-        this.timers?.map(timer => clearTimeout(timer))
-
-        // this.videoBox = null;
+    if (!this._isPlaying) {
+      //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+      return
     }
-    
-    private videoBox: VideoBox | null = null;
+    this._isPlaying = false
+    // TODO: set userdoc presentMode to browsing
+    this.timers?.map(timer => clearTimeout(timer))
+
+    // this.videoBox = null;
+  }
+
+  private videoBox: VideoBox | null = null;
+
+  // by calling pause on the VideoBox, the pauseMovements will be called
+  public pauseVideoAndMovements = (): boolean => {
+    this.videoBox?.Pause()
+
+    this.pauseMovements()
+    return this.videoBox == null
+  }
 
-    // by calling pause on the VideoBox, the pauseMovements will be called
-    public pauseVideoAndMovements = (): boolean => {
-        this.videoBox?.Pause()
+  public _isPlaying = false;
 
-        this.pauseMovements()
-        return this.videoBox == 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;
 
-    public _isPlaying = false;
-
-    public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
-        if (presentation.movements === null || this.playFFView === null) {
-            return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
-        }
-        if(this._isPlaying) return;
-
-        this._isPlaying = true;
-        Doc.UserDoc().presentationMode = 'watching';
-
-        // TODO: consider this bug at the end of the clip on seek
-        this.videoBox = videoBox || null;
-
-        // only get the movements that are remaining in the video time left
-        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
-
-        // helper to replay a movement
-        const document = this.playFFView
-        let preScale = -1;
-        const zoomAndPan = (movement: Movement) => {
-            const { panX, panY, scale } = movement;
-            (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
-            document.Document._panX = panX;
-            document.Document._panY = panY;
-
-            preScale = scale;
-        }
-
-        // set the first frame to be at the start of the pres
-        zoomAndPan(filteredMovements[0]);
-
-        // make timers that will execute each movement at the correct replay time
-        this.timers = filteredMovements.map(movement => {
-            const timeDiff = movement.time - timeViewed*1000
-            return setTimeout(() => {
-                // replay the movement
-                zoomAndPan(movement)
-                // if last movement, presentation is done -> set the instance var
-                if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
-            }, timeDiff)
-        })
+    this._isPlaying = true;
+    Doc.UserDoc().presentationMode = 'watching';
+
+    // TODO: consider this bug at the end of the clip on seek
+    this.videoBox = videoBox || null;
+
+    // only get the movements that are remaining in the video time left
+    const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+    // helper to replay a movement
+    const document = this.playFFView
+    let preScale = -1;
+    const zoomAndPan = (movement: Movement) => {
+      const { panX, panY, scale } = movement;
+      (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+      document.Document._panX = panX;
+      document.Document._panY = panY;
+
+      preScale = scale;
     }
-  
+
+    // set the first frame to be at the start of the pres
+    zoomAndPan(filteredMovements[0]);
+
+    // make timers that will execute each movement at the correct replay time
+    this.timers = filteredMovements.map(movement => {
+      const timeDiff = movement.time - timeViewed * 1000
+      return setTimeout(() => {
+        // replay the movement
+        zoomAndPan(movement)
+        // if last movement, presentation is done -> set the instance var
+        if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
+      }, timeDiff)
+    })
+  }
+
   // method that concatenates an array of presentatations into one
   public concatPresentations = (presentations: Presentation[]): Presentation => {
     // these three will lead to the combined presentation
@@ -295,6 +253,7 @@ export class RecordingApi {
 
     presentations.forEach((presentation) => {
       const { movements, totalTime, meta } = presentation;
+
       // update movements if they had one
       if (movements) {
         // add the summed time to the movements
@@ -302,8 +261,10 @@ export class RecordingApi {
         // concat the movements already in the combined presentation with these new ones
         combinedMovements.push(...addedTimeMovements);
       }
+
       // update the totalTime
       sumTime += totalTime;
+
       // concatenate the metas
       combinedMetas.push(...meta);
     });
@@ -312,19 +273,19 @@ export class RecordingApi {
     return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
   }
 
-    // Unfinished code for tracing multiple free form views
-    // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
-
-    // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
-    //     pres.set(ffView,
-    //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
-    //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
-    //     )
-    // }
-
-    // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
-    //     const disposer = pres.get(ffView);
-    //     disposer?.();
-    //     pres.delete(ffView)
-    // }
+  // Unfinished code for tracing multiple free form views
+  // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
+
+  // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
+  //     pres.set(ffView,
+  //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
+  //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
+  //     )
+  // }
+
+  // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
+  //     const disposer = pres.get(ffView);
+  //     disposer?.();
+  //     pres.delete(ffView)
+  // }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 6fe67b6db..a28677525 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -46,7 +46,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
-            // stringify the presenation and store it
+            // stringify the presentation and store it
             presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
       }
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index ba9479f41..15883632a 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -60,7 +60,7 @@ export function RecordingView(props: IRecordingViewProps) {
     }
   }
 
-  useEffect(() => console.log('progress', progress), [progress])
+  // useEffect(() => console.debug('progress', progress), [progress])
 
   useEffect(() => {
     if (finished) {
@@ -93,8 +93,7 @@ export function RecordingView(props: IRecordingViewProps) {
   useEffect(() => {
     let interval: any = null;
     if (recording) {
-      interval = setTimeout(() => {
-        console.log('update interval')
+      interval = setInterval(() => {
         setRecordingTimer(unit => unit + 1);
       }, 10);
     } else if (!recording && recordingTimer !== 0) {
@@ -141,7 +140,6 @@ export function RecordingView(props: IRecordingViewProps) {
     }
 
     videoRecorder.current.onstop = () => {
-      // RecordingApi.Instance.stop();
       // if we have a last portion
       if (videoChunks.length > 1) {
         // append the current portion to the video pieces
@@ -150,9 +148,10 @@ export function RecordingView(props: IRecordingViewProps) {
           endTime: recordingTimerRef.current,
           startTime: videos?.lastElement()?.endTime || 0
         };
+
         // depending on if a presenation exists, add it to the video
-        const { done: presError, value: presentation } = RecordingApi.Instance.yieldPresentation().next();
-        setVideos(videos => [...videos, (!presError && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+        const presentation = RecordingApi.Instance.yieldPresentation();
+        setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
       }
 
       // reset the temporary chunks
@@ -175,8 +174,8 @@ export function RecordingView(props: IRecordingViewProps) {
     const stream = videoElementRef.current!.srcObject;
     stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
 
-    // clear the recoringApi
-    RecordingApi.Instance.clear();
+    // finish/clear the recoringApi
+    RecordingApi.Instance.finish();
 
     // this will call upon progessbar to update videos to be in the correct order
     setFinished(true);
@@ -189,12 +188,10 @@ export function RecordingView(props: IRecordingViewProps) {
   }
 
   const start = (e: React.PointerEvent) => {
-    // the code to start or resume does not get triggered if we start dragging the button
     setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-      if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
-        record();
-        // trackScreen &&
-      }
+      // start recording if not already recording
+      if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
+
       return true; // cancels propagation to documentView to avoid selecting it.
     }, false, false);
   }
@@ -204,9 +201,7 @@ export function RecordingView(props: IRecordingViewProps) {
     setDoUndo(prev => !prev);
   }
 
-  const handleOnTimeUpdate = () => {
-    playing && setVideoProgressHelper(videoElementRef.current!.currentTime);
-  };
+  const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
 
   const millisecondToMinuteSecond = (milliseconds: number) => {
     const toTwoDigit = (digit: number) => {
@@ -223,7 +218,7 @@ export function RecordingView(props: IRecordingViewProps) {
         <video id={`video-${props.id}`}
           autoPlay
           muted
-          onTimeUpdate={handleOnTimeUpdate}
+          onTimeUpdate={() => handleOnTimeUpdate()}
           ref={videoElementRef}
         />
         <div className="recording-sign">
-- 
cgit v1.2.3-70-g09d2


From d3bf3086d95ff6f895614d0942b84752662c1373 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>
Date: Mon, 13 Jun 2022 15:10:22 -0400
Subject: formatting and removing logs

---
 src/client/util/RecordingApi.ts                    | 506 ++++++++++-----------
 .../views/nodes/RecordingBox/RecordingView.tsx     | 443 +++++++++---------
 2 files changed, 472 insertions(+), 477 deletions(-)

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

diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index d56093cee..12ca07d67 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -3,289 +3,285 @@ import { IReactionDisposer, observable, reaction } from "mobx";
 import { NumCast } from "../../fields/Types";
 import { Doc } from "../../fields/Doc";
 import { VideoBox } from "../views/nodes/VideoBox";
+import { isArray } from "lodash";
 
 type Movement = {
-  time: number,
-  panX: number,
-  panY: number,
-  scale: number,
+    time: number,
+    panX: number,
+    panY: number,
+    scale: number,
 }
 
 export type Presentation = {
-  movements: Movement[] | null,
-  totalTime: number,
-  meta: any,
+    movements: Movement[] | null,
+    totalTime: number,
+    meta: Object | Object[],
 }
 
 export class RecordingApi {
 
-  private static get NULL_PRESENTATION(): Presentation {
-    return { movements: null, meta: {}, totalTime: -1, }
-  }
-
-  // instance variables
-  private currentPresentation: Presentation;
-  private tracking: boolean;
-  private absoluteStart: number;
-
-
-  // create static instance and getter for global use
-  @observable static _instance: RecordingApi;
-  public static get Instance(): RecordingApi { return RecordingApi._instance }
-  public constructor() {
-    // init the global instance
-    RecordingApi._instance = this;
-
-    // init the instance variables
-    this.currentPresentation = RecordingApi.NULL_PRESENTATION
-    this.tracking = false;
-    this.absoluteStart = -1;
-
-    // used for tracking movements in the view frame
-    this.disposeFunc = null;
-    this.recordingFFView = null;
-
-    // for now, set playFFView
-    this.playFFView = null;
-    this.timers = null;
-
-    // put a pointerdown event on the doucment to see what the target
-  }
-
-  // little helper :)
-  private get nullPresentation(): boolean {
-    return this.currentPresentation.movements === null
-  }
-
-  public start = (meta?: Object) => {
-    // update the presentation mode
-    Doc.UserDoc().presentationMode = 'recording';
-
-    // (1a) get start date for presenation
-    const startDate = new Date();
-    // (1b) set start timestamp to absolute timestamp
-    this.absoluteStart = startDate.getTime();
-
-    // (2) assign meta content if it exists
-    this.currentPresentation.meta = meta || {}
-    // (3) assign start date to currentPresenation
-    this.currentPresentation.movements = []
-    // (4) set tracking true to allow trackMovements
-    this.tracking = true
-  }
-
-  /* stops the video and returns the presentatation; if no presentation, returns undefined */
-  public yieldPresentation(clearData: boolean = true): Presentation | null {
-    // if no presentation or done tracking, return null
-    if (this.nullPresentation || !this.tracking) return null;
-
-    // set the previus recording view to the play view
-    this.playFFView = this.recordingFFView;
-
-    // ensure we add the endTime now that they are done recording    
-    const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
-
-    // reset the current presentation
-    clearData && this.clear();
-
-    return cpy;
-  }
-
-  public finish = (): void => {
-    // make is tracking false
-    this.tracking = false
-    // reset the RecordingApi instance
-    this.clear();
-  }
-
-  public clear = (): void => {
-    // clear the disposeFunc if we are done (not tracking)
-    if (!this.tracking) {
-      this.removeRecordingFFView();
-      // update the presentation mode now that we are done tracking
-      Doc.UserDoc().presentationMode = 'none';
+    private static get NULL_PRESENTATION(): Presentation {
+        return { movements: null, meta: {}, totalTime: -1, }
     }
-    // clear presenation data
-    this.currentPresentation = RecordingApi.NULL_PRESENTATION
-    // clear isRecording
-    // this.tracking = false
-    // clear absoluteStart
-    this.absoluteStart = -1
-  }
-
-  // call on dispose function to stop tracking movements
-  public removeRecordingFFView = (): void => {
-    this.disposeFunc?.();
-    this.disposeFunc = null;
-  }
-
-  private trackMovements = (panX: number, panY: number, scale: number = 0) => {
-    // ensure we are recording
-    if (!this.tracking) {
-      console.error('[recordingApi.ts] trackMovements(): tracking is false')
-      return;
+
+    // instance variables
+    private currentPresentation: Presentation;
+    private tracking: boolean;
+    private absoluteStart: number;
+
+
+    // create static instance and getter for global use
+    @observable static _instance: RecordingApi;
+    public static get Instance(): RecordingApi { return RecordingApi._instance }
+    public constructor() {
+        // init the global instance
+        RecordingApi._instance = this;
+
+        // init the instance variables
+        this.currentPresentation = RecordingApi.NULL_PRESENTATION
+        this.tracking = false;
+        this.absoluteStart = -1;
+
+        // used for tracking movements in the view frame
+        this.disposeFunc = null;
+        this.recordingFFView = null;
+
+        // for now, set playFFView
+        this.playFFView = null;
+        this.timers = null;
+    }
+
+    // little helper :)
+    private get nullPresentation(): boolean {
+        return this.currentPresentation.movements === null
+    }
+
+    public start = (meta?: Object) => {
+        // update the presentation mode
+        Doc.UserDoc().presentationMode = 'recording';
+
+        // (1a) get start date for presenation
+        const startDate = new Date();
+        // (1b) set start timestamp to absolute timestamp
+        this.absoluteStart = startDate.getTime();
+
+        // (2) assign meta content if it exists
+        this.currentPresentation.meta = meta || {}
+        // (3) assign start date to currentPresenation
+        this.currentPresentation.movements = []
+        // (4) set tracking true to allow trackMovements
+        this.tracking = true
+    }
+
+    /* stops the video and returns the presentatation; if no presentation, returns undefined */
+    public yieldPresentation(clearData: boolean = true): Presentation | null {
+        // if no presentation or done tracking, return null
+        if (this.nullPresentation || !this.tracking) return null;
+
+        // set the previus recording view to the play view
+        this.playFFView = this.recordingFFView;
+
+        // ensure we add the endTime now that they are done recording    
+        const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart };
+
+        // reset the current presentation
+        clearData && this.clear();
+
+        return cpy;
+    }
+
+    public finish = (): void => {
+        // make is tracking false
+        this.tracking = false
+        // reset the RecordingApi instance
+        this.clear();
     }
-    // check to see if the presetation is init
-    if (this.nullPresentation) {
-      console.error('[recordingApi.ts] trackMovements(): no presentation')
-      return;
+
+    public clear = (): void => {
+        // clear the disposeFunc if we are done (not tracking)
+        if (!this.tracking) {
+            this.removeRecordingFFView();
+            // update the presentation mode now that we are done tracking
+            Doc.UserDoc().presentationMode = 'none';
+        }
+        // clear presenation data
+        this.currentPresentation = RecordingApi.NULL_PRESENTATION
+        // clear isRecording
+        // this.tracking = false
+        // clear absoluteStart
+        this.absoluteStart = -1
     }
 
-    // TO FIX: bob
-    // console.debug('track movment')
-
-    // get the time
-    const time = new Date().getTime() - this.absoluteStart
-    // make new movement object
-    const movement: Movement = { time, panX, panY, scale }
-
-    // add that movement to the current presentation data's movement array
-    this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
-  }
-
-  // instance variable for the FFView
-  private disposeFunc: IReactionDisposer | null;
-  private recordingFFView: CollectionFreeFormView | null;
-
-  // set the FFView that will be used in a reaction to track the movements
-  public setRecordingFFView = (view: CollectionFreeFormView): void => {
-    // set the view to the current view
-    if (view === this.recordingFFView || view == null) return;
-
-    // this.recordingFFView = view;
-    // set the reaction to track the movements
-    this.disposeFunc = reaction(
-      () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
-      (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
-    )
-
-    // for now, set the most recent recordingFFView to the playFFView
-    this.recordingFFView = view;
-  }
-
-  // TODO: extract this into different class with pause and resume recording
-  // TODO: store the FFview with the movements
-  private playFFView: CollectionFreeFormView | null;
-  private timers: NodeJS.Timeout[] | null;
-
-  public setPlayFFView = (view: CollectionFreeFormView): void => {
-    this.playFFView = view
-  }
-
-  // pausing movements will dispose all timers that are planned to replay the movements
-  // play movemvents will recreate them when the user resumes the presentation
-  public pauseMovements = (): undefined | Error => {
-    if (this.playFFView === null) {
-      return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+    // call on dispose function to stop tracking movements
+    public removeRecordingFFView = (): void => {
+        this.disposeFunc?.();
+        this.disposeFunc = null;
     }
 
-    if (!this._isPlaying) {
-      //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
-      return
+    private trackMovements = (panX: number, panY: number, scale: number = 0) => {
+        // ensure we are recording
+        if (!this.tracking) {
+            console.error('[recordingApi.ts] trackMovements(): tracking is false')
+            return;
+        }
+        // check to see if the presetation is init
+        if (this.nullPresentation) {
+            console.error('[recordingApi.ts] trackMovements(): no presentation')
+            return;
+        }
+
+        // get the time
+        const time = new Date().getTime() - this.absoluteStart
+        // make new movement object
+        const movement: Movement = { time, panX, panY, scale }
+
+        // add that movement to the current presentation data's movement array
+        this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
     }
-    this._isPlaying = false
-    // TODO: set userdoc presentMode to browsing
-    this.timers?.map(timer => clearTimeout(timer))
 
-    // this.videoBox = null;
-  }
+    // instance variable for the FFView
+    private disposeFunc: IReactionDisposer | null;
+    private recordingFFView: CollectionFreeFormView | null;
+
+    // set the FFView that will be used in a reaction to track the movements
+    public setRecordingFFView = (view: CollectionFreeFormView): void => {
+        // set the view to the current view
+        if (view === this.recordingFFView || view == null) return;
 
-  private videoBox: VideoBox | null = null;
+        // this.recordingFFView = view;
+        // set the reaction to track the movements
+        this.disposeFunc = reaction(
+            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
+            (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, res.scale)
+        )
 
-  // by calling pause on the VideoBox, the pauseMovements will be called
-  public pauseVideoAndMovements = (): boolean => {
-    this.videoBox?.Pause()
+        // for now, set the most recent recordingFFView to the playFFView
+        this.recordingFFView = view;
+    }
 
-    this.pauseMovements()
-    return this.videoBox == null
-  }
+    // TODO: extract this into different class with pause and resume recording
+    // TODO: store the FFview with the movements
+    private playFFView: CollectionFreeFormView | null;
+    private timers: NodeJS.Timeout[] | null;
 
-  public _isPlaying = false;
+    public setPlayFFView = (view: CollectionFreeFormView): void => {
+        this.playFFView = view
+    }
 
-  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')
+    // pausing movements will dispose all timers that are planned to replay the movements
+    // play movemvents will recreate them when the user resumes the presentation
+    public pauseMovements = (): undefined | Error => {
+        if (this.playFFView === null) {
+            return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+        }
+
+        if (!this._isPlaying) {
+            //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+            return
+        }
+        this._isPlaying = false
+        // TODO: set userdoc presentMode to browsing
+        this.timers?.map(timer => clearTimeout(timer))
+
+        // this.videoBox = null;
     }
-    if (this._isPlaying) return;
 
-    this._isPlaying = true;
-    Doc.UserDoc().presentationMode = 'watching';
+    private videoBox: VideoBox | null = null;
 
-    // TODO: consider this bug at the end of the clip on seek
-    this.videoBox = videoBox || null;
+    // by calling pause on the VideoBox, the pauseMovements will be called
+    public pauseVideoAndMovements = (): boolean => {
+        this.videoBox?.Pause()
 
-    // only get the movements that are remaining in the video time left
-    const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+        this.pauseMovements()
+        return this.videoBox == null
+    }
 
-    // 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;
+    public _isPlaying = false;
+
+    public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
+        if (presentation.movements === null || this.playFFView === null) {
+            return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+        }
+        if (this._isPlaying) return;
+
+        this._isPlaying = true;
+        Doc.UserDoc().presentationMode = 'watching';
+
+        // TODO: consider this bug at the end of the clip on seek
+        this.videoBox = videoBox || null;
+
+        // only get the movements that are remaining in the video time left
+        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+        // helper to replay a movement
+        const document = this.playFFView
+        let preScale = -1;
+        const zoomAndPan = (movement: Movement) => {
+            const { panX, panY, scale } = movement;
+            (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+            document.Document._panX = panX;
+            document.Document._panY = panY;
+
+            preScale = scale;
+        }
+
+        // set the first frame to be at the start of the pres
+        zoomAndPan(filteredMovements[0]);
+
+        // make timers that will execute each movement at the correct replay time
+        this.timers = filteredMovements.map(movement => {
+            const timeDiff = movement.time - timeViewed * 1000
+            return setTimeout(() => {
+                // replay the movement
+                zoomAndPan(movement)
+                // if last movement, presentation is done -> set the instance var
+                if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
+            }, timeDiff)
+        })
+    }
 
-      preScale = scale;
+    // method that concatenates an array of presentatations into one
+    public concatPresentations = (presentations: Presentation[]): Presentation => {
+        // these three will lead to the combined presentation
+        let combinedMovements: Movement[] = [];
+        let sumTime = 0;
+        let combinedMetas: any[] = [];
+
+        presentations.forEach((presentation) => {
+            const { movements, totalTime, meta } = presentation;
+
+            // update movements if they had one
+            if (movements) {
+                // add the summed time to the movements
+                const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
+                // concat the movements already in the combined presentation with these new ones
+                combinedMovements.push(...addedTimeMovements);
+            }
+
+            // update the totalTime
+            sumTime += totalTime;
+
+            // concatenate the metas
+            combinedMetas.push(meta);
+        });
+
+        // return the combined presentation with the updated total summed time
+		return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
     }
 
-    // set the first frame to be at the start of the pres
-    zoomAndPan(filteredMovements[0]);
-
-    // make timers that will execute each movement at the correct replay time
-    this.timers = filteredMovements.map(movement => {
-      const timeDiff = movement.time - timeViewed * 1000
-      return setTimeout(() => {
-        // replay the movement
-        zoomAndPan(movement)
-        // if last movement, presentation is done -> set the instance var
-        if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
-      }, timeDiff)
-    })
-  }
-
-  // method that concatenates an array of presentatations into one
-  public concatPresentations = (presentations: Presentation[]): Presentation => {
-    // these three will lead to the combined presentation
-    let combinedMovements: Movement[] = [];
-    let sumTime = 0;
-    let combinedMetas: any[] = [];
-
-    presentations.forEach((presentation) => {
-      const { movements, totalTime, meta } = presentation;
-
-      // update movements if they had one
-      if (movements) {
-        // add the summed time to the movements
-        const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } });
-        // concat the movements already in the combined presentation with these new ones
-        combinedMovements.push(...addedTimeMovements);
-      }
-
-      // update the totalTime
-      sumTime += totalTime;
-
-      // concatenate the metas
-      combinedMetas.push(...meta);
-    });
-
-    // return the combined presentation with the updated total summed time
-    return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas };
-  }
-
-  // Unfinished code for tracing multiple free form views
-  // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
-
-  // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
-  //     pres.set(ffView,
-  //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
-  //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
-  //     )
-  // }
-
-  // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
-  //     const disposer = pres.get(ffView);
-  //     disposer?.();
-  //     pres.delete(ffView)
-  // }
+    // Unfinished code for tracing multiple free form views
+    // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
+
+    // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
+    //     pres.set(ffView,
+    //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
+    //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
+    //     )
+    // }
+
+    // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
+    //     const disposer = pres.get(ffView);
+    //     disposer?.();
+    //     pres.delete(ffView)
+    // }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 15883632a..ec9838bdd 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -11,262 +11,261 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
 import { Presentation, RecordingApi } from '../../../util/RecordingApi';
 
 export interface MediaSegment {
-  videoChunks: any[],
-  endTime: number,
-  startTime: number,
-  presentation?: Presentation,
+    videoChunks: any[],
+    endTime: number,
+    startTime: number,
+    presentation?: Presentation,
 }
 
 interface IRecordingViewProps {
-  setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
-  setDuration: (seconds: number) => void
-  id: string
+    setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
+    setDuration: (seconds: number) => void
+    id: string
 }
 
 const MAXTIME = 100000;
 
 export function RecordingView(props: IRecordingViewProps) {
 
-  const [recording, setRecording] = useState(false);
-  const recordingTimerRef = useRef<number>(0);
-  const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
-  const [playing, setPlaying] = useState(false);
-  const [progress, setProgress] = useState(0);
-
-  // acts as a "refresh state" to tell progressBar when to undo
-  const [doUndo, setDoUndo] = useState(false);
-  // whether an undo can occur or not
-  const [canUndo, setCanUndo] = useState(false);
-
-  const [videos, setVideos] = useState<MediaSegment[]>([]);
-  const [orderVideos, setOrderVideos] = useState<boolean>(false);
-  const videoRecorder = useRef<MediaRecorder | null>(null);
-  const videoElementRef = useRef<HTMLVideoElement | null>(null);
-
-  const [finished, setFinished] = useState<boolean>(false)
-  const [trackScreen, setTrackScreen] = useState<boolean>(true)
+    const [recording, setRecording] = useState(false);
+    const recordingTimerRef = useRef<number>(0);
+    const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
+    const [playing, setPlaying] = useState(false);
+    const [progress, setProgress] = useState(0);
+
+    // acts as a "refresh state" to tell progressBar when to undo
+    const [doUndo, setDoUndo] = useState(false);
+    // whether an undo can occur or not
+    const [canUndo, setCanUndo] = useState(false);
+
+    const [videos, setVideos] = useState<MediaSegment[]>([]);
+    const [orderVideos, setOrderVideos] = useState<boolean>(false);
+    const videoRecorder = useRef<MediaRecorder | null>(null);
+    const videoElementRef = useRef<HTMLVideoElement | null>(null);
+
+    const [finished, setFinished] = useState<boolean>(false)
+    const [trackScreen, setTrackScreen] = useState<boolean>(true)
+
+
+
+    const DEFAULT_MEDIA_CONSTRAINTS = {
+        video: {
+            width: 1280,
+            height: 720,
+
+        },
+        audio: {
+            echoCancellation: true,
+            noiseSuppression: true,
+            sampleRate: 44100
+        }
+    };
+
+    useEffect(() => {
+        if (finished) {
+            // make the total presentation that'll match the concatted video
+            let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+
+            // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
+            (async () => {
+                const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
+
+                // upload the segments to the server and get their server access paths
+                const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
+                    .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+
+                // concat the segments together using post call
+                const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
+                !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+            })();
+        }
+    }, [videos]);
+
+    // this will call upon the progress bar to edit videos to be in the correct order
+    useEffect(() => {
+        finished && setOrderVideos(true);
+    }, [finished]);
+
+    // check if the browser supports media devices on first load
+    useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
+
+    useEffect(() => {
+        let interval: any = null;
+        if (recording) {
+            interval = setInterval(() => {
+                setRecordingTimer(unit => unit + 1);
+            }, 10);
+        } else if (!recording && recordingTimer !== 0) {
+            clearInterval(interval);
+        }
+        return () => clearInterval(interval);
+    }, [recording]);
+
+    useEffect(() => {
+        setVideoProgressHelper(recordingTimer)
+        recordingTimerRef.current = recordingTimer;
+    }, [recordingTimer]);
+
+    const setVideoProgressHelper = (progress: number) => {
+        const newProgress = (progress / MAXTIME) * 100;
+        setProgress(newProgress);
+    }
 
+    const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
+        const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
 
+        videoElementRef.current!.src = "";
+        videoElementRef.current!.srcObject = stream;
+        videoElementRef.current!.muted = true;
 
-  const DEFAULT_MEDIA_CONSTRAINTS = {
-    video: {
-      width: 1280,
-      height: 720,
-    },
-    audio: {
-      echoCancellation: true,
-      noiseSuppression: true,
-      sampleRate: 44100
+        return stream;
     }
-  }
 
-  // useEffect(() => console.debug('progress', progress), [progress])
+    const record = async () => {
+        // don't need to start a new stream every time we start recording a new segment
+        if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream());
 
-  useEffect(() => {
-    if (finished) {
-      // make the total presentation that'll match the concatted video
-      let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+        // temporary chunks of video
+        let videoChunks: any = [];
 
-      // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
-      (async () => {
-        const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
+        videoRecorder.current.ondataavailable = (event: any) => {
+            if (event.data.size > 0) videoChunks.push(event.data);
+        };
 
-        // upload the segments to the server and get their server access paths
-        const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
-          .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+        videoRecorder.current.onstart = (event: any) => {
+            setRecording(true);
+            // start the recording api when the video recorder starts
+            trackScreen && RecordingApi.Instance.start();
+        };
 
-        // concat the segments together using post call
-        const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
-        !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
-      })();
+        videoRecorder.current.onstop = () => {
+            // if we have a last portion
+            if (videoChunks.length > 1) {
+                // append the current portion to the video pieces
+                const nextVideo = {
+                    videoChunks,
+                    endTime: recordingTimerRef.current,
+                    startTime: videos?.lastElement()?.endTime || 0
+                };
+
+                // depending on if a presenation exists, add it to the video
+                const presentation = RecordingApi.Instance.yieldPresentation();
+                setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+            }
+
+            // reset the temporary chunks
+            videoChunks = [];
+            setRecording(false);
+        }
+
+        videoRecorder.current.start(200);
     }
-  }, [videos])
-
-  // this will call upon the progress bar to edit videos to be in the correct order
-  useEffect(() => {
-    finished && setOrderVideos(true);
-  }, [finished])
-
-  // check if the browser supports media devices on first load
-  useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, [])
-
-  useEffect(() => {
-    let interval: any = null;
-    if (recording) {
-      interval = setInterval(() => {
-        setRecordingTimer(unit => unit + 1);
-      }, 10);
-    } else if (!recording && recordingTimer !== 0) {
-      clearInterval(interval);
-    }
-    return () => clearInterval(interval);
-  }, [recording])
-
-  useEffect(() => {
-    setVideoProgressHelper(recordingTimer)
-    recordingTimerRef.current = recordingTimer;
-  }, [recordingTimer])
-
-  const setVideoProgressHelper = (progress: number) => {
-    const newProgress = (progress / MAXTIME) * 100;
-    setProgress(newProgress)
-  }
 
-  const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
-    const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
 
-    videoElementRef.current!.src = ""
-    videoElementRef.current!.srcObject = stream
-    videoElementRef.current!.muted = true
+    // if this is called, then we're done recording all the segments
+    const finish = (e: React.PointerEvent) => {
+        e.stopPropagation();
 
-    return stream
-  }
+        // call stop on the video recorder if active
+        videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
 
-  const record = async () => {
-    // don't need to start a new stream every time we start recording a new segment
-    if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream())
+        // end the streams (audio/video) to remove recording icon
+        const stream = videoElementRef.current!.srcObject;
+        stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
 
-    // temporary chunks of video
-    let videoChunks: any = []
+        // finish/clear the recoringApi
+        RecordingApi.Instance.finish();
 
-    videoRecorder.current.ondataavailable = (event: any) => {
-      if (event.data.size > 0) videoChunks.push(event.data)
+        // this will call upon progessbar to update videos to be in the correct order
+        setFinished(true);
     }
 
-    videoRecorder.current.onstart = (event: any) => {
-      setRecording(true);
-      // start the recording api when the video recorder starts
-      trackScreen && RecordingApi.Instance.start();
+    const pause = (e: React.PointerEvent) => {
+        e.stopPropagation();
+        // if recording, then this is just a new segment
+        videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
     }
 
-    videoRecorder.current.onstop = () => {
-      // if we have a last portion
-      if (videoChunks.length > 1) {
-        // append the current portion to the video pieces
-        const nextVideo = {
-          videoChunks,
-          endTime: recordingTimerRef.current,
-          startTime: videos?.lastElement()?.endTime || 0
-        };
-
-        // depending on if a presenation exists, add it to the video
-        const presentation = RecordingApi.Instance.yieldPresentation();
-        setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
-      }
+    const start = (e: React.PointerEvent) => {
+        setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
+            // start recording if not already recording
+            if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
 
-      // reset the temporary chunks
-      videoChunks = []
-      setRecording(false);
+            return true; // cancels propagation to documentView to avoid selecting it.
+        }, false, false);
     }
 
-    videoRecorder.current.start(200)
-  }
-
-
-  // if this is called, then we're done recording all the segments
-  const finish = (e: React.PointerEvent) => {
-    e.stopPropagation();
-
-    // call stop on the video recorder if active
-    videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
-
-    // end the streams (audio/video) to remove recording icon
-    const stream = videoElementRef.current!.srcObject;
-    stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
-
-    // finish/clear the recoringApi
-    RecordingApi.Instance.finish();
-
-    // this will call upon progessbar to update videos to be in the correct order
-    setFinished(true);
-  }
-
-  const pause = (e: React.PointerEvent) => {
-    e.stopPropagation()
-    // if recording, then this is just a new segment
-    videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
-  }
-
-  const start = (e: React.PointerEvent) => {
-    setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
-      // start recording if not already recording
-      if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
-
-      return true; // cancels propagation to documentView to avoid selecting it.
-    }, false, false);
-  }
-
-  const undoPrevious = (e: React.PointerEvent) => {
-    e.stopPropagation();
-    setDoUndo(prev => !prev);
-  }
+    const undoPrevious = (e: React.PointerEvent) => {
+        e.stopPropagation();
+        setDoUndo(prev => !prev);
+    }
 
-  const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+    const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
 
-  const millisecondToMinuteSecond = (milliseconds: number) => {
-    const toTwoDigit = (digit: number) => {
-      return String(digit).length == 1 ? "0" + digit : digit
+    const millisecondToMinuteSecond = (milliseconds: number) => {
+        const toTwoDigit = (digit: number) => {
+            return String(digit).length == 1 ? "0" + digit : digit
+        }
+        const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
+        const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
+        return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
     }
-    const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
-    const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
-    return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
-  }
-
-  return (
-    <div className="recording-container">
-      <div className="video-wrapper">
-        <video id={`video-${props.id}`}
-          autoPlay
-          muted
-          onTimeUpdate={() => handleOnTimeUpdate()}
-          ref={videoElementRef}
-        />
-        <div className="recording-sign">
-          <span className="dot" />
-          <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
-        </div>
-        <div className="controls">
-
-          <div className="controls-inner-container">
-            <div className="record-button-wrapper">
-              {recording ?
-                <button className="stop-button" onPointerDown={pause} /> :
-                <button className="record-button" onPointerDown={start} />
-              }
-            </div>
 
-            {!recording && (videos.length > 0 ?
-
-              <div className="options-wrapper video-edit-wrapper">
-                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
-                  <MdBackspace onPointerDown={undoPrevious} />
-                </IconContext.Provider>
-                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
-                  <FaCheckCircle onPointerDown={finish} />
-                </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>
-
-        </div>
-
-        <ProgressBar
-          videos={videos}
-          setVideos={setVideos}
-          orderVideos={orderVideos}
-          progress={progress}
-          recording={recording}
-          doUndo={doUndo}
-          setCanUndo={setCanUndo}
-        />
-      </div>
-    </div>)
+    return (
+        <div className="recording-container">
+            <div className="video-wrapper">
+                <video id={`video-${props.id}`}
+                    autoPlay
+                    muted
+                    onTimeUpdate={() => handleOnTimeUpdate()}
+                    ref={videoElementRef}
+                />
+                <div className="recording-sign">
+                    <span className="dot" />
+                    <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+                </div>
+                <div className="controls">
+
+                    <div className="controls-inner-container">
+                        <div className="record-button-wrapper">
+                            {recording ?
+                                <button className="stop-button" onPointerDown={pause} /> :
+                                <button className="record-button" onPointerDown={start} />
+                            }
+                        </div>
+
+                        {!recording && (videos.length > 0 ?
+
+                            <div className="options-wrapper video-edit-wrapper">
+                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
+                                    <MdBackspace onPointerDown={undoPrevious} />
+                                </IconContext.Provider>
+                                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+                                    <FaCheckCircle onPointerDown={finish} />
+                                </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>
+
+                </div>
+
+                <ProgressBar
+                    videos={videos}
+                    setVideos={setVideos}
+                    orderVideos={orderVideos}
+                    progress={progress}
+                    recording={recording}
+                    doUndo={doUndo}
+                    setCanUndo={setCanUndo}
+                />
+            </div>
+        </div>)
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 622830b6fd673688db938c64d20885d12d3afb28 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@Michaels-MacBook-Pro-5.local>
Date: Thu, 30 Jun 2022 17:00:10 -0400
Subject: small bug fixes for the ui of replaying the presentations from
 slides. can't get the video to autmoatically open from PresBox.tsx - an
 inifnite loop somewhere.

---
 .../views/nodes/RecordingBox/RecordingView.tsx     |   4 +-
 src/client/views/nodes/trails/PresBox.tsx          |   2 +
 src/client/views/nodes/trails/PresElementBox.tsx   | 389 +++++++++++----------
 3 files changed, 212 insertions(+), 183 deletions(-)

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

diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 83ed6914e..ec5917b9e 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -43,8 +43,8 @@ export function RecordingView(props: IRecordingViewProps) {
     const videoRecorder = useRef<MediaRecorder | null>(null);
     const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
-    const [finished, setFinished] = useState<boolean>(false)
-    const [trackScreen, setTrackScreen] = useState<boolean>(true)
+    const [finished, setFinished] = useState<boolean>(false);
+    const [trackScreen, setTrackScreen] = useState<boolean>(false);
 
 
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 9f858539f..7045d6d5d 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -30,6 +30,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import "./PresBox.scss";
 import { PresEffect, PresMovement, PresStatus } from "./PresEnums";
 import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
+import { PresElementBox } from ".";
 
 export interface PinProps {
     audioRange?: boolean;
@@ -221,6 +222,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // No more frames in current doc and next slide is defined, therefore move to next slide 
     nextSlide = (activeNext: Doc) => {
         const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null);
+        console.info('nextSlide', activeNext.title, targetNext?.title);
         let nextSelected = this.itemIndex + 1;
         this.gotoDocument(nextSelected, this.activeItem);
         for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index a4c69f66b..cc7bd5189 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from "mobx"
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc";
 import { Id } from "../../../../fields/FieldSymbols";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from "../../../../fields/Types";
 import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils";
 import { Docs, DocUtils } from "../../../documents/Documents";
 import { DocumentType } from "../../../documents/DocumentTypes";
@@ -35,6 +35,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     _heightDisposer: IReactionDisposer | undefined;
 
     @observable _dragging = false;
+    // Idea: this boolean will determine whether to automatically show the video when this preselement is selected.
+    // @observable static showVideo: boolean = false;
     @computed get indexInPres() { return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, "data")]).indexOf(this.rootDoc); }  // 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 [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31; } // 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 this.presBox.presStatus; }
@@ -260,12 +262,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @undoBatch
     removeItem = action((e: React.MouseEvent) => {
-      e.stopPropagation();
+        e.stopPropagation();
         this.props.removeDocument?.(this.rootDoc);
         if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
             PresBox.Instance._selectedArray.delete(this.rootDoc);
         }
-        this.removeAllRecordingInOverlay() 
+        this.removeAllRecordingInOverlay()
     });
 
     // set the value/title of the individual pres element
@@ -286,17 +288,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     updateView = (targetDoc: Doc, activeItem: Doc) => {
         switch (targetDoc.type) {
-            case DocumentType.PDF: case  DocumentType.WEB: case DocumentType.RTF :
+            case DocumentType.PDF: case DocumentType.WEB: case DocumentType.RTF:
                 const scroll = targetDoc._scrollTop;
                 activeItem.presPinViewScroll = scroll;
-            break;
+                break;
             case DocumentType.VID: case DocumentType.AUDIO:
                 activeItem.presStartTime = targetDoc._currentTimecode;
-            break;
-            case  DocumentType.COMPARISON :
+                break;
+            case DocumentType.COMPARISON:
                 const clipWidth = targetDoc._clipWidth;
                 activeItem.presPinClipWidth = clipWidth;
-            break;
+                break;
             default:
                 const x = targetDoc._panX;
                 const y = targetDoc._panY;
@@ -307,122 +309,147 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     }
 
-      @computed get recordingIsInOverlay() {
-            let isInOverlay = false
-            DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
-                  if (doc.slides === this.rootDoc) {
-                        isInOverlay = true
-                        return
-                  }
-            })
-            return isInOverlay
-      }
-
-      removeAllRecordingInOverlay = () => {
-            DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
-                  if (doc.slides === this.rootDoc) {
-                        Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
-                  }
-            })
-      }
-
-      @undoBatch
-      @action
-      hideRecording = (e: React.MouseEvent) => {
-            e.stopPropagation()
-            this.removeAllRecordingInOverlay()
-      }
-
-      @undoBatch
-      @action
-      showRecording = (activeItem: Doc) => {
-            this.removeAllRecordingInOverlay()
-            if (activeItem.recording) {
-                  // if we already have an existing recording
-                  Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+    @computed get recordingIsInOverlay() {
+        let isInOverlay = false
+        DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+            if (doc.slides === this.rootDoc) {
+                isInOverlay = true
+                // return
+            }
+        })
+        return isInOverlay
+    }
+
+    // a previously recorded video will have timecode defined
+    static videoIsRecorded = (activeItem: Doc) => {
+        const casted = Cast(activeItem.recording, Doc, null);
+        return casted && 'currentTimecode' in casted;
+    }
 
+    removeAllRecordingInOverlay = () => {
+        DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+            if (doc.slides === this.rootDoc) {
+                Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
             }
-      }
+        })
+    }
+
+    static removeEveryExistingRecordingInOverlay = () => {
+        // Remove every recording that already exists in overlay view
+        DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+            // if it's a recording video, don't remove from overlay (user can lose data)
+            if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return
+
+            if (doc.slides !== null) {
+                Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
+            }
+        })
+    }
+
+    @undoBatch
+    @action
+    hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => {
+        e.stopPropagation()
+        this.removeAllRecordingInOverlay()
+
+        // if (iconClick) PresElementBox.showVideo = false;
+    }
+
+    @undoBatch
+    @action
+    showRecording = (activeItem: Doc, iconClick: boolean = false) => {
+        // if (iconClick) PresElementBox.showVideo = true;
+        // if (!PresElementBox.showVideo) return;
+
+        // remove the overlays on switch *IF* not opened from the specific icon
+        if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay();
+
+
+        if (activeItem.recording) {
+            Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+        }
+    }
 
-      @undoBatch
-      @action
-      startRecording = (activeItem: Doc) => {
+    @undoBatch
+    @action
+    startRecording = (activeItem: Doc) => {
+        console.log('start recording', 'activeItem', activeItem)
+        if (PresElementBox.videoIsRecorded(activeItem)) {
+            // if we already have an existing recording
+            this.showRecording(activeItem, true);
+            //   // if we already have an existing recording
+            //   Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+
+        } else {
             // Remove every recording that already exists in overlay view
-            DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
-                  if (doc.slides !== null) {
-                        Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
-                  }
-            })
+            // this is a design decision to clear to focus in on the recoding mode
+            PresElementBox.removeEveryExistingRecordingInOverlay();
+
+            // if we dont have any recording
+            const recording = Docs.Create.WebCamDocument("", {
+                _width: 384, _height: 216,
+                hideDocumentButtonBar: true,
+                hideDecorationTitle: true,
+                hideOpenButton: true,
+                // hideDeleteButton: 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
+
+            // make recording box appear in the bottom right corner of the screen
+            recording.x = window.innerWidth - recording[WidthSym]() - 20;
+            recording.y = window.innerHeight - recording[HeightSym]() - 20;
+            Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, recording);
+        }
+    }
 
-            if (activeItem.recording) {
-                  // if we already have an existing recording
-                  Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+    @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;
+    }
 
-            } else {
-                  // if we dont have any recording
-                  const recording = Docs.Create.WebCamDocument("", {
-                        _width: 384, _height: 216,
-                        hideDocumentButtonBar: true,
-                        hideDecorationTitle: true,
-                        hideOpenButton: true,
-                        // hideDeleteButton: 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
-
-                  // make recording box appear in the bottom right corner of the screen
-                  recording.x = window.innerWidth - recording[WidthSym]() - 20;
-                  recording.y = window.innerHeight - recording[HeightSym]() - 20;
-                  Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, 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);
-                              this.showRecording(activeItem);
-                        }}
-                        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 ?
+    @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);
+                    this.showRecording(activeItem);
+                }}
+                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}.`}
@@ -432,53 +459,53 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                               <div className="presItem-number">
                                     {`${this.indexInPres + 1}.`}
                               </div>} */}
-                        {/* <div className="presItem-number">
+                {/* <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' }}>
-                                    <div>{`${this.indexInPres + 1}.  `}</div>
-                                    <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(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></>}>
+                {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' }}>
+                        <div>{`${this.indexInPres + 1}.  `}</div>
+                        <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={(e) => this.hideRecording(e, true)}
+                                    style={{ fontWeight: 700 }}>
+                                    <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} />
+                                </div>
+                            </Tooltip> :
+                            <Tooltip title={<><div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? "Show" : "Start"} recording`}</div></>}>
+                                <div className="slideButton"
+                                    onClick={(e) => { e.stopPropagation(); this.startRecording(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={{
@@ -493,22 +520,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                                 </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">{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;
+    }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2

