diff options
Diffstat (limited to 'src/client/util/ReplayMovements.ts')
-rw-r--r-- | src/client/util/ReplayMovements.ts | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts new file mode 100644 index 000000000..86bc4c5de --- /dev/null +++ b/src/client/util/ReplayMovements.ts @@ -0,0 +1,208 @@ +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; + } + Doc.UserDoc().presentationMode = 'none'; + + this.isPlaying = false + // TODO: set userdoc presentMode to browsing + this.timers?.map(timer => clearTimeout(timer)) + } + + setVideoBox = async (videoBox: VideoBox) => { + // console.info('setVideoBox', videoBox); + if (this.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 = () => { + 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.info('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; + } + // console.log('openTab', docId, doc); + CollectionDockingView.AddSplit(doc, 'right'); + const docView = DocumentManager.Instance.getDocumentView(doc); + // BUG - this returns undefined if the doc is already open + 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[]): 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; + Doc.UserDoc().presentationMode = 'none'; + } + + public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => { + // console.info('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); + 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); + console.log('openedColFFView', openedColFFView); + openedColFFView && this.zoomAndPan(movement, openedColFFView); + } + + // if last movement, presentation is done -> cleanup :) + if (movement === filteredMovements[filteredMovements.length - 1]) { + this.endPlayingPresentation(); + } + }, timeDiff); + }); + } +} |