aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/util/RecordingApi.ts195
-rw-r--r--src/client/views/nodes/VideoBox.tsx150
2 files changed, 210 insertions, 135 deletions
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
index ae5431a03..87cb85497 100644
--- a/src/client/util/RecordingApi.ts
+++ b/src/client/util/RecordingApi.ts
@@ -57,7 +57,7 @@ export class RecordingApi {
this.absoluteStart = -1;
// used for tracking movements in the view frame
- this.recordingFFViews = new Map();
+ this.recordingFFViews = null;
this.tabChangeDisposeFunc = null;
// for now, set playFFView
@@ -127,6 +127,10 @@ export class RecordingApi {
}
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);
@@ -142,6 +146,8 @@ export class RecordingApi {
}
public start = (meta?: Object) => {
+ this.initTabTracker();
+
// update the presentation mode
Doc.UserDoc().presentationMode = 'recording';
@@ -190,6 +196,9 @@ export class RecordingApi {
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
@@ -250,13 +259,28 @@ export class RecordingApi {
private videoBoxDisposeFunc: IReactionDisposer | null = null;
private videoBox: VideoBox | null = null;
- setVideoBox = (videoBox: VideoBox) => {
+ setVideoBox = async (videoBox: VideoBox) => {
console.log('setVideoBox', videoBox);
- if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox'); this.videoBoxDisposeFunc(); }
+ 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(videoBox.presentation, timeViewed) : this.pauseMovements()
+ playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
);
this.videoBox = videoBox;
}
@@ -283,10 +307,81 @@ export class RecordingApi {
public _isPlaying = false;
- public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
- console.log('playMovements', presentation, timeViewed, videoBox);
+ 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) { //|| this.playFFView === null) {
+ 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;
@@ -294,65 +389,45 @@ export class RecordingApi {
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
- let preScale = -1;
- const zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => {
- const { panX, panY, scale } = movement;
- (scale !== 0 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
- document.Document._panX = panX;
- document.Document._panY = panY;
-
- preScale = scale;
+ 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);
+ }
}
- // set the first frame to be at the start of the pres
- // zoomAndPan(filteredMovements[0]);
-
- // generate a set of all unique docIds
- const docIds = new Set(filteredMovements.map(movement => movement.docId))
- // TODO: optimize only ons-first load
- // TODO: make async await
- // TODO: make sure the cahce still hs the id
- // TODO: if they are open, set them to their first move
- // this will load the cache, so getCachedREfFields won't have to reach server
- DocServer.GetRefFields([...docIds]).then(refFields => {
- console.log('refFields', refFields)
-
- const openTab = (docId: string) : DocumentView | undefined => {
- const isInView = DocumentManager.Instance.getDocumentViewById(docId);
- if (isInView) { return isInView; }
-
- const doc = DocServer.GetCachedRefField(docId) as Doc;
- if (doc == undefined) {
- console.warn('Doc server cache did not contain docId', docId)
- return undefined;
+ 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);
}
- CollectionDockingView.AddSplit(doc, 'right');
- return DocumentManager.Instance.getDocumentView(doc);
- }
- // 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(() => {
- // open tab if it is not already open
- const view = openTab(movement.docId);
- if (view) {
- const collectionFFView = view.ComponentView;
- console.log(collectionFFView instanceof CollectionFreeFormView)
- // replay the movement
- zoomAndPan(movement, collectionFFView as CollectionFreeFormView);
- }
- // if last movement, presentation is done -> set the instance var
- if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
- }, timeDiff)
- });
- })
+ // 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
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index ed9bcf29b..34df03954 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -28,7 +28,7 @@ import { AnchorMenu } from "../pdf/AnchorMenu";
import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
-import { RecordingApi } from "../../util/RecordingApi";
+import { Presentation, RecordingApi } from "../../util/RecordingApi";
import { List } from "../../../fields/List";
import { RecordingBox } from "./RecordingBox";
const path = require('path');
@@ -48,80 +48,80 @@ const path = require('path');
@observer
export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
- /**
- * Uploads an image buffer to the server and stores with specified filename. by default the image
- * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
- * @param imageUri the bytes of the image
- * @param returnedFilename the base filename to store the image on the server
- * @param nosuffix optionally suppress creating multiple resolution images
- */
- public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
- try {
- const posting = Utils.prepend("/uploadURI");
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename,
- nosuffix,
- replaceRootFilename
- },
- json: true,
- });
- return returnedUri;
-
- } catch (e) {
- console.log("VideoBox :" + e);
- }
- }
-
- static _youtubeIframeCounter: number = 0;
- static heightPercent = 80; // height of video relative to videoBox when timeline is open
- private _disposers: { [name: string]: IReactionDisposer } = {};
- private _youtubePlayer: YT.Player | undefined = undefined;
- private _videoRef: HTMLVideoElement | null = null; // <video> ref
- private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
- private _youtubeIframeId: number = -1;
- private _youtubeContentCreated = false;
- private _audioPlayer: HTMLAudioElement | null = null;
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
- private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
- private _playRegionTimer: any = null; // timeout for playback
- @observable _stackedTimeline: any; // CollectionStackedTimeline ref
- @observable static _nativeControls: boolean; // default html controls
- @observable _marqueeing: number[] | undefined; // coords for marquee selection
- @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
- @observable _screenCapture = false;
- @observable _clicking = false; // used for transition between showing/hiding timeline
- @observable _forceCreateYouTubeIFrame = false;
- @observable _playTimer?: NodeJS.Timeout = undefined;
- @observable _fullScreen = false;
- @observable _playing = false;
- @observable _finished: boolean = false; // has playback reached end of clip
- @observable _volume: number = 1;
- @observable _muted: boolean = false;
-
- @computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
- // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
- @observable rawDuration: number = 0;
-
-
- @computed get youtubeVideoId() {
- const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
- return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
- }
-
-
- // returns the path of the audio file
- @computed get audiopath() {
- const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
- const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
- return field?.url.href ?? vfield?.url.href ?? "";
- }
-
- // returns the presentation data if it exists, null otherwise
- @computed get presentation() {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+ /**
+ * Uploads an image buffer to the server and stores with specified filename. by default the image
+ * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+ * @param imageUri the bytes of the image
+ * @param returnedFilename the base filename to store the image on the server
+ * @param nosuffix optionally suppress creating multiple resolution images
+ */
+ public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
+ try {
+ const posting = Utils.prepend("/uploadURI");
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename,
+ nosuffix,
+ replaceRootFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log("VideoBox :" + e);
+ }
+ }
+
+ static _youtubeIframeCounter: number = 0;
+ static heightPercent = 80; // height of video relative to videoBox when timeline is open
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _youtubePlayer: YT.Player | undefined = undefined;
+ private _videoRef: HTMLVideoElement | null = null; // <video> ref
+ private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
+ private _youtubeIframeId: number = -1;
+ private _youtubeContentCreated = false;
+ private _audioPlayer: HTMLAudioElement | null = null;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _playRegionTimer: any = null; // timeout for playback
+ @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+ @observable static _nativeControls: boolean; // default html controls
+ @observable _marqueeing: number[] | undefined; // coords for marquee selection
+ @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @observable _screenCapture = false;
+ @observable _clicking = false; // used for transition between showing/hiding timeline
+ @observable _forceCreateYouTubeIFrame = false;
+ @observable _playTimer?: NodeJS.Timeout = undefined;
+ @observable _fullScreen = false;
+ @observable _playing = false;
+ @observable _finished: boolean = false; // has playback reached end of clip
+ @observable _volume: number = 1;
+ @observable _muted: boolean = false;
+
+ @computed get links() { return DocListCast(this.dataDoc.links); }
+ @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
+ // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ @observable rawDuration: number = 0;
+
+
+ @computed get youtubeVideoId() {
+ const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+ return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+ }
+
+
+ // returns the path of the audio file
+ @computed get audiopath() {
+ const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
+ const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
+ return field?.url.href ?? vfield?.url.href ?? "";
+ }
+
+ // returns the presentation data if it exists, null otherwise
+ @computed get presentation(): Presentation | null {
const data = this.dataDoc[this.fieldKey + '-presentation'];
return data ? JSON.parse(data) : null;
}