1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { Doc, IdToDoc } from '../../fields/Doc';
import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
import { DocumentView } from '../views/nodes/DocumentView';
import { OpenWhereMod } from '../views/nodes/OpenWhere';
import { SnappingManager } from './SnappingManager';
import { Movement, Presentation } from './TrackMovements';
import { ViewBoxInterface } from '../views/ViewBoxInterface';
import { StrCast } from '../../fields/Types';
import { FieldViewProps } from '../views/nodes/FieldView';
export class ReplayMovements {
private timers: NodeJS.Timeout[] | null;
private videoBoxDisposeFunc: IReactionDisposer | null;
private videoBox: ViewBoxInterface<FieldViewProps> | null;
private isPlaying: boolean;
// create static instance and getter for global use
// eslint-disable-next-line no-use-before-define
@observable static _instance: ReplayMovements;
static get Instance(): ReplayMovements {
return ReplayMovements._instance;
}
constructor() {
makeObservable(this);
// init the global instance
ReplayMovements._instance = this;
// instance vars for replaying
this.timers = null;
this.videoBoxDisposeFunc = null;
this.videoBox = null;
this.isPlaying = false;
reaction(
() => SnappingManager.UserPanned,
() => {
if (Doc.UserDoc()?.presentationMode === 'watching') this.pauseFromInteraction();
}
);
reaction(
() => DocumentView.Selected().slice(),
selviews => {
const selVideo = selviews.find(dv => dv.ComponentView?.playFrom);
if (selVideo?.ComponentView?.Play) {
this.setVideoBox(selVideo.ComponentView);
} else this.removeVideoBox();
}
);
}
// 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: ViewBoxInterface<FieldViewProps>) => {
if (this.videoBox !== null) {
console.warn('setVideoBox on already videoBox');
}
this.videoBoxDisposeFunc?.();
const data = StrCast(videoBox.dataDoc?.[videoBox.fieldKey + '_presentation']);
const presentation = data ? JSON.parse(data) : null;
if (presentation === null) {
console.warn('setVideoBox on null videoBox presentation');
return;
}
this.loadPresentation(presentation);
this.videoBoxDisposeFunc = reaction(
() => ({ playing: videoBox.IsPlaying?.(), timeViewed: videoBox.PlayerTime?.() || 0 }),
({ playing, timeViewed }) => (playing ? this.playMovements(presentation, timeViewed) : this.pauseMovements())
);
this.videoBox = videoBox;
};
removeVideoBox = () => {
this.videoBoxDisposeFunc?.();
this.videoBox = null;
this.videoBoxDisposeFunc = null;
};
// should be called from interacting with the screen
pauseFromInteraction = () => {
this.videoBox?.Pause?.();
this.pauseMovements();
};
loadPresentation = (presentation: Presentation) => {
const { movements } = presentation;
if (movements === null) {
throw new Error('[recordingApi.ts] followMovements() failed: no presentation data');
}
movements.forEach((movement, i) => {
if (typeof movement.doc === 'string') {
const doc = IdToDoc(movement.doc);
if (!doc) {
console.log('ERROR: tracked doc not found');
} else {
movements[i].doc = doc;
}
}
});
};
// returns undefined if the docView isn't open on the screen
getCollectionFFView = (doc: Doc) => {
const isInView = DocumentView.getDocumentView(doc);
return isInView?.ComponentView as CollectionFreeFormView;
};
// will open the doc in a tab then return the CollectionFFView that holds it
openTab = (doc: Doc) => {
if (doc === undefined) {
console.error(`doc undefined`);
return undefined;
}
// console.log('openTab', docId, doc);
DocumentView.addSplit(doc, OpenWhereMod.right);
const docView = DocumentView.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._freeform_panX = panX;
document.Document._freeform_panY = panY;
};
getFirstMovements = (movements: Movement[]): Map<Doc, Movement> => {
if (movements === null) return new Map();
// generate a set of all unique docIds
const docIdtoFirstMove = new Map<Doc, Movement>();
movements.forEach(move => {
if (!docIdtoFirstMove.has(move.doc as Doc)) docIdtoFirstMove.set(move.doc as Doc, move);
});
return docIdtoFirstMove;
};
endPlayingPresentation = () => {
this.isPlaying = false;
Doc.UserDoc().presentationMode = 'none';
};
public playMovements = (presentation: Presentation, timeViewed: number = 0) => {
// console.info('playMovements', presentation, timeViewed, docIdtoDoc);
if (presentation.movements === null || presentation.movements.length === 0) {
// || this.playFFView === null) {
return '[recordingApi.ts] followMovements() failed: no presentation data';
}
if (this.isPlaying) return undefined;
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.doc as Doc) === undefined;
if (isClosed) this.openTab(firstMovement.doc as Doc);
// for the open tabs, set it to the first move
const docIdtoFirstMove = this.getFirstMovements(filteredMovements);
Array.from(docIdtoFirstMove).forEach(([doc, firstMove]) => {
const colFFView = this.getCollectionFFView(doc);
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.doc as Doc);
if (collectionFFView) {
this.zoomAndPan(movement, collectionFFView);
} else {
// tab wasn't open - open it and play the movement
const openedColFFView = this.openTab(movement.doc as Doc);
openedColFFView && this.zoomAndPan(movement, openedColFFView);
}
// if last movement, presentation is done -> cleanup :)
if (movement === filteredMovements[filteredMovements.length - 1]) {
this.endPlayingPresentation();
}
}, timeDiff);
});
return undefined;
};
}
|