diff options
Diffstat (limited to 'src/client/util/RecordingApi.ts')
| -rw-r--r-- | src/client/util/RecordingApi.ts | 269 | 
1 files changed, 269 insertions, 0 deletions
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts new file mode 100644 index 000000000..021feee9a --- /dev/null +++ b/src/client/util/RecordingApi.ts @@ -0,0 +1,269 @@ +import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; +import { IReactionDisposer, observable, reaction } from "mobx"; +import { NumCast } from "../../fields/Types"; +import { Doc } from "../../fields/Doc"; +import { VideoBox } from "../views/nodes/VideoBox"; +import { scaleDiverging } from "d3-scale"; +import { Transform } from "./Transform"; + +type Movement = { +    time: number, +    panX: number, +    panY: number, +    scale: number, +} + +type Presentation = { +    movements: Array<Movement> | null +    meta: Object, +} + +export class RecordingApi { + +    private static NULL_PRESENTATION: Presentation = { +        movements: null, +        meta: {}, +    } + +    // instance variables +    private currentPresentation: Presentation; +    private isRecording: 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.isRecording = 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 isInitPresenation(): boolean { +        return this.currentPresentation.movements === null +    } + +    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.') +            return new Error('[recordingApi.ts] start()') +        } + +        // 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 isRecording true to allow trackMovements +        this.isRecording = true +    } + +    public clear = (): Error | 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; +    } + +    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 +    } + +    private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => { +        // ensure we are recording +        if (!this.isRecording) { +            return new Error('[recordingApi.ts] trackMovements()') +        } +        // check to see if the presetation is init +        if (this.isInitPresenation) { +            return new Error('[recordingApi.ts] trackMovements(): no presentation') +        } + +        // get the time +        const 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.isRecording) && this.trackMovements(res.x, res.y, res.scale) +        ) + +        // for now, set the most recent recordingFFView to the playFFView +        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; +    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') +        } + +        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; + +    // 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; + +    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) +        }) +    } + +    // 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) +    // } +}  | 
