aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/RecordingApi.ts
diff options
context:
space:
mode:
authorMichael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>2022-06-15 15:39:55 -0400
committerMichael Foiani <sotech117@michaels-mbp-5.devices.brown.edu>2022-06-15 15:39:55 -0400
commit98fba8bdb0fe81d6f71d0ae6018fcaaf7d8897df (patch)
tree0fda678c5c6ad3707816e0c1d148750468e115a2 /src/client/util/RecordingApi.ts
parentfafc1fc678433a95240e8ecefa1b31b7912d5993 (diff)
Refactor RecordingApi into two main files - TrackMovements and ReplayMovements. TrackMovements is for recording presentations and ReplayMovments is for replaying them.
Diffstat (limited to 'src/client/util/RecordingApi.ts')
-rw-r--r--src/client/util/RecordingApi.ts461
1 files changed, 0 insertions, 461 deletions
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 };
- }
-}