diff options
Diffstat (limited to 'src/client/util/RecordingApi.ts')
| -rw-r--r-- | src/client/util/RecordingApi.ts | 461 |
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 }; - } -} |
