@raydiant/playlist-window-manager 0.0.0-test-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +6 -0
- package/README.md +16 -0
- package/babel.config.js +13 -0
- package/build/BrowserViewRenderer.d.ts +19 -0
- package/build/BrowserViewRenderer.js +214 -0
- package/build/BrowserWindowRenderer.d.ts +47 -0
- package/build/BrowserWindowRenderer.js +528 -0
- package/build/IFrameRenderer.d.ts +20 -0
- package/build/IFrameRenderer.js +226 -0
- package/build/PlaylistWindowManager.d.ts +106 -0
- package/build/PlaylistWindowManager.js +1018 -0
- package/build/PlaylistWindowManagerState.d.ts +28 -0
- package/build/PlaylistWindowManagerState.js +60 -0
- package/build/PresentationWindow.d.ts +142 -0
- package/build/PresentationWindow.js +1140 -0
- package/build/PresentationWindow.test.d.ts +1 -0
- package/build/PresentationWindow.test.js +241 -0
- package/build/WindowLoadError.d.ts +3 -0
- package/build/WindowLoadError.js +29 -0
- package/build/WindowTimeoutError.d.ts +3 -0
- package/build/WindowTimeoutError.js +29 -0
- package/build/WindowUnresponsiveError.d.ts +3 -0
- package/build/WindowUnresponsiveError.js +29 -0
- package/build/embeddedApps.d.ts +2 -0
- package/build/embeddedApps.js +59 -0
- package/build/embeddedApps.test.d.ts +1 -0
- package/build/embeddedApps.test.js +123 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +30 -0
- package/build/preload.d.ts +1 -0
- package/build/preload.js +20 -0
- package/build/testUtils.d.ts +43 -0
- package/build/testUtils.js +208 -0
- package/build/types.d.ts +142 -0
- package/build/types.js +2 -0
- package/jest.config.js +6 -0
- package/package.json +30 -0
- package/src/BrowserViewRenderer.ts +182 -0
- package/src/BrowserWindowRenderer.ts +624 -0
- package/src/IFrameRenderer.ts +251 -0
- package/src/PlaylistWindowManager.ts +1042 -0
- package/src/PlaylistWindowManagerState.ts +70 -0
- package/src/PresentationWindow.test.ts +221 -0
- package/src/PresentationWindow.ts +1327 -0
- package/src/WindowLoadError.ts +8 -0
- package/src/WindowTimeoutError.ts +8 -0
- package/src/WindowUnresponsiveError.ts +8 -0
- package/src/embeddedApps.test.ts +231 -0
- package/src/embeddedApps.ts +74 -0
- package/src/index.ts +13 -0
- package/src/preload.ts +27 -0
- package/src/testUtils.ts +159 -0
- package/src/types.ts +175 -0
- package/tasks/npm-publish.sh +13 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
import * as RE from '@raydiant/playlist-rule-engine';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import EventEmitter from 'eventemitter3';
|
|
4
|
+
import {
|
|
5
|
+
PresentationWindowRenderer,
|
|
6
|
+
Device,
|
|
7
|
+
WindowBounds,
|
|
8
|
+
Theme,
|
|
9
|
+
Logger,
|
|
10
|
+
WorkerState,
|
|
11
|
+
AnalyticsEvent
|
|
12
|
+
} from './types';
|
|
13
|
+
import PresentationWindow from './PresentationWindow';
|
|
14
|
+
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
15
|
+
import WindowUnresponsiveError from './WindowUnresponsiveError';
|
|
16
|
+
import WindowTimeoutError from './WindowTimeoutError';
|
|
17
|
+
import WindowLoadError from './WindowLoadError';
|
|
18
|
+
|
|
19
|
+
export interface PlaylistWindowManagerOptions {
|
|
20
|
+
// The rule engine
|
|
21
|
+
ruleEngine: RE.RuleEngine;
|
|
22
|
+
|
|
23
|
+
// Render data
|
|
24
|
+
state: PlaylistWindowManagerState;
|
|
25
|
+
|
|
26
|
+
// The logger to use
|
|
27
|
+
logger: Logger;
|
|
28
|
+
|
|
29
|
+
// The device object to forward along to each window
|
|
30
|
+
device?: Device;
|
|
31
|
+
|
|
32
|
+
// Set the duration (in seconds) before a presentation will time out
|
|
33
|
+
// waiting for presentation_ready
|
|
34
|
+
readyTimeoutDurationSeconds?: number;
|
|
35
|
+
|
|
36
|
+
// Override the default app renderer url
|
|
37
|
+
appRendererUrl?: string;
|
|
38
|
+
|
|
39
|
+
// The bounds of the window manager, used for child windows
|
|
40
|
+
bounds?: WindowBounds;
|
|
41
|
+
|
|
42
|
+
// The background color of the windows
|
|
43
|
+
backgroundColor?: string;
|
|
44
|
+
|
|
45
|
+
// Override the selected theme of the presentations
|
|
46
|
+
themeOverride?: Theme;
|
|
47
|
+
|
|
48
|
+
// Set to true to force mute all presentations
|
|
49
|
+
forceMute?: boolean;
|
|
50
|
+
|
|
51
|
+
// The parent window id of the playlist window, set when rendering a playlist from a presentation (MZ, Kiosk).
|
|
52
|
+
// parentWindowId is unique across all windows being rendererd and is used by window renderers to lookup the
|
|
53
|
+
// parent window when creating a child window. For example, the BrowserWindowRenderer needs to use a BrowserView
|
|
54
|
+
// to render child windows instead of a BrowserWindow.
|
|
55
|
+
parentWindowId?: string;
|
|
56
|
+
|
|
57
|
+
// The window name of the playlist window, set when rendering a playlist from a presentation (MZ, Kiosk).
|
|
58
|
+
// windowName is unique within the parent window. For example, MZ will spawn two child windows with names
|
|
59
|
+
// 'zone1' and 'zone2'.
|
|
60
|
+
windowName?: string;
|
|
61
|
+
|
|
62
|
+
// Set to true to render the presentation window as a preview.
|
|
63
|
+
isPreview?: boolean;
|
|
64
|
+
|
|
65
|
+
// Called when the current window wants to enable the interrupt lock
|
|
66
|
+
onEnableInterruptLock?: () => void;
|
|
67
|
+
|
|
68
|
+
// Called when the current window wants to disable the interrupt lock
|
|
69
|
+
onDisableInterruptLock?: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Wait at least 3 seconds before restarting or moving onto the next window
|
|
73
|
+
// to prevent log spam causing devices to become unresponsive.
|
|
74
|
+
const minTimeBetweenCompletes = 3000;
|
|
75
|
+
|
|
76
|
+
interface EvalRuleEngineResult {
|
|
77
|
+
next: RE.NextPresentationID;
|
|
78
|
+
preview: RE.NextPresentationID;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default class PlaylistWindowManager extends EventEmitter {
|
|
82
|
+
private currentPresentation: RE.NextPresentationID | null = null;
|
|
83
|
+
private nextPresentation: RE.NextPresentationID | null = null;
|
|
84
|
+
private overrideNextPresentation: RE.NextPresentationID | null = null;
|
|
85
|
+
private currentWindow: PresentationWindow | null = null;
|
|
86
|
+
private nextWindow: PresentationWindow | null = null;
|
|
87
|
+
private hasPendingInterrupt: boolean = false;
|
|
88
|
+
private ruleEngine: RE.RuleEngine | null = null;
|
|
89
|
+
private state: PlaylistWindowManagerState | null = null;
|
|
90
|
+
private renderer: PresentationWindowRenderer | null = null;
|
|
91
|
+
private restartCurrentWindowOnEnd: boolean = false;
|
|
92
|
+
private shouldHardRestart: boolean = false;
|
|
93
|
+
private currentWindowError: null | Error = null;
|
|
94
|
+
private interruptCheckTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
95
|
+
private currentPresentationCompleteTimeout: ReturnType<
|
|
96
|
+
typeof setTimeout
|
|
97
|
+
> | null = null;
|
|
98
|
+
private lastCompleteAt: Date | null = null;
|
|
99
|
+
private bounds?: WindowBounds;
|
|
100
|
+
private themeOverride?: Theme;
|
|
101
|
+
private forceMute: boolean = false;
|
|
102
|
+
private isFocused: boolean = false;
|
|
103
|
+
|
|
104
|
+
private workerState: WorkerState = {};
|
|
105
|
+
|
|
106
|
+
constructor(private options: PlaylistWindowManagerOptions) {
|
|
107
|
+
super();
|
|
108
|
+
|
|
109
|
+
this.ruleEngine = options.ruleEngine;
|
|
110
|
+
this.state = options.state;
|
|
111
|
+
this.bounds = options.bounds;
|
|
112
|
+
this.themeOverride = options.themeOverride;
|
|
113
|
+
this.forceMute = options.forceMute || false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private currentPresentationId(): string | null {
|
|
117
|
+
return this.currentPresentation
|
|
118
|
+
? this.currentPresentation.presentationId
|
|
119
|
+
: null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private nextPresentationId(): string | null {
|
|
123
|
+
return this.nextPresentation ? this.nextPresentation.presentationId : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public async load(): Promise<void> {
|
|
127
|
+
if (!this.renderer) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Attempted to load playlist window manager with missing renderer'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { next, preview } = this.evalRuleEngine();
|
|
134
|
+
|
|
135
|
+
const nextPresentationId = next.presentationId;
|
|
136
|
+
const nextPreviewPresentationId = preview.presentationId;
|
|
137
|
+
|
|
138
|
+
// Resolve immediately if there if there is no presentation to play next.
|
|
139
|
+
if (!nextPresentationId) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// We need to set this.currentPresentation before calling shouldPreload in order
|
|
144
|
+
// to avoid preloading the next presentation id if it's the same as the current.
|
|
145
|
+
this.currentPresentation = next;
|
|
146
|
+
|
|
147
|
+
// Preload next presentation if there is one.
|
|
148
|
+
if (this.shouldPreload(nextPreviewPresentationId)) {
|
|
149
|
+
this.nextPresentation = preview;
|
|
150
|
+
this.preloadNextWindow();
|
|
151
|
+
} else {
|
|
152
|
+
await this.removeNextWindow();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Load and wait for current window before playing.
|
|
156
|
+
await this.loadCurrentWindow();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public isEmpty() {
|
|
160
|
+
if (!this.ruleEngine) return true;
|
|
161
|
+
const preview = this.ruleEngine.previewNext();
|
|
162
|
+
return !preview.presentationId;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public play() {
|
|
166
|
+
this.restartCurrentWindowOnEnd = false;
|
|
167
|
+
this.playCurrentWindow();
|
|
168
|
+
this.startPreviewCheck();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public isPlaying() {
|
|
172
|
+
if (!this.currentWindow) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return this.currentWindow.isPlaying();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public pause() {
|
|
179
|
+
// Pausing a playlist means restarting the current window when it completes
|
|
180
|
+
// instead of moving on to the next one.
|
|
181
|
+
this.restartCurrentWindowOnEnd = true;
|
|
182
|
+
this.pauseCurrentWindow();
|
|
183
|
+
this.stopInterruptCheck();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public resume() {
|
|
187
|
+
// Resuming a playlist will switch to the next window when the current window
|
|
188
|
+
// completes. We don't want to fire 'play' because that could restart the window's
|
|
189
|
+
// on complete timer.
|
|
190
|
+
this.restartCurrentWindowOnEnd = false;
|
|
191
|
+
this.resumeCurrentWindow();
|
|
192
|
+
this.startPreviewCheck();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public blur() {
|
|
196
|
+
// Use this method to stop creating windows that are focused. There is no renderer API for
|
|
197
|
+
// blurring a window because Electron doesn't have a blur() method on BrowserWindows. If focus
|
|
198
|
+
// should be switched to another PlaylistWindowManager call blur() and then show(true) on the other.
|
|
199
|
+
this.isFocused = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public isPaused() {
|
|
203
|
+
return this.restartCurrentWindowOnEnd;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public getRenderer() {
|
|
207
|
+
return this.renderer;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public setRenderer(renderer: PresentationWindowRenderer | null) {
|
|
211
|
+
this.renderer = renderer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public getRuleEngine() {
|
|
215
|
+
return this.ruleEngine;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public async setRuleEngine(ruleEngine: RE.RuleEngine): Promise<void> {
|
|
219
|
+
this.ruleEngine = ruleEngine;
|
|
220
|
+
|
|
221
|
+
// Eval the rule engine with the latest state. Passing true to suppress preload
|
|
222
|
+
// mismatch warning, which is expected because the preloaded presentation of the
|
|
223
|
+
// old playlist won't match the next presentation to preload of the new playlist.
|
|
224
|
+
const { next, preview } = this.evalRuleEngine(false);
|
|
225
|
+
|
|
226
|
+
const nextPresentationId = next.presentationId;
|
|
227
|
+
|
|
228
|
+
// If the window manager doesn't have a renderer, update the current and next
|
|
229
|
+
// presentations but don't load any windows. This handles the case where the
|
|
230
|
+
// window manager is created with an empty playlist and then has a non-empty
|
|
231
|
+
// playlist is published to it.
|
|
232
|
+
if (!this.renderer) {
|
|
233
|
+
this.currentPresentation = next;
|
|
234
|
+
this.nextPresentation = preview;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If there is no next presentation id then we are have updated to an empty playlist.
|
|
239
|
+
// Reset the current and next presentations back to null to ensure isEmpty returns true.
|
|
240
|
+
if (!nextPresentationId) {
|
|
241
|
+
this.currentPresentation = null;
|
|
242
|
+
await this.removeCurrentWindow();
|
|
243
|
+
this.nextPresentation = null;
|
|
244
|
+
await this.removeNextWindow();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// We've already evalulated the rule engine to get the new current presentation and the
|
|
249
|
+
// new next presentation. The current next presentation is replaced with the new current
|
|
250
|
+
// presentation. Since we've already evaluated the new next presentation above, we need to
|
|
251
|
+
// prevent evaling the rule engine again when new current presentation completes by setting
|
|
252
|
+
// this.overrideNextPresentationId. This fixes an issue where the second presentation in the
|
|
253
|
+
// playlists is skipped after reinitializing.
|
|
254
|
+
this.overrideNextPresentation = preview;
|
|
255
|
+
|
|
256
|
+
// Assume the next window is no longer correct when swapping the rule engine.
|
|
257
|
+
await this.removeNextWindow();
|
|
258
|
+
|
|
259
|
+
// Load the new current presentation with the new rule engine.
|
|
260
|
+
this.nextPresentation = next;
|
|
261
|
+
await this.preloadNextWindow();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public getState() {
|
|
265
|
+
return this.state;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public setState(state: PlaylistWindowManagerState) {
|
|
269
|
+
this.state = state;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// For setting dynamic tags data from workers and reevaluate rules for playlists
|
|
273
|
+
// input: newRulesDataStoreObj: {'workers': {'ai': {'percentMales': 30}}}
|
|
274
|
+
public reinitializeRuleEngineState(newRulesDataStoreObj: RE.RulesDataStore) {
|
|
275
|
+
const { logger } = this.options;
|
|
276
|
+
|
|
277
|
+
logger.debug('Dynamic Rule engine state change event received!');
|
|
278
|
+
if (this.ruleEngine) {
|
|
279
|
+
logger.debug(
|
|
280
|
+
`OLD Rule Data Store: ${JSON.stringify(
|
|
281
|
+
this.ruleEngine.state
|
|
282
|
+
)} \n NEW Rule Data Store: ${JSON.stringify(newRulesDataStoreObj)}`
|
|
283
|
+
);
|
|
284
|
+
this.ruleEngine.state.rulesDataStore = newRulesDataStoreObj;
|
|
285
|
+
} else {
|
|
286
|
+
logger.warn(`No Rule engine found for reinitiazling the state!`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// re-evaluate the next presentations depending upon the new rule engine state
|
|
291
|
+
const checkOverridePresentation = async () => {
|
|
292
|
+
if (this.ruleEngine) {
|
|
293
|
+
this.checkPreviewAction();
|
|
294
|
+
|
|
295
|
+
// calling reinitialize Rule engine state for inner playlist windows (for MZ)
|
|
296
|
+
if (this.currentWindow) {
|
|
297
|
+
this.currentWindow.reinitializePresentationWindowRuleEngineState(
|
|
298
|
+
newRulesDataStoreObj
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (this.nextWindow) {
|
|
303
|
+
this.nextWindow.reinitializePresentationWindowRuleEngineState(
|
|
304
|
+
newRulesDataStoreObj
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
checkOverridePresentation();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Replace state and rule engine, and reinitialize the window manager.
|
|
313
|
+
public async reinitialize(
|
|
314
|
+
state: PlaylistWindowManagerState,
|
|
315
|
+
ruleEngine: RE.RuleEngine,
|
|
316
|
+
shouldInterruptPlayback: boolean
|
|
317
|
+
) {
|
|
318
|
+
this.setState(state);
|
|
319
|
+
|
|
320
|
+
await this.setRuleEngine(ruleEngine);
|
|
321
|
+
if (shouldInterruptPlayback) {
|
|
322
|
+
this.registerInterrupt();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private registerInterrupt() {
|
|
327
|
+
if (!this.isLocked()) {
|
|
328
|
+
this.endCurrentPresentation();
|
|
329
|
+
} else {
|
|
330
|
+
this.hasPendingInterrupt = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public endCurrentPresentation() {
|
|
335
|
+
this.handleCurrentPresentationComplete();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public async destroy() {
|
|
339
|
+
this.stopInterruptCheck();
|
|
340
|
+
if (this.currentPresentationCompleteTimeout) {
|
|
341
|
+
clearTimeout(this.currentPresentationCompleteTimeout);
|
|
342
|
+
}
|
|
343
|
+
await Promise.all([this.removeCurrentWindow(), this.removeNextWindow()]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public updateBounds(bounds: WindowBounds) {
|
|
347
|
+
// TODO log bounds
|
|
348
|
+
this.bounds = bounds;
|
|
349
|
+
if (this.currentWindow) {
|
|
350
|
+
this.currentWindow.updateBounds(this.bounds);
|
|
351
|
+
}
|
|
352
|
+
if (this.nextWindow) {
|
|
353
|
+
this.nextWindow.updateBounds(this.bounds);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public getBounds() {
|
|
358
|
+
return this.bounds;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public setThemeOverride(theme?: Theme) {
|
|
362
|
+
this.themeOverride = theme;
|
|
363
|
+
const currentPresentationId = this.currentPresentationId();
|
|
364
|
+
const nextPresentationId = this.nextPresentationId();
|
|
365
|
+
if (this.currentWindow && currentPresentationId) {
|
|
366
|
+
this.currentWindow.setTheme(this.getTheme(currentPresentationId));
|
|
367
|
+
}
|
|
368
|
+
if (this.nextWindow && nextPresentationId) {
|
|
369
|
+
this.nextWindow.setTheme(this.getTheme(nextPresentationId));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public getThemeOverride() {
|
|
374
|
+
return this.themeOverride;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public setForceMute(forceMute: boolean) {
|
|
378
|
+
this.forceMute = forceMute;
|
|
379
|
+
if (this.currentWindow) {
|
|
380
|
+
this.currentWindow.setForceMute(forceMute);
|
|
381
|
+
}
|
|
382
|
+
if (this.nextWindow) {
|
|
383
|
+
this.nextWindow.setForceMute(forceMute);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
public getForceMute() {
|
|
388
|
+
return this.forceMute;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
public reload() {
|
|
392
|
+
if (this.currentWindow) {
|
|
393
|
+
this.currentWindow.reload();
|
|
394
|
+
}
|
|
395
|
+
if (this.nextWindow) {
|
|
396
|
+
this.nextWindow.reload();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
public async show(isFocused: boolean = true) {
|
|
401
|
+
if (this.currentWindow) {
|
|
402
|
+
// Save the focused state so we know if preloaded windows should be
|
|
403
|
+
// focused when swapped for the current.
|
|
404
|
+
this.isFocused = isFocused;
|
|
405
|
+
this.currentWindow.show(isFocused);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public async hide() {
|
|
410
|
+
if (this.currentWindow) {
|
|
411
|
+
this.currentWindow.hide();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
public isReady() {
|
|
416
|
+
if (!this.currentWindow) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
return this.currentWindow.isReady();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public isLocked() {
|
|
423
|
+
if (!this.currentWindow) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return this.currentWindow.isLocked();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
public runWorkerSubscriptions(state: WorkerState) {
|
|
430
|
+
// Persist worker state so new windows can be initialized with it.
|
|
431
|
+
this.workerState = state;
|
|
432
|
+
|
|
433
|
+
// Run worker subcriptions on the current window.
|
|
434
|
+
if (this.currentWindow) {
|
|
435
|
+
this.currentWindow.runWorkerSubscriptions(state);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Run worker subscriptions on the next window.
|
|
439
|
+
if (this.nextWindow) {
|
|
440
|
+
this.nextWindow.runWorkerSubscriptions(state);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private createWindow(pr: RE.NextPresentationID): PresentationWindow {
|
|
445
|
+
const {
|
|
446
|
+
readyTimeoutDurationSeconds,
|
|
447
|
+
appRendererUrl,
|
|
448
|
+
backgroundColor,
|
|
449
|
+
device,
|
|
450
|
+
logger,
|
|
451
|
+
windowName,
|
|
452
|
+
parentWindowId,
|
|
453
|
+
isPreview
|
|
454
|
+
} = this.options;
|
|
455
|
+
|
|
456
|
+
if (!this.ruleEngine) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
'Attempted to create window but ruleEngine was not provided'
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!this.state) {
|
|
463
|
+
throw new Error('Attempted to create window but state was not provided');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!this.renderer) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
'Attempted to create window but renderer was not provided'
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const presentationId = pr.presentationId;
|
|
473
|
+
if (!presentationId) {
|
|
474
|
+
throw new Error('Attempted to create window but presentationId is null');
|
|
475
|
+
}
|
|
476
|
+
const presentation = this.state.getPresentation(presentationId);
|
|
477
|
+
const applicationVersion = this.state.getApplicationVersion(
|
|
478
|
+
presentation.applicationDeploymentId
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const windowId = uuidv4();
|
|
482
|
+
|
|
483
|
+
return new PresentationWindow({
|
|
484
|
+
windowManager: this,
|
|
485
|
+
renderer: this.renderer,
|
|
486
|
+
logger,
|
|
487
|
+
windowId,
|
|
488
|
+
windowName,
|
|
489
|
+
parentWindowId,
|
|
490
|
+
presentation,
|
|
491
|
+
applicationVersion,
|
|
492
|
+
device,
|
|
493
|
+
playlistId: pr.playlistId,
|
|
494
|
+
playlistName: this.ruleEngine.state.playlist(pr.playlistId).name,
|
|
495
|
+
rule: pr.rule,
|
|
496
|
+
publishedAt: this.state.getPublishedAt(),
|
|
497
|
+
theme: this.getTheme(presentationId),
|
|
498
|
+
forceMute: this.forceMute,
|
|
499
|
+
readyTimeoutDurationSeconds,
|
|
500
|
+
appRendererUrl,
|
|
501
|
+
bounds: this.bounds,
|
|
502
|
+
backgroundColor,
|
|
503
|
+
playlistWindowManagerState: this.state,
|
|
504
|
+
ruleEngineState: this.ruleEngine.state,
|
|
505
|
+
globalRule: this.ruleEngine.globalRule,
|
|
506
|
+
isPreview,
|
|
507
|
+
workerState: this.workerState
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async loadCurrentWindow() {
|
|
512
|
+
const { logger } = this.options;
|
|
513
|
+
|
|
514
|
+
if (!this.currentPresentation) {
|
|
515
|
+
logger.warn(
|
|
516
|
+
`Attempted to load current window but currentPresentation not set`
|
|
517
|
+
);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.currentWindow = this.createWindow(this.currentPresentation);
|
|
522
|
+
this.currentWindow.onComplete(this.handleCurrentPresentationComplete);
|
|
523
|
+
this.currentWindow.onError(this.handleCurrentPresentationError);
|
|
524
|
+
this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
|
|
525
|
+
this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
|
|
526
|
+
this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
|
|
527
|
+
|
|
528
|
+
await this.currentWindow.load();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private shouldPreload(
|
|
532
|
+
presentationId: string | null
|
|
533
|
+
): presentationId is string {
|
|
534
|
+
// Don't preload if there is no presentation.
|
|
535
|
+
if (presentationId === null) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
// Don't preload the next presentation if it's already preloaded. When the window manager
|
|
539
|
+
// is first created it will eval the rule engine and set this.nextPresentationId but not
|
|
540
|
+
// this.nextWindow.
|
|
541
|
+
if (presentationId === this.nextPresentationId()) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
// An optimization to avoid preloading the same presentation we are currenly playing.
|
|
545
|
+
// When the current presentation completes we will play it again and then check for
|
|
546
|
+
// a new presentation to preload.
|
|
547
|
+
if (presentationId === this.currentPresentationId()) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private async preloadNextWindow() {
|
|
554
|
+
const { logger } = this.options;
|
|
555
|
+
|
|
556
|
+
if (!this.nextPresentation) {
|
|
557
|
+
logger.warn(
|
|
558
|
+
`Attempted to preload next window but nextPresentation not set`
|
|
559
|
+
);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.nextWindow = this.createWindow(this.nextPresentation);
|
|
564
|
+
this.nextWindow.onError(this.handleNextPresentationError);
|
|
565
|
+
await this.nextWindow.load();
|
|
566
|
+
|
|
567
|
+
// If there is no current window and the next window successfully loaded, show the preloaded window.
|
|
568
|
+
// NOTE: Apps that use raydiant-kit's onError handler will end up showing an error on screen anyways
|
|
569
|
+
// because the onError method fires presentation_ready before sending the error. We should fix this
|
|
570
|
+
// but would require updating raydiant-kit and then all apps.
|
|
571
|
+
if (!this.currentWindow && !this.nextWindow.didError()) {
|
|
572
|
+
this.handleCurrentPresentationComplete();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private playCurrentWindow() {
|
|
577
|
+
const { logger } = this.options;
|
|
578
|
+
|
|
579
|
+
if (this.currentWindow) {
|
|
580
|
+
this.currentWindow.play();
|
|
581
|
+
this.emitCurrentWindowEvent('window_play');
|
|
582
|
+
} else {
|
|
583
|
+
logger.warn(`Attempted to play current window but it doesn't exist`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private pauseCurrentWindow() {
|
|
588
|
+
const { logger } = this.options;
|
|
589
|
+
|
|
590
|
+
if (this.currentWindow) {
|
|
591
|
+
this.currentWindow.pause();
|
|
592
|
+
} else {
|
|
593
|
+
logger.warn(`Attempted to pause current window but it doesn't exist`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private resumeCurrentWindow() {
|
|
598
|
+
const { logger } = this.options;
|
|
599
|
+
|
|
600
|
+
if (this.currentWindow) {
|
|
601
|
+
this.currentWindow.resume();
|
|
602
|
+
} else {
|
|
603
|
+
logger.warn(`Attempted to resume current window but it doesn't exist`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private async restartCurrentWindow() {
|
|
608
|
+
const { logger } = this.options;
|
|
609
|
+
|
|
610
|
+
if (this.currentWindow) {
|
|
611
|
+
if (this.shouldHardRestart) {
|
|
612
|
+
logger.info(`Restart current window with hard restart`);
|
|
613
|
+
await this.removeCurrentWindow();
|
|
614
|
+
await this.loadCurrentWindow();
|
|
615
|
+
this.currentWindow.show(this.isFocused);
|
|
616
|
+
} else {
|
|
617
|
+
logger.info(`Restart current window`);
|
|
618
|
+
this.currentWindow.reset();
|
|
619
|
+
}
|
|
620
|
+
this.playCurrentWindow();
|
|
621
|
+
} else {
|
|
622
|
+
logger.warn(`Attempted to restart current window but it doesn't exist`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private async removeCurrentWindow() {
|
|
627
|
+
if (this.currentWindow) {
|
|
628
|
+
await this.currentWindow.remove();
|
|
629
|
+
}
|
|
630
|
+
this.currentWindow = null;
|
|
631
|
+
this.currentPresentation = null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private async removeNextWindow() {
|
|
635
|
+
if (this.nextWindow) {
|
|
636
|
+
await this.nextWindow.remove();
|
|
637
|
+
}
|
|
638
|
+
this.nextWindow = null;
|
|
639
|
+
this.nextPresentation = null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private async swapWindows() {
|
|
643
|
+
// Get the new next presentation to preload after swapping.
|
|
644
|
+
let nextPreviewPresentationId: string | null = null;
|
|
645
|
+
let preview: RE.NextPresentationID | null = null;
|
|
646
|
+
if (this.overrideNextPresentation) {
|
|
647
|
+
preview = this.overrideNextPresentation;
|
|
648
|
+
nextPreviewPresentationId = this.overrideNextPresentation.presentationId;
|
|
649
|
+
this.overrideNextPresentation = null;
|
|
650
|
+
} else {
|
|
651
|
+
const result = this.evalRuleEngine();
|
|
652
|
+
preview = result.preview;
|
|
653
|
+
nextPreviewPresentationId = preview.presentationId;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Set this.currentPresentationId before removing the current window to avoid timing issues with previewCheck
|
|
657
|
+
// triggering during removeCurrentWindow. Current presentation id needs to be set to the next
|
|
658
|
+
// presentation before calling shouldPreload().
|
|
659
|
+
this.currentPresentation = this.nextPresentation;
|
|
660
|
+
// We need to call shouldPreload() before setting this.nextPresentationId. Store the result
|
|
661
|
+
// here so we can preload the new next presentation after removing the current. Calling
|
|
662
|
+
// shouldPreload() again after setting this.nextPresentationId will cause the next window
|
|
663
|
+
// to not preload.
|
|
664
|
+
const shouldPreload = this.shouldPreload(nextPreviewPresentationId);
|
|
665
|
+
if (shouldPreload) {
|
|
666
|
+
// Set this.nextPresentationId before removing the current window. This prevents
|
|
667
|
+
// race conditions with previewCheck when using the window pool, where removeWindow
|
|
668
|
+
// is async because we unload the window instead of destroying it.
|
|
669
|
+
this.nextPresentation = preview;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Clear the pending interrupt whenever we swap the current window for the next one.
|
|
673
|
+
this.hasPendingInterrupt = false;
|
|
674
|
+
|
|
675
|
+
// Next window is ready, show the next one, hide the current window, and
|
|
676
|
+
// then remove the current window. Show the next window before hiding the
|
|
677
|
+
// current to avoid flashes when rendering with BrowserWindows on device.
|
|
678
|
+
if (this.nextWindow) {
|
|
679
|
+
this.nextWindow.show(this.isFocused);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// If current window isn't defined we don't need to hide it but we still
|
|
683
|
+
// want to make the next window the new current window.
|
|
684
|
+
if (this.currentWindow) {
|
|
685
|
+
this.currentWindow.hide();
|
|
686
|
+
await this.removeCurrentWindow();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Swap windows.
|
|
690
|
+
this.currentWindow = this.nextWindow;
|
|
691
|
+
if (this.currentWindow) {
|
|
692
|
+
this.currentWindow.onComplete(this.handleCurrentPresentationComplete);
|
|
693
|
+
this.currentWindow.onError(this.handleCurrentPresentationError);
|
|
694
|
+
this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
|
|
695
|
+
this.currentWindow.onDisableInterruptLock(
|
|
696
|
+
this.handleDisableInterruptLock
|
|
697
|
+
);
|
|
698
|
+
this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Play new current window.
|
|
702
|
+
this.playCurrentWindow();
|
|
703
|
+
|
|
704
|
+
// Preload the next presentation if there is one.
|
|
705
|
+
if (shouldPreload) {
|
|
706
|
+
this.preloadNextWindow();
|
|
707
|
+
} else {
|
|
708
|
+
// Set next window to null to reset preload state. We don't want to
|
|
709
|
+
// remove the next window because it's the new current window.
|
|
710
|
+
this.nextWindow = null;
|
|
711
|
+
this.nextPresentation = null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private evalRuleEngine(warnIfPreloadMismatch = true): EvalRuleEngineResult {
|
|
716
|
+
const { logger } = this.options;
|
|
717
|
+
|
|
718
|
+
if (!this.ruleEngine) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
'Attempted to eval rule engine but ruleEngine not provided'
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const next = this.ruleEngine.next();
|
|
725
|
+
const preview = this.ruleEngine.previewNext();
|
|
726
|
+
|
|
727
|
+
const nextPresentationId = next.presentationId;
|
|
728
|
+
|
|
729
|
+
if (
|
|
730
|
+
this.nextPresentationId() !== null &&
|
|
731
|
+
nextPresentationId !== this.nextPresentationId() &&
|
|
732
|
+
warnIfPreloadMismatch
|
|
733
|
+
) {
|
|
734
|
+
// TODO: Handle the case when the next presentation isn't the preloaded window.
|
|
735
|
+
// This can happen when we support dynamic rule engine state.
|
|
736
|
+
logger.warn(
|
|
737
|
+
`The next presentation id '${nextPresentationId}' doesnt match preloaded presentation id '${this.nextPresentationId()}'`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return { next, preview };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private handleEnableInterruptLock = () => {
|
|
745
|
+
const { onEnableInterruptLock } = this.options;
|
|
746
|
+
if (onEnableInterruptLock) {
|
|
747
|
+
onEnableInterruptLock();
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
private handleDisableInterruptLock = () => {
|
|
752
|
+
const { onDisableInterruptLock } = this.options;
|
|
753
|
+
if (onDisableInterruptLock) {
|
|
754
|
+
onDisableInterruptLock();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (this.hasPendingInterrupt) {
|
|
758
|
+
this.endCurrentPresentation();
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
private handleAnalyticsEvent = (event: AnalyticsEvent) => {
|
|
763
|
+
// Emit the event on the current window. Only the current window should
|
|
764
|
+
// send analytics events since the next window is not visible.
|
|
765
|
+
this.emitCurrentWindowEvent('window_analytics_event', event);
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
private handleCurrentPresentationComplete = async () => {
|
|
769
|
+
const { logger } = this.options;
|
|
770
|
+
|
|
771
|
+
// First check if we need to throttle the presentation complete handler.
|
|
772
|
+
let isThrottled = false;
|
|
773
|
+
if (this.lastCompleteAt !== null) {
|
|
774
|
+
const now = new Date();
|
|
775
|
+
const timeSinceLastComplete = +now - +this.lastCompleteAt;
|
|
776
|
+
|
|
777
|
+
if (timeSinceLastComplete < minTimeBetweenCompletes) {
|
|
778
|
+
isThrottled = true;
|
|
779
|
+
|
|
780
|
+
// Throttle presentation complete handler if we have not throttled already.
|
|
781
|
+
if (this.currentPresentationCompleteTimeout === null) {
|
|
782
|
+
const delay = minTimeBetweenCompletes - timeSinceLastComplete + 1;
|
|
783
|
+
|
|
784
|
+
logger.warn(
|
|
785
|
+
`Throttling presentation complete handler for ${delay}ms because it was called again in less than ${minTimeBetweenCompletes}ms`
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
this.currentPresentationCompleteTimeout = setTimeout(() => {
|
|
789
|
+
this.currentPresentationCompleteTimeout = null;
|
|
790
|
+
this.handleCurrentPresentationComplete();
|
|
791
|
+
}, delay);
|
|
792
|
+
} else {
|
|
793
|
+
logger.warn(
|
|
794
|
+
`Skipping presentation complete because it is currently throttled`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Don't run presentation complete handler when throttled.
|
|
801
|
+
if (isThrottled) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Not throttled, update lastCompleteAt and run presentation complete handler.
|
|
806
|
+
this.lastCompleteAt = new Date();
|
|
807
|
+
|
|
808
|
+
// Emit presentation end events.
|
|
809
|
+
if (this.currentWindowError) {
|
|
810
|
+
this.emitCurrentWindowErrorEvent(
|
|
811
|
+
'window_end_error',
|
|
812
|
+
this.currentWindowError
|
|
813
|
+
);
|
|
814
|
+
} else {
|
|
815
|
+
this.emitCurrentWindowEvent('window_end_success');
|
|
816
|
+
}
|
|
817
|
+
// Clear current window error flag.
|
|
818
|
+
this.currentWindowError = null;
|
|
819
|
+
|
|
820
|
+
// If not locked or paused, check if we can move to the next presentation.
|
|
821
|
+
if (
|
|
822
|
+
!this.isLocked() &&
|
|
823
|
+
!this.restartCurrentWindowOnEnd &&
|
|
824
|
+
this.currentWindow &&
|
|
825
|
+
this.nextWindow
|
|
826
|
+
) {
|
|
827
|
+
// If the next window is ready, swap windows and preload the next one.
|
|
828
|
+
if (this.nextWindow.isReady()) {
|
|
829
|
+
await this.swapWindows();
|
|
830
|
+
} else {
|
|
831
|
+
// The next window isn't ready yet, restart the current presentation.
|
|
832
|
+
// restartCurrentWindow depends on this.shouldHardRestart
|
|
833
|
+
await this.restartCurrentWindow();
|
|
834
|
+
// TODO: Do we want to clear shouldHardRestart and currentWindowError on restart?
|
|
835
|
+
}
|
|
836
|
+
} else if (this.currentWindow) {
|
|
837
|
+
// If we aren't supposed to swap to the next window, check if the current window should
|
|
838
|
+
// be restarted or removed.
|
|
839
|
+
|
|
840
|
+
// Eval the rule engine to see if we should still be playing the current window or if
|
|
841
|
+
// there's a new next window we need to preload.
|
|
842
|
+
const { next, preview } = this.evalRuleEngine();
|
|
843
|
+
const nextPreviewPresentationId = preview.presentationId;
|
|
844
|
+
|
|
845
|
+
if (!next.presentationId) {
|
|
846
|
+
// If next.presentationId is null after evaluating the rule engine that means
|
|
847
|
+
// nothing should be playing, remove the current window.
|
|
848
|
+
this.removeCurrentWindow();
|
|
849
|
+
} else {
|
|
850
|
+
// Restart the current presentation when:
|
|
851
|
+
// - There's only one presentation in the playlist (no preload)
|
|
852
|
+
// - The playlist is locked or paused
|
|
853
|
+
// - There is a still a presentation that should be
|
|
854
|
+
// NOTE: restartCurrentWindow depends on this.shouldHardRestart
|
|
855
|
+
await this.restartCurrentWindow();
|
|
856
|
+
|
|
857
|
+
// Check to see if there is a presentation to preload. This can happen if
|
|
858
|
+
// if state has changed since initial load (ie. publish).
|
|
859
|
+
if (this.shouldPreload(nextPreviewPresentationId)) {
|
|
860
|
+
this.nextPresentation = preview;
|
|
861
|
+
this.preloadNextWindow();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} else if (this.nextWindow) {
|
|
865
|
+
// There is no current window but there is a next window. Swap to the next window.
|
|
866
|
+
// This can happen if updating the rule engine from an empty playlist to a non-empty playlist.
|
|
867
|
+
await this.swapWindows();
|
|
868
|
+
} else {
|
|
869
|
+
logger.warn(
|
|
870
|
+
`Presentation complete received but there's no current window.`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Clear hard restart after running complete handler. This could be true when the current window errors
|
|
875
|
+
// but if there's another presentation to move on to we won't restart it but we still need to clear it
|
|
876
|
+
// so we don't hard restart the new current window when we shouldn't.
|
|
877
|
+
this.shouldHardRestart = false;
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
private handleCurrentPresentationError = async (error: any) => {
|
|
881
|
+
const { logger } = this.options;
|
|
882
|
+
|
|
883
|
+
logger.warn(
|
|
884
|
+
`Current presentation had an error, move to next presentation or hard restart current window`
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const isWindowError =
|
|
888
|
+
error instanceof WindowUnresponsiveError ||
|
|
889
|
+
error instanceof WindowTimeoutError ||
|
|
890
|
+
error instanceof WindowLoadError;
|
|
891
|
+
|
|
892
|
+
if (isWindowError) {
|
|
893
|
+
// Error is window error, hard restart the window if it's the only presentation in the playlist.
|
|
894
|
+
this.shouldHardRestart = true;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Flag the current presentation as having errored.
|
|
898
|
+
this.currentWindowError = error;
|
|
899
|
+
|
|
900
|
+
this.handleCurrentPresentationComplete();
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
private handleNextPresentationError = async (error: any) => {
|
|
904
|
+
const { logger } = this.options;
|
|
905
|
+
|
|
906
|
+
// Emit event that preload has failed.
|
|
907
|
+
this.emitNextWindowErrorEvent('window_preload_error', error);
|
|
908
|
+
|
|
909
|
+
logger.warn(
|
|
910
|
+
`Next presentation had an error, preload the next presentation`
|
|
911
|
+
);
|
|
912
|
+
const { preview } = this.evalRuleEngine();
|
|
913
|
+
const nextPreviewPresentationId = preview.presentationId;
|
|
914
|
+
|
|
915
|
+
if (this.shouldPreload(nextPreviewPresentationId)) {
|
|
916
|
+
// Remove the window before preloading a new one.
|
|
917
|
+
await this.removeNextWindow();
|
|
918
|
+
// Calling removeNextWindow will set this.nextPresentation to null so we need to
|
|
919
|
+
// set after but before preloadNextWindow.
|
|
920
|
+
this.nextPresentation = preview;
|
|
921
|
+
this.preloadNextWindow();
|
|
922
|
+
} else {
|
|
923
|
+
await this.removeNextWindow();
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
private checkPreviewAction = async () => {
|
|
928
|
+
const { logger } = this.options;
|
|
929
|
+
if (!this.ruleEngine) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const preview = this.ruleEngine.previewNext();
|
|
933
|
+
|
|
934
|
+
if (this.shouldPreload(preview.presentationId)) {
|
|
935
|
+
logger.info(
|
|
936
|
+
`Preview check replacing preloaded presentation '${this.nextPresentationId()}' with new presentation '${
|
|
937
|
+
preview.presentationId
|
|
938
|
+
}'`
|
|
939
|
+
);
|
|
940
|
+
await this.removeNextWindow();
|
|
941
|
+
this.nextPresentation = preview;
|
|
942
|
+
await this.preloadNextWindow();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (preview.shouldInterrupt) {
|
|
946
|
+
logger.info(
|
|
947
|
+
`Interrupting current presentation with '${preview.presentationId}'`
|
|
948
|
+
);
|
|
949
|
+
this.registerInterrupt();
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
private startPreviewCheck() {
|
|
954
|
+
const checkPreview = async () => {
|
|
955
|
+
if (this.ruleEngine) {
|
|
956
|
+
this.checkPreviewAction();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Check the preview again on the next minute.
|
|
960
|
+
const now = new Date();
|
|
961
|
+
const nextMinute = new Date();
|
|
962
|
+
nextMinute.setMilliseconds(0);
|
|
963
|
+
nextMinute.setSeconds(1);
|
|
964
|
+
nextMinute.setMinutes(now.getMinutes() + 1);
|
|
965
|
+
const timeUntilNextMinute = +nextMinute - +now;
|
|
966
|
+
|
|
967
|
+
this.interruptCheckTimeout = setTimeout(
|
|
968
|
+
checkPreview,
|
|
969
|
+
timeUntilNextMinute
|
|
970
|
+
);
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
checkPreview();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private stopInterruptCheck() {
|
|
977
|
+
if (this.interruptCheckTimeout) {
|
|
978
|
+
clearTimeout(this.interruptCheckTimeout);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
private getTheme(presentationId: string) {
|
|
983
|
+
if (!this.state) return;
|
|
984
|
+
|
|
985
|
+
let theme: Theme | undefined;
|
|
986
|
+
|
|
987
|
+
const presentation = this.state.getPresentation(presentationId);
|
|
988
|
+
|
|
989
|
+
if (this.themeOverride) {
|
|
990
|
+
theme = this.themeOverride;
|
|
991
|
+
} else if (presentation.themeId && this.state) {
|
|
992
|
+
theme = this.state.getTheme(presentation.themeId);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return theme;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private emitWindowEvent(
|
|
999
|
+
label: string,
|
|
1000
|
+
event: string,
|
|
1001
|
+
window: PresentationWindow | null,
|
|
1002
|
+
data?: any
|
|
1003
|
+
) {
|
|
1004
|
+
const { logger } = this.options;
|
|
1005
|
+
|
|
1006
|
+
if (window) {
|
|
1007
|
+
this.emit(event, {
|
|
1008
|
+
...window.metadata(),
|
|
1009
|
+
data
|
|
1010
|
+
});
|
|
1011
|
+
} else {
|
|
1012
|
+
logger.warn(`Attempted to emit ${label} window event but window not set`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private emitCurrentWindowEvent(event: string, data?: any) {
|
|
1017
|
+
return this.emitWindowEvent('current', event, this.currentWindow, data);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private emitNextWindowEvent(event: string, data?: any) {
|
|
1021
|
+
return this.emitWindowEvent('next', event, this.nextWindow, data);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private emitCurrentWindowErrorEvent(event: string, error: any) {
|
|
1025
|
+
return this.emitCurrentWindowEvent(event, {
|
|
1026
|
+
errorMessage: this.getErrorMessage(error)
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
private emitNextWindowErrorEvent(event: string, error: any) {
|
|
1031
|
+
return this.emitNextWindowEvent(event, {
|
|
1032
|
+
errorMessage: this.getErrorMessage(error)
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private getErrorMessage(error: any) {
|
|
1037
|
+
if (typeof error === 'string') return error;
|
|
1038
|
+
if (error && typeof error === 'object' && error.message)
|
|
1039
|
+
return error.message;
|
|
1040
|
+
return '';
|
|
1041
|
+
}
|
|
1042
|
+
}
|