aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/recording/recordingApi.ts300
-rw-r--r--src/client/apis/recording/recordingApi.tsx151
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx33
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx7
4 files changed, 305 insertions, 186 deletions
diff --git a/src/client/apis/recording/recordingApi.ts b/src/client/apis/recording/recordingApi.ts
new file mode 100644
index 000000000..b57e675b7
--- /dev/null
+++ b/src/client/apis/recording/recordingApi.ts
@@ -0,0 +1,300 @@
+import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm";
+import React, { useState } from "react";
+import { IReactionDisposer, observable, reaction } from "mobx";
+import { NumCast } from "../../../fields/Types";
+
+type Movement = {
+ time: number,
+ panX: number,
+ panY: number,
+}
+
+type Presentation = {
+ movements: Array<Movement>
+ meta: Object,
+ startDate: Date | null,
+}
+
+export class RecordingApi {
+
+ private static NULL_PRESENTATION: Presentation = {
+ movements: [],
+ meta: {},
+ startDate: null,
+ }
+
+ // 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;
+
+ // for now, set playFFView
+ this.playFFView = null;
+ this.timers = null;
+ }
+
+ // little helper :)
+ private get isInitPresenation(): boolean {
+ return this.currentPresentation.startDate === null
+ }
+
+ public start = (view: CollectionFreeFormView, 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()')
+ }
+
+ // (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.startDate = startDate
+ // (4) set isRecording true to allow trackMovements
+ this.isRecording = true
+ }
+
+ public clear = (): Error | undefined => {
+ // TODO: maybe archive the data?
+ if (this.isRecording) {
+ console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() or finish() first')
+ return new Error('[recordingApi.ts] clear()')
+ }
+
+ // clear presenation data
+ this.currentPresentation = RecordingApi.NULL_PRESENTATION
+ // clear isRecording
+ this.isRecording = false
+ // clear absoluteStart
+ this.absoluteStart = -1
+
+ // clear the disposeFunc
+ this.disposeFunc = null
+ }
+
+ public pause = (): Error | undefined => {
+ if (this.currentPresentation.startDate === null) {
+ console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
+ return new Error('[recordingApi.ts] pause(): no presenation')
+ }
+ // don't allow track movments
+ this.isRecording = false
+
+ // set relativeStart to the pausedTimestamp
+ const timestamp = new Date().getTime()
+ this.absoluteStart = timestamp
+ }
+
+ public resume = () => {
+ const timestamp = new Date().getTime()
+ const startTimestamp = this.currentPresentation.startDate?.getTime()
+ if (!startTimestamp) {
+ console.error('[recordingApi.ts] resume() failed: no presentation data. try calling start() first')
+ return new Error('[recordingApi.ts] pause()')
+ }
+
+ // update absoluteStart to bridge the paused time
+ const absoluteTimePaused = timestamp - this.absoluteStart
+ this.absoluteStart = absoluteTimePaused
+ }
+
+ public finish = (): Error | Presentation => {
+ if (this.isInitPresenation) {
+ console.error('[recordingApi.ts] finish() failed: no presentation data. try calling start() first')
+ return new Error('[recordingApi.ts] finish()')
+ }
+
+ // return a copy of the the presentation data
+ return { ...this.currentPresentation }
+ }
+
+ private trackMovements = (panX: number, panY: number): Error | undefined => {
+ // ensure we are recording
+ if (!this.isRecording) {
+ console.error('[recordingApi.ts] pause() failed: recording is paused()')
+ return new Error('[recordingApi.ts] pause()')
+ }
+
+ // get the relative time
+ const timestamp = new Date().getTime()
+ const relativeTime = timestamp - this.absoluteStart
+
+ // make new movement struct
+ const movement: Movement = { time: relativeTime, panX, panY }
+
+ // add that movement to the current presentation data's movement array
+ this.currentPresentation.movements.push(movement)
+ }
+
+ // instance variable for the FFView
+ private disposeFunc: IReactionDisposer | 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 === 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) }),
+ (res) => (res.x !== -1 && res.y !== -1) && this.trackMovements(res.x, res.y)
+ )
+
+ // for now, set the most recent recordingFFView to the playFFView
+ this.playFFView = 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
+ private playFFView: CollectionFreeFormView | null;
+ private timers: Timer[] | null;
+
+ public followMovements = (presentation: Presentation): undefined | Error => {
+ if (presentation.startDate === null || this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+ }
+
+ const document = this.playFFView.Document
+ const { movements } = presentation
+ this.timers = movements.map(movement => {
+ const { panX, panY, time } = movement
+ return new Timer(() => {
+ document._panX = panX;
+ document._panY = panY;
+ // TODO: consider cleaning this array to null or some state
+ }, time)
+ })
+ }
+
+ public pauseMovements = (): undefined | Error => {
+ if (this.playFFView === null) {
+ return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+ }
+ // TODO: set userdoc presentMode to browsing
+ this.timers?.forEach(timer => timer.pause())
+ }
+
+ public resumeMovements = (): undefined | Error => {
+ if (this.playFFView === null) {
+ return new Error('[recordingApi.ts] resumeMovements() failed: no view')
+ }
+ this.timers?.forEach(timer => timer.resume())
+ }
+
+ // public followMovements = (presentation: Presentation): undefined | Error => {
+ // if (presentation.startDate === null || this.playFFView === null) {
+ // return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+ // }
+
+ // const document = this.playFFView.Document
+ // const { movements } = presentation
+ // movements.forEach(movement => {
+ // const { panX, panY, time } = movement
+ // // set the pan to what was stored
+ // setTimeout(() => {
+ // document._panX = panX;
+ // document._panY = panY;
+ // }, time)
+ // })
+ // }
+ // 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)
+ // }
+}
+
+/** Represents the `setTimeout` with an ability to perform pause/resume actions
+ * citation: https://stackoverflow.com/questions/3969475/javascript-pause-settimeout
+ */
+export class Timer {
+ private _start: Date;
+ private _remaining: number;
+ private _durationTimeoutId?: NodeJS.Timeout;
+ private _callback: (...args: any[]) => void;
+ private _done = false;
+ get done () {
+ return this._done;
+ }
+
+ public constructor(callback: (...args: any[]) => void, ms = 0) {
+ this._callback = () => {
+ callback();
+ this._done = true;
+ };
+ this._remaining = ms;
+ this.resume();
+ }
+
+ /** pauses the timer */
+ public pause(): Timer {
+ if (this._durationTimeoutId && !this._done) {
+ this._clearTimeoutRef();
+ this._remaining -= new Date().getTime() - this._start.getTime();
+ }
+ return this;
+ }
+
+ /** resumes the timer */
+ public resume(): Timer {
+ if (!this._durationTimeoutId && !this._done) {
+ this._start = new Date;
+ this._durationTimeoutId = setTimeout(this._callback, this._remaining);
+ }
+ return this;
+ }
+
+ /**
+ * clears the timeout and marks it as done.
+ *
+ * After called, the timeout will not resume
+ */
+ public clearTimeout() {
+ this._clearTimeoutRef();
+ this._done = true;
+ }
+
+ private _clearTimeoutRef() {
+ if (this._durationTimeoutId) {
+ clearTimeout(this._durationTimeoutId);
+ this._durationTimeoutId = undefined;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/apis/recording/recordingApi.tsx b/src/client/apis/recording/recordingApi.tsx
deleted file mode 100644
index 55714f03b..000000000
--- a/src/client/apis/recording/recordingApi.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm";
-import React, { useState } from "react";
-
-export function RecordingApi() {
-
- type Movement = {
- time: number,
- panX: number,
- panY: number,
- }
-
- type Presentation = {
- movements: Array<Movement>
- meta: Object,
- startDate: Date | null,
- }
-
- const NULL_PRESENTATION = {
- movements: [],
- meta: {},
- startDate: null,
- }
-
- const [currentPresentation, setCurrentPresenation] = useState<Presentation>(NULL_PRESENTATION)
- const [isRecording, setIsRecording] = useState(false)
- const [absoluteStart, setAbsoluteStart] = useState<number>(-1)
-
- const initAndStart = (view: CollectionFreeFormView, meta?: Object): Error | undefined => {
- // check if already init a presentation
- if (currentPresentation.startDate !== null) {
- console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
- return new Error('[recordingApi.ts] start()')
- }
-
- // (1a) get start date for presenation
- const startDate = new Date()
- // (1b) set start timestamp to absolute timestamp
- setAbsoluteStart(startDate.getTime())
-
- // TODO: (2) assign meta content
-
- // (3) assign init values to currentPresenation
- setCurrentPresenation({ ...currentPresentation, startDate })
-
- // (4) set isRecording true to allow trackMovements
- setIsRecording(true)
- }
-
- const clear = (): Error | undefined => {
- // TODO: maybe archive the data?
- if (isRecording) {
- console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() or finish() first')
- return new Error('[recordingApi.ts] clear()')
- }
- // clear presenation data
- setCurrentPresenation(NULL_PRESENTATION)
-
- // clear isRecording
- setIsRecording(false)
-
- // clear absoluteStart
- setAbsoluteStart(-1)
- }
-
- const pause = (): Error | undefined => {
- if (currentPresentation.startDate === null) {
- console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
- return new Error('[recordingApi.ts] pause(): no presenation')
- }
- // don't allow track movments
- setIsRecording(false)
-
- // set relativeStart to the pausedTimestamp
- const timestamp = new Date().getTime()
- setAbsoluteStart(timestamp)
- }
-
- const resume = () => {
- if (currentPresentation.startDate === null) {
- console.error('[recordingApi.ts] resume() failed: no presentation started. try calling init() first')
- return new Error('[recordingApi.ts] resume()')
- }
-
- const timestamp = new Date().getTime()
- const startTimestamp = currentPresentation.startDate?.getTime()
- if (!startTimestamp) {
- console.error('[recordingApi.ts] resume() failed: no presentation data. try calling init() first')
- return new Error('[recordingApi.ts] pause()')
- }
-
- setAbsoluteStart(prevTime => {
- // const relativeUnpause = timestamp - absoluteStart
- // const timePaused = relativeUnpause - prevTime
- // return timePaused + absoluteStart
- const absoluteTimePaused = timestamp - prevTime
- return absoluteTimePaused
- })
- }
-
- const finish = (): Error | Presentation => {
- if (currentPresentation.movements === null) {
- console.error('[recordingApi.ts] finish() failed: no presentation data. try calling init() first')
- return new Error('[recordingApi.ts] finish()')
- }
-
- // make copy and clear this class's data
- const returnCopy = { ...currentPresentation }
- clear()
-
- // return the copy
- return returnCopy
- }
-
- const trackMovements = (panX: number, panY: number): Error | undefined => {
- // ensure we are recording
- if (!isRecording) {
- console.error('[recordingApi.ts] pause() failed: recording is paused()')
- return new Error('[recordingApi.ts] pause()')
- }
-
- // get the relative time
- const timestamp = new Date().getTime()
- const relativeTime = timestamp - absoluteStart
-
- // make new movement struct
- const movement: Movement = { time: relativeTime, panX, panY }
-
- // add that movement struct to the current presentation data
- setCurrentPresenation(prevPres => {
- const movements = [...prevPres.movements, movement]
- return {...prevPres, movements}
- })
- }
-
- // TOOD: need to pause all intervals if possible lol
- // TODO: extract this into different class with pause and resume recording
- const followMovements = (presentation: Presentation, docView: CollectionFreeFormView): void => {
- const document = docView.Document
-
- const { movements } = presentation
- movements.forEach(movement => {
- const { panX, panY, time } = movement
- // set the pan to what was stored
- setTimeout(() => {
- document._panX = panX;
- document._panY = panY;
- }, time)
- })
- }
-
-} \ 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 edc872a40..35bd9cf79 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,6 +54,7 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import e = require("connect-flash");
+import { RecordingApi } from "../../../apis/recording/RecordingApi";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -121,8 +122,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable _keyframeEditing = false;
@observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged.
- @observable storedMovements: object[] = []; // stores the movement if in recoding mode
-
@computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
@computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); }
@computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); }
@@ -964,38 +963,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
}
-
- followMovements = (): void => {
- // need the first for subtraction
- let first = null;
-
- this.storedMovements.forEach(movement => {
- if (first === null) first = movement.time;
-
- // set the pan to what was stored
- setTimeout(() => {
- this.Document._panX = movement.panX;
- this.Document._panY = movement.panY;
- }, movement.time - first)
- })
-
- // for now, clear the movements
- this.storedMovements = []
- }
-
@action
setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
- // if not presenting, just retrace the movements
- if (Doc.UserDoc()?.presentationMode === 'watching') {
- this.followMovements()
- return;
- }
-
- if (Doc.UserDoc()?.presentationMode === 'recording') {
- // store as many movments as possible
- this.storedMovements.push({time: new Date().getTime(), panX, panY})
- }
-
if (!this.isAnnotationOverlay && clamp) {
// this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index a2a08f5b8..d99492095 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -8,6 +8,8 @@ import { IconContext } from "react-icons";
import { Networking } from '../../../Network';
import { Upload } from '../../../../server/SharedMediaTypes';
+import { RecordingApi } from '../../../apis/recording/RecordingApi';
+
enum RecordingStatus {
Recording,
@@ -43,7 +45,7 @@ export function RecordingView(props: IRecordingViewProps) {
const audioRecorder = useRef<MediaRecorder | null>(null);
const videoElementRef = useRef<HTMLVideoElement | null>(null);
- const [finished, setFinished] = useState<Boolean>(false)
+ const [finished, setFinished] = useState<Boolean>(false)
@@ -300,8 +302,7 @@ export function RecordingView(props: IRecordingViewProps) {
<i className="bx bxs-volume-mute"></i>
)}
</button> */}
- </div>
-
+ </div>
</div>
</div>)
} \ No newline at end of file