@raydiant/playlist-window-manager 0.15.2 → 0.17.0

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.
@@ -1,6 +1,6 @@
1
1
  import * as RE from '@raydiant/playlist-rule-engine';
2
2
  import EventEmitter from 'eventemitter3';
3
- import { PresentationWindowRenderer, Device, WindowBounds, Theme, Logger } from './types';
3
+ import { PresentationWindowRenderer, Device, WindowBounds, Theme, Logger, WorkerState } from './types';
4
4
  import PlaylistWindowManagerState from './PlaylistWindowManagerState';
5
5
  export interface PlaylistWindowManagerOptions {
6
6
  ruleEngine: RE.RuleEngine;
@@ -40,6 +40,7 @@ export default class PlaylistWindowManager extends EventEmitter {
40
40
  private themeOverride?;
41
41
  private forceMute;
42
42
  private isFocused;
43
+ private workerState;
43
44
  constructor(options: PlaylistWindowManagerOptions);
44
45
  private currentPresentationId;
45
46
  private nextPresentationId;
@@ -73,6 +74,7 @@ export default class PlaylistWindowManager extends EventEmitter {
73
74
  hide(): Promise<void>;
74
75
  isReady(): boolean;
75
76
  isLocked(): boolean;
77
+ runWorkerSubscriptions(state: WorkerState): void;
76
78
  private createWindow;
77
79
  private loadCurrentWindow;
78
80
  private shouldPreload;
@@ -93,6 +93,7 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
93
93
  _this.lastCompleteAt = null;
94
94
  _this.forceMute = false;
95
95
  _this.isFocused = false;
96
+ _this.workerState = {};
96
97
  _this.handleEnableInterruptLock = function () {
97
98
  var onEnableInterruptLock = _this.options.onEnableInterruptLock;
98
99
  if (onEnableInterruptLock) {
@@ -619,6 +620,18 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
619
620
  }
620
621
  return this.currentWindow.isLocked();
621
622
  };
623
+ PlaylistWindowManager.prototype.runWorkerSubscriptions = function (state) {
624
+ // Persist worker state so new windows can be initialized with it.
625
+ this.workerState = state;
626
+ // Run worker subcriptions on the current window.
627
+ if (this.currentWindow) {
628
+ this.currentWindow.runWorkerSubscriptions(state);
629
+ }
630
+ // Run worker subscriptions on the next window.
631
+ if (this.nextWindow) {
632
+ this.nextWindow.runWorkerSubscriptions(state);
633
+ }
634
+ };
622
635
  PlaylistWindowManager.prototype.createWindow = function (pr) {
623
636
  var _a = this.options, readyTimeoutDurationSeconds = _a.readyTimeoutDurationSeconds, appRendererUrl = _a.appRendererUrl, backgroundColor = _a.backgroundColor, device = _a.device, logger = _a.logger, windowName = _a.windowName, parentWindowId = _a.parentWindowId, isPreview = _a.isPreview;
624
637
  if (!this.ruleEngine) {
@@ -660,7 +673,8 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
660
673
  playlistWindowManagerState: this.state,
661
674
  ruleEngineState: this.ruleEngine.state,
662
675
  globalRule: this.ruleEngine.globalRule,
663
- isPreview: isPreview
676
+ isPreview: isPreview,
677
+ workerState: this.workerState
664
678
  });
665
679
  };
666
680
  PlaylistWindowManager.prototype.loadCurrentWindow = function () {
@@ -1,7 +1,7 @@
1
1
  import * as RE from '@raydiant/playlist-rule-engine';
2
2
  import PlaylistWindowManagerState from './PlaylistWindowManagerState';
3
3
  import PlaylistWindowManager from './PlaylistWindowManager';
4
- import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState } from './types';
4
+ import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState, WorkerState } from './types';
5
5
  export interface PresentationWindowOptions {
6
6
  windowManager?: PlaylistWindowManager;
7
7
  windowId: string;
@@ -27,6 +27,7 @@ export interface PresentationWindowOptions {
27
27
  backgroundColor?: string;
28
28
  isPreview?: boolean;
29
29
  forceMute?: boolean;
30
+ workerState: WorkerState;
30
31
  onPresentationProperties?: (properties: any[], string: {
31
32
  [key: string]: string;
32
33
  }) => void;
@@ -55,6 +56,9 @@ export default class PresentationWindow {
55
56
  private theme;
56
57
  private forceMute;
57
58
  private error;
59
+ private workerState;
60
+ private workerSubscriptions;
61
+ private runtimeSubscriptions;
58
62
  constructor(options: PresentationWindowOptions);
59
63
  load(): Promise<void>;
60
64
  show(isFocused?: boolean): void;
@@ -84,8 +88,9 @@ export default class PresentationWindow {
84
88
  private isEmbeddedApp;
85
89
  private allChildWindowManagersReady;
86
90
  private checkIsReadyCallback;
91
+ private appendWindowIdToHash;
87
92
  private loadEmbeddedApp;
88
- private loadRaydiantApp;
93
+ private loadLegacyApp;
89
94
  private createAndLoadWindow;
90
95
  private loadWindow;
91
96
  private sendMessage;
@@ -96,7 +101,10 @@ export default class PresentationWindow {
96
101
  private closeChildWindow;
97
102
  private enableInteruptLock;
98
103
  private disableInteruptLock;
99
- private handleMessage;
104
+ private handleEmbeddedMessage;
105
+ private handleLegacyMessage;
106
+ private handleReady;
107
+ private handleComplete;
100
108
  private handleError;
101
109
  private logDebug;
102
110
  private logInfo;
@@ -117,6 +125,7 @@ export default class PresentationWindow {
117
125
  playlistName: string | undefined;
118
126
  rule: RE.RuleOperatorType | RE.RuleGroupType | RE.RuleScheduleType | null | undefined;
119
127
  };
128
+ runWorkerSubscriptions(state: WorkerState): void;
120
129
  private createRuleEngine;
121
130
  private isValidOpenChildWindowPayload;
122
131
  private shouldMute;
@@ -63,7 +63,7 @@ var PlaylistWindowManager_1 = __importDefault(require("./PlaylistWindowManager")
63
63
  var WindowTimeoutError_1 = __importDefault(require("./WindowTimeoutError"));
64
64
  var WindowLoadError_1 = __importDefault(require("./WindowLoadError"));
65
65
  // TODO: Automatically inject latest version when building in CI.
66
- var defaultAppRendererUrl = 'https://apps.raydiant.com/1.4.7/index.html';
66
+ var defaultAppRendererUrl = 'https://apps.raydiant.com/2.0.0/index.html';
67
67
  var isValidNumber = function (value) {
68
68
  return typeof value === 'number' && !isNaN(value);
69
69
  };
@@ -89,7 +89,93 @@ var PresentationWindow = /** @class */ (function () {
89
89
  this.disableInterruptLockCallback = null;
90
90
  this.forceMute = false;
91
91
  this.error = null;
92
- this.handleMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
92
+ this.workerState = {};
93
+ this.workerSubscriptions = new Set();
94
+ this.runtimeSubscriptions = new Set();
95
+ this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
96
+ var windowId, workerState;
97
+ return __generator(this, function (_a) {
98
+ windowId = this.options.windowId;
99
+ switch (type) {
100
+ case 'ready': {
101
+ this.logInfo("Received 'ready' from window '" + windowId + "'");
102
+ this.handleReady();
103
+ break;
104
+ }
105
+ case 'complete': {
106
+ if (payload === undefined) {
107
+ this.logInfo("Received 'complete' from window '" + windowId + "'");
108
+ this.handleComplete();
109
+ }
110
+ else {
111
+ this.logInfo("Received 'complete' with error from window '" + windowId + ", error=" + payload + "'");
112
+ this.handleError(payload);
113
+ }
114
+ break;
115
+ }
116
+ case 'runtime-subscribe': {
117
+ if (typeof payload === 'string') {
118
+ this.logInfo("Received 'runtime-subscribe' from window '" + windowId + "'");
119
+ // The payload is the event name (ex. 'play').
120
+ this.runtimeSubscriptions.add(payload);
121
+ // Immediately send play if the app if already playing.
122
+ if (this.isPlaying()) {
123
+ this.sendMessage('play');
124
+ }
125
+ }
126
+ else {
127
+ this.logWarning("Invalid payload for runtime-subscribe message: " + payload);
128
+ }
129
+ break;
130
+ }
131
+ case 'runtime-unsubscribe': {
132
+ if (typeof payload === 'string') {
133
+ this.logInfo("Received 'runtime-unsubscribe' from window '" + windowId + "'");
134
+ // The payload is the event name (ex. 'play').
135
+ this.runtimeSubscriptions.delete(payload);
136
+ }
137
+ else {
138
+ this.logWarning("Invalid payload for runtime-subscribe message: " + payload);
139
+ }
140
+ break;
141
+ }
142
+ case 'worker-subscribe': {
143
+ if (typeof payload === 'string') {
144
+ this.logInfo("Received 'worker-subscribe' from window '" + windowId + "'");
145
+ // The payload is the worker name.
146
+ this.workerSubscriptions.add(payload);
147
+ workerState = this.workerState[payload];
148
+ if (workerState !== undefined) {
149
+ this.sendMessage('worker-state', {
150
+ name: payload,
151
+ state: workerState
152
+ });
153
+ }
154
+ }
155
+ else {
156
+ this.logWarning("Invalid payload for worker-subscribe message: " + payload);
157
+ }
158
+ break;
159
+ }
160
+ case 'worker-unsubscribe': {
161
+ if (typeof payload === 'string') {
162
+ this.logInfo("Received 'worker-unsubscribe' from window '" + windowId + "'");
163
+ // The payload is the worker name.
164
+ this.workerSubscriptions.delete(payload);
165
+ }
166
+ else {
167
+ this.logWarning("Invalid payload for worker-unsubscribe message: " + payload);
168
+ }
169
+ break;
170
+ }
171
+ default: {
172
+ this.logWarning("Received unknown embedded message type: " + type);
173
+ }
174
+ }
175
+ return [2 /*return*/];
176
+ });
177
+ }); };
178
+ this.handleLegacyMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
93
179
  var _a, windowId, onPresentationProperties;
94
180
  return __generator(this, function (_b) {
95
181
  _a = this.options, windowId = _a.windowId, onPresentationProperties = _a.onPresentationProperties;
@@ -99,15 +185,11 @@ var PresentationWindow = /** @class */ (function () {
99
185
  }
100
186
  if (type === 'presentation_ready') {
101
187
  this.logInfo("Received 'presentation_ready' from window '" + windowId + "'");
102
- this.didReceiveReady = true;
103
- this.checkIsReadyCallback();
188
+ this.handleReady();
104
189
  }
105
190
  if (type === 'presentation_complete') {
106
191
  this.logInfo("Received 'presentation_complete' from window '" + windowId + "'");
107
- this.didReceiveComplete = true;
108
- if (this.completeCallback) {
109
- this.completeCallback();
110
- }
192
+ this.handleComplete();
111
193
  }
112
194
  if (type === 'enable_interrupt_lock') {
113
195
  this.logInfo("Received 'enable_interrupt_lock' from window '" + windowId + "'");
@@ -168,12 +250,13 @@ var PresentationWindow = /** @class */ (function () {
168
250
  this.ruleEngineState = options.ruleEngineState;
169
251
  this.theme = options.theme;
170
252
  this.forceMute = options.forceMute || false;
253
+ this.workerState = options.workerState;
171
254
  }
172
255
  PresentationWindow.prototype.load = function () {
173
256
  if (this.isEmbeddedApp()) {
174
257
  return this.loadEmbeddedApp();
175
258
  }
176
- return this.loadRaydiantApp();
259
+ return this.loadLegacyApp();
177
260
  };
178
261
  PresentationWindow.prototype.show = function (isFocused) {
179
262
  var _this = this;
@@ -225,6 +308,8 @@ var PresentationWindow = /** @class */ (function () {
225
308
  this.errorCallback = null;
226
309
  this.enableInterruptLockCallback = null;
227
310
  this.disableInterruptLockCallback = null;
311
+ // Clean up subscriptions
312
+ this.workerSubscriptions.clear();
228
313
  // Clear timers
229
314
  if (this.readyTimeout) {
230
315
  clearTimeout(this.readyTimeout);
@@ -279,13 +364,17 @@ var PresentationWindow = /** @class */ (function () {
279
364
  this.sendMessage('play');
280
365
  // Play any child window managers.
281
366
  Object.values(this.windowManagers).forEach(function (wm) {
282
- // We only want trigger play on child windows the first play of the main window or if
367
+ // We only want to trigger play on child windows the first play of the main window or if
283
368
  // the child window is paused.
284
369
  if (!wm.isPlaying() || wm.isPaused()) {
285
370
  wm.play();
286
371
  }
287
372
  });
288
373
  }
374
+ else if (this.runtimeSubscriptions.has('play')) {
375
+ // If the embedded app has subscribed to the play event, send it a play event.
376
+ this.sendMessage('play');
377
+ }
289
378
  this.lastPlayedAt = new Date();
290
379
  };
291
380
  PresentationWindow.prototype.pause = function () {
@@ -312,6 +401,9 @@ var PresentationWindow = /** @class */ (function () {
312
401
  this.disableInterruptLockCallback = callback;
313
402
  };
314
403
  PresentationWindow.prototype.isReady = function () {
404
+ // TODO: To support the 'ready' event in embedded apps, they will need to tell the
405
+ // runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
406
+ // for ready" option in the Developer Portal.
315
407
  if (this.isEmbeddedApp()) {
316
408
  return this.didEmbeddedAppLoad;
317
409
  }
@@ -379,7 +471,7 @@ var PresentationWindow = /** @class */ (function () {
379
471
  PresentationWindow.prototype.isEmbeddedApp = function () {
380
472
  var _a = this.options, applicationVersion = _a.applicationVersion, isPreview = _a.isPreview;
381
473
  // If the application has a source url, always use it for rendering the preview.
382
- // This fixes an issue with Hoopla which is an embedded app but uses a Raydiant app
474
+ // This fixes an issue with Hoopla which is an embedded app but uses a legacy app
383
475
  // for it's preview.
384
476
  if (isPreview && applicationVersion.sourceUrl) {
385
477
  return false;
@@ -408,6 +500,13 @@ var PresentationWindow = /** @class */ (function () {
408
500
  }
409
501
  }
410
502
  };
503
+ PresentationWindow.prototype.appendWindowIdToHash = function (urlStr, windowId) {
504
+ var url = new URL(urlStr);
505
+ var hashParams = new URLSearchParams(url.hash.slice(1));
506
+ hashParams.set('__raydiantWindowId', windowId);
507
+ url.hash = hashParams.toString();
508
+ return url.toString();
509
+ };
411
510
  PresentationWindow.prototype.loadEmbeddedApp = function () {
412
511
  return __awaiter(this, void 0, void 0, function () {
413
512
  var _a, windowId, applicationVersion, url;
@@ -420,16 +519,11 @@ var PresentationWindow = /** @class */ (function () {
420
519
  }
421
520
  url = embeddedApps_1.default(this.presentation, applicationVersion);
422
521
  this.logInfo("Create window '" + windowId + "' for embedded app url '" + url + "'");
423
- // Embedded apps should not have a backgroundColor bexcause some sites expect
424
- // the browser default of white will be applied. We also don't attach the message
425
- // listeners for embedded apps. We could eventually allow embedded apps to send
426
- // lifecycle events but we should consider namespacing our message events.
427
- return [4 /*yield*/, this.createAndLoadWindow(url, '#ffffff')];
522
+ return [4 /*yield*/, this.createAndLoadWindow(this.appendWindowIdToHash(url, windowId),
523
+ // Embedded apps should not have a backgroundColor because some sites expect
524
+ // the browser default of white will be applied.
525
+ '#ffffff', this.handleEmbeddedMessage)];
428
526
  case 1:
429
- // Embedded apps should not have a backgroundColor bexcause some sites expect
430
- // the browser default of white will be applied. We also don't attach the message
431
- // listeners for embedded apps. We could eventually allow embedded apps to send
432
- // lifecycle events but we should consider namespacing our message events.
433
527
  _b.sent();
434
528
  this.didEmbeddedAppLoad = true;
435
529
  this.checkIsReadyCallback();
@@ -438,17 +532,16 @@ var PresentationWindow = /** @class */ (function () {
438
532
  });
439
533
  });
440
534
  };
441
- PresentationWindow.prototype.loadRaydiantApp = function () {
535
+ PresentationWindow.prototype.loadLegacyApp = function () {
442
536
  return __awaiter(this, void 0, void 0, function () {
443
- var _a, windowId, _b, appRendererUrl, backgroundColor, url;
537
+ var _a, windowId, _b, appRendererUrl, backgroundColor;
444
538
  return __generator(this, function (_c) {
445
539
  switch (_c.label) {
446
540
  case 0:
447
541
  _a = this.options, windowId = _a.windowId, _b = _a.appRendererUrl, appRendererUrl = _b === void 0 ? defaultAppRendererUrl : _b, backgroundColor = _a.backgroundColor;
448
- url = appRendererUrl + "#windowId=" + windowId;
449
542
  this.logInfo("Create window '" + windowId + "'");
450
- return [4 /*yield*/, this.createAndLoadWindow(url, backgroundColor || '#000000', // Default to black bg for Raydiant apps
451
- this.handleMessage)];
543
+ return [4 /*yield*/, this.createAndLoadWindow(this.appendWindowIdToHash(appRendererUrl, windowId), backgroundColor || '#000000', // Default to black bg for Raydiant apps
544
+ this.handleLegacyMessage)];
452
545
  case 1:
453
546
  _c.sent();
454
547
  return [4 /*yield*/, this.waitForPesentationReady()];
@@ -734,6 +827,16 @@ var PresentationWindow = /** @class */ (function () {
734
827
  this.disableInterruptLockCallback();
735
828
  }
736
829
  };
830
+ PresentationWindow.prototype.handleReady = function () {
831
+ this.didReceiveReady = true;
832
+ this.checkIsReadyCallback();
833
+ };
834
+ PresentationWindow.prototype.handleComplete = function () {
835
+ this.didReceiveComplete = true;
836
+ if (this.completeCallback) {
837
+ this.completeCallback();
838
+ }
839
+ };
737
840
  PresentationWindow.prototype.logDebug = function (message) {
738
841
  this.options.logger.debug(message, this.getLogContext());
739
842
  };
@@ -766,6 +869,24 @@ var PresentationWindow = /** @class */ (function () {
766
869
  PresentationWindow.prototype.metadata = function () {
767
870
  return this.getLogContext();
768
871
  };
872
+ PresentationWindow.prototype.runWorkerSubscriptions = function (state) {
873
+ // For each worker subscription, compare the previous state to the new state.
874
+ // If the state has changed, send the new state to the subscription.
875
+ for (var _i = 0, _a = Array.from(this.workerSubscriptions); _i < _a.length; _i++) {
876
+ var workerName = _a[_i];
877
+ var workerState = state[workerName];
878
+ var previousWorkerState = this.workerState[workerName];
879
+ if (workerState !== undefined &&
880
+ !fast_deep_equal_1.default(workerState, previousWorkerState)) {
881
+ this.sendMessage('worker-state', {
882
+ name: workerName,
883
+ state: workerState
884
+ });
885
+ }
886
+ }
887
+ // Persist the new state so we can compare if the previous values have changed..
888
+ this.workerState = state;
889
+ };
769
890
  PresentationWindow.prototype.createRuleEngine = function (playlistId) {
770
891
  var globalRule = this.options.globalRule;
771
892
  return new RE.RuleEngine(playlistId, this.ruleEngineState, globalRule
package/build/preload.js CHANGED
@@ -1,20 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- var electron_1 = require("electron"); // eslint-disable-line
4
- var sendMessage = function (windowId, type, payload) {
5
- electron_1.ipcRenderer.send.apply(electron_1.ipcRenderer, [windowId, type].concat(payload));
3
+ var electron_1 = require("electron");
4
+ var internals = {
5
+ sendMessage: function (windowId, type, payload) {
6
+ electron_1.ipcRenderer.send(windowId, type, payload);
7
+ }
6
8
  };
7
- var messageHandler = function (_, message) {
8
- var receiveMessage = window.receiveMessage;
9
- if (typeof receiveMessage === 'function') {
10
- receiveMessage(message);
9
+ // Set __RAYDIANT_INTERNALS on window so it is accessible in to the running application.
10
+ window.__RAYDIANT_INTERNALS = internals;
11
+ // Forward messages from the main process to the app (render process) via receiveMessage.
12
+ // __RAYDIANT_INTERNALS.receiveMessage is required to be set by the app with the Raydiant SDK.
13
+ electron_1.ipcRenderer.on('electron-main', function (_, message) {
14
+ if (typeof internals.receiveMessage === 'function') {
15
+ internals.receiveMessage(message);
11
16
  }
12
17
  else {
13
- throw new Error('receiveMessage is not defined');
18
+ console.error('Failed to handle message from main process, receiveMessage is not defined');
14
19
  }
15
- };
16
- // Add sendMessage and messageHandler to window so that app renderer knows we're
17
- // running in an Electron environment.
18
- window.sendMessage = sendMessage;
19
- window.messageHandler = messageHandler;
20
- electron_1.ipcRenderer.on('electron-main', messageHandler);
20
+ });
package/build/types.d.ts CHANGED
@@ -71,6 +71,9 @@ export interface WindowBounds {
71
71
  width: number;
72
72
  height: number;
73
73
  }
74
+ export interface WorkerState {
75
+ [key: string]: any;
76
+ }
74
77
  export interface CreateWindowOptions {
75
78
  windowId: string;
76
79
  parentWindowId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raydiant/playlist-window-manager",
3
- "version": "0.15.2",
3
+ "version": "0.17.0",
4
4
  "description": "Window manager for playlist playback.",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -6,7 +6,8 @@ import {
6
6
  Device,
7
7
  WindowBounds,
8
8
  Theme,
9
- Logger
9
+ Logger,
10
+ WorkerState
10
11
  } from './types';
11
12
  import PresentationWindow from './PresentationWindow';
12
13
  import PlaylistWindowManagerState from './PlaylistWindowManagerState';
@@ -99,6 +100,8 @@ export default class PlaylistWindowManager extends EventEmitter {
99
100
  private forceMute: boolean = false;
100
101
  private isFocused: boolean = false;
101
102
 
103
+ private workerState: WorkerState = {};
104
+
102
105
  constructor(private options: PlaylistWindowManagerOptions) {
103
106
  super();
104
107
 
@@ -421,6 +424,21 @@ export default class PlaylistWindowManager extends EventEmitter {
421
424
  return this.currentWindow.isLocked();
422
425
  }
423
426
 
427
+ public runWorkerSubscriptions(state: WorkerState) {
428
+ // Persist worker state so new windows can be initialized with it.
429
+ this.workerState = state;
430
+
431
+ // Run worker subcriptions on the current window.
432
+ if (this.currentWindow) {
433
+ this.currentWindow.runWorkerSubscriptions(state);
434
+ }
435
+
436
+ // Run worker subscriptions on the next window.
437
+ if (this.nextWindow) {
438
+ this.nextWindow.runWorkerSubscriptions(state);
439
+ }
440
+ }
441
+
424
442
  private createWindow(pr: RE.NextPresentationID): PresentationWindow {
425
443
  const {
426
444
  readyTimeoutDurationSeconds,
@@ -483,7 +501,8 @@ export default class PlaylistWindowManager extends EventEmitter {
483
501
  playlistWindowManagerState: this.state,
484
502
  ruleEngineState: this.ruleEngine.state,
485
503
  globalRule: this.ruleEngine.globalRule,
486
- isPreview
504
+ isPreview,
505
+ workerState: this.workerState
487
506
  });
488
507
  }
489
508
 
@@ -14,7 +14,8 @@ import {
14
14
  WindowBounds,
15
15
  Logger,
16
16
  OpenChildWindowOptions,
17
- BuilderState
17
+ BuilderState,
18
+ WorkerState
18
19
  } from './types';
19
20
 
20
21
  export interface PresentationWindowOptions {
@@ -42,6 +43,7 @@ export interface PresentationWindowOptions {
42
43
  backgroundColor?: string;
43
44
  isPreview?: boolean;
44
45
  forceMute?: boolean;
46
+ workerState: WorkerState;
45
47
  onPresentationProperties?: (
46
48
  properties: any[],
47
49
  string: { [key: string]: string }
@@ -49,7 +51,7 @@ export interface PresentationWindowOptions {
49
51
  }
50
52
 
51
53
  // TODO: Automatically inject latest version when building in CI.
52
- const defaultAppRendererUrl = 'https://apps.raydiant.com/1.4.7/index.html';
54
+ const defaultAppRendererUrl = 'https://apps.raydiant.com/2.0.0/index.html';
53
55
 
54
56
  const isValidNumber = (value: any) =>
55
57
  typeof value === 'number' && !isNaN(value);
@@ -82,6 +84,10 @@ export default class PresentationWindow {
82
84
  private forceMute: boolean = false;
83
85
  private error: Error | null = null;
84
86
 
87
+ private workerState: WorkerState = {};
88
+ private workerSubscriptions = new Set<string>();
89
+ private runtimeSubscriptions = new Set<string>();
90
+
85
91
  constructor(private options: PresentationWindowOptions) {
86
92
  this.builderState = options.builderState;
87
93
  this.presentation = options.presentation;
@@ -89,13 +95,14 @@ export default class PresentationWindow {
89
95
  this.ruleEngineState = options.ruleEngineState;
90
96
  this.theme = options.theme;
91
97
  this.forceMute = options.forceMute || false;
98
+ this.workerState = options.workerState;
92
99
  }
93
100
 
94
101
  public load(): Promise<void> {
95
102
  if (this.isEmbeddedApp()) {
96
103
  return this.loadEmbeddedApp();
97
104
  }
98
- return this.loadRaydiantApp();
105
+ return this.loadLegacyApp();
99
106
  }
100
107
 
101
108
  public show(isFocused: boolean = true): void {
@@ -154,6 +161,9 @@ export default class PresentationWindow {
154
161
  this.enableInterruptLockCallback = null;
155
162
  this.disableInterruptLockCallback = null;
156
163
 
164
+ // Clean up subscriptions
165
+ this.workerSubscriptions.clear();
166
+
157
167
  // Clear timers
158
168
  if (this.readyTimeout) {
159
169
  clearTimeout(this.readyTimeout);
@@ -209,12 +219,15 @@ export default class PresentationWindow {
209
219
 
210
220
  // Play any child window managers.
211
221
  Object.values(this.windowManagers).forEach(wm => {
212
- // We only want trigger play on child windows the first play of the main window or if
222
+ // We only want to trigger play on child windows the first play of the main window or if
213
223
  // the child window is paused.
214
224
  if (!wm.isPlaying() || wm.isPaused()) {
215
225
  wm.play();
216
226
  }
217
227
  });
228
+ } else if (this.runtimeSubscriptions.has('play')) {
229
+ // If the embedded app has subscribed to the play event, send it a play event.
230
+ this.sendMessage('play');
218
231
  }
219
232
 
220
233
  this.lastPlayedAt = new Date();
@@ -251,6 +264,9 @@ export default class PresentationWindow {
251
264
  }
252
265
 
253
266
  public isReady() {
267
+ // TODO: To support the 'ready' event in embedded apps, they will need to tell the
268
+ // runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
269
+ // for ready" option in the Developer Portal.
254
270
  if (this.isEmbeddedApp()) {
255
271
  return this.didEmbeddedAppLoad;
256
272
  }
@@ -329,7 +345,7 @@ export default class PresentationWindow {
329
345
  const { applicationVersion, isPreview } = this.options;
330
346
 
331
347
  // If the application has a source url, always use it for rendering the preview.
332
- // This fixes an issue with Hoopla which is an embedded app but uses a Raydiant app
348
+ // This fixes an issue with Hoopla which is an embedded app but uses a legacy app
333
349
  // for it's preview.
334
350
  if (isPreview && applicationVersion.sourceUrl) {
335
351
  return false;
@@ -365,6 +381,15 @@ export default class PresentationWindow {
365
381
  }
366
382
  }
367
383
 
384
+ private appendWindowIdToHash(urlStr: string, windowId: string) {
385
+ const url = new URL(urlStr);
386
+ const hashParams = new URLSearchParams(url.hash.slice(1));
387
+ hashParams.set('__raydiantWindowId', windowId);
388
+ url.hash = hashParams.toString();
389
+
390
+ return url.toString();
391
+ }
392
+
368
393
  private async loadEmbeddedApp(): Promise<void> {
369
394
  const { windowId, applicationVersion } = this.options;
370
395
 
@@ -375,34 +400,35 @@ export default class PresentationWindow {
375
400
  }
376
401
 
377
402
  const url = createEmbeddedApp(this.presentation, applicationVersion);
378
-
379
403
  this.logInfo(`Create window '${windowId}' for embedded app url '${url}'`);
380
404
 
381
- // Embedded apps should not have a backgroundColor bexcause some sites expect
382
- // the browser default of white will be applied. We also don't attach the message
383
- // listeners for embedded apps. We could eventually allow embedded apps to send
384
- // lifecycle events but we should consider namespacing our message events.
385
- await this.createAndLoadWindow(url, '#ffffff');
405
+ await this.createAndLoadWindow(
406
+ this.appendWindowIdToHash(url, windowId),
407
+ // Embedded apps should not have a backgroundColor because some sites expect
408
+ // the browser default of white will be applied.
409
+ '#ffffff',
410
+ this.handleEmbeddedMessage
411
+ );
386
412
  this.didEmbeddedAppLoad = true;
387
413
 
388
414
  this.checkIsReadyCallback();
389
415
  }
390
416
 
391
- private async loadRaydiantApp(): Promise<void> {
417
+ private async loadLegacyApp(): Promise<void> {
392
418
  const {
393
419
  windowId,
394
420
  appRendererUrl = defaultAppRendererUrl,
395
421
  backgroundColor
396
422
  } = this.options;
397
423
 
398
- let url = `${appRendererUrl}#windowId=${windowId}`;
399
-
400
424
  this.logInfo(`Create window '${windowId}'`);
425
+
401
426
  await this.createAndLoadWindow(
402
- url,
427
+ this.appendWindowIdToHash(appRendererUrl, windowId),
403
428
  backgroundColor || '#000000', // Default to black bg for Raydiant apps
404
- this.handleMessage
429
+ this.handleLegacyMessage
405
430
  );
431
+
406
432
  await this.waitForPesentationReady();
407
433
  }
408
434
 
@@ -709,7 +735,111 @@ export default class PresentationWindow {
709
735
  }
710
736
  }
711
737
 
712
- private handleMessage = async (type: string, payload?: any) => {
738
+ private handleEmbeddedMessage = async (type: string, payload?: any) => {
739
+ const { windowId } = this.options;
740
+
741
+ switch (type) {
742
+ case 'ready': {
743
+ this.logInfo(`Received 'ready' from window '${windowId}'`);
744
+ this.handleReady();
745
+ break;
746
+ }
747
+
748
+ case 'complete': {
749
+ if (payload === undefined) {
750
+ this.logInfo(`Received 'complete' from window '${windowId}'`);
751
+ this.handleComplete();
752
+ } else {
753
+ this.logInfo(
754
+ `Received 'complete' with error from window '${windowId}, error=${payload}'`
755
+ );
756
+ this.handleError(payload);
757
+ }
758
+ break;
759
+ }
760
+
761
+ case 'runtime-subscribe': {
762
+ if (typeof payload === 'string') {
763
+ this.logInfo(
764
+ `Received 'runtime-subscribe' from window '${windowId}'`
765
+ );
766
+ // The payload is the event name (ex. 'play').
767
+ this.runtimeSubscriptions.add(payload);
768
+
769
+ // Immediately send play if the app if already playing.
770
+ if (this.isPlaying()) {
771
+ this.sendMessage('play');
772
+ }
773
+ } else {
774
+ this.logWarning(
775
+ `Invalid payload for runtime-subscribe message: ${payload}`
776
+ );
777
+ }
778
+ break;
779
+ }
780
+
781
+ case 'runtime-unsubscribe': {
782
+ if (typeof payload === 'string') {
783
+ this.logInfo(
784
+ `Received 'runtime-unsubscribe' from window '${windowId}'`
785
+ );
786
+ // The payload is the event name (ex. 'play').
787
+ this.runtimeSubscriptions.delete(payload);
788
+ } else {
789
+ this.logWarning(
790
+ `Invalid payload for runtime-subscribe message: ${payload}`
791
+ );
792
+ }
793
+ break;
794
+ }
795
+
796
+ case 'worker-subscribe': {
797
+ if (typeof payload === 'string') {
798
+ this.logInfo(`Received 'worker-subscribe' from window '${windowId}'`);
799
+
800
+ // The payload is the worker name.
801
+ this.workerSubscriptions.add(payload);
802
+
803
+ // Send the current state to the worker if it exists as soon
804
+ // as the app subscribes.
805
+ const workerState = this.workerState[payload];
806
+ if (workerState !== undefined) {
807
+ this.sendMessage('worker-state', {
808
+ name: payload,
809
+ state: workerState
810
+ });
811
+ }
812
+ } else {
813
+ this.logWarning(
814
+ `Invalid payload for worker-subscribe message: ${payload}`
815
+ );
816
+ }
817
+ break;
818
+ }
819
+
820
+ case 'worker-unsubscribe': {
821
+ if (typeof payload === 'string') {
822
+ this.logInfo(
823
+ `Received 'worker-unsubscribe' from window '${windowId}'`
824
+ );
825
+
826
+ // The payload is the worker name.
827
+ this.workerSubscriptions.delete(payload);
828
+ } else {
829
+ this.logWarning(
830
+ `Invalid payload for worker-unsubscribe message: ${payload}`
831
+ );
832
+ }
833
+ break;
834
+ }
835
+
836
+ default: {
837
+ this.logWarning(`Received unknown embedded message type: ${type}`);
838
+ }
839
+ }
840
+ };
841
+
842
+ private handleLegacyMessage = async (type: string, payload?: any) => {
713
843
  const { windowId, onPresentationProperties } = this.options;
714
844
 
715
845
  if (type === 'loaded') {
@@ -719,18 +849,14 @@ export default class PresentationWindow {
719
849
 
720
850
  if (type === 'presentation_ready') {
721
851
  this.logInfo(`Received 'presentation_ready' from window '${windowId}'`);
722
- this.didReceiveReady = true;
723
- this.checkIsReadyCallback();
852
+ this.handleReady();
724
853
  }
725
854
 
726
855
  if (type === 'presentation_complete') {
727
856
  this.logInfo(
728
857
  `Received 'presentation_complete' from window '${windowId}'`
729
858
  );
730
- this.didReceiveComplete = true;
731
- if (this.completeCallback) {
732
- this.completeCallback();
733
- }
859
+ this.handleComplete();
734
860
  }
735
861
 
736
862
  if (type === 'enable_interrupt_lock') {
@@ -814,6 +940,18 @@ export default class PresentationWindow {
814
940
  }
815
941
  };
816
942
 
943
+ private handleReady() {
944
+ this.didReceiveReady = true;
945
+ this.checkIsReadyCallback();
946
+ }
947
+
948
+ private handleComplete() {
949
+ this.didReceiveComplete = true;
950
+ if (this.completeCallback) {
951
+ this.completeCallback();
952
+ }
953
+ }
954
+
817
955
  private handleError = (error: any) => {
818
956
  this.error = error;
819
957
 
@@ -874,6 +1012,28 @@ export default class PresentationWindow {
874
1012
  return this.getLogContext();
875
1013
  }
876
1014
 
1015
+ public runWorkerSubscriptions(state: WorkerState) {
1016
+ // For each worker subscription, compare the previous state to the new state.
1017
+ // If the state has changed, send the new state to the subscription.
1018
+ for (const workerName of Array.from(this.workerSubscriptions)) {
1019
+ const workerState = state[workerName];
1020
+ const previousWorkerState = this.workerState[workerName];
1021
+
1022
+ if (
1023
+ workerState !== undefined &&
1024
+ !deepEqual(workerState, previousWorkerState)
1025
+ ) {
1026
+ this.sendMessage('worker-state', {
1027
+ name: workerName,
1028
+ state: workerState
1029
+ });
1030
+ }
1031
+ }
1032
+
1033
+ // Persist the new state so we can compare if the previous values have changed..
1034
+ this.workerState = state;
1035
+ }
1036
+
877
1037
  private createRuleEngine(playlistId: string) {
878
1038
  const { globalRule } = this.options;
879
1039
  return new RE.RuleEngine(
package/src/preload.ts CHANGED
@@ -1,21 +1,27 @@
1
- import { ipcRenderer, IpcRendererEvent } from 'electron'; // eslint-disable-line
1
+ import { ipcRenderer, IpcRendererEvent } from 'electron';
2
2
 
3
- const sendMessage = (windowId: string, type: string, payload?: any) => {
4
- ipcRenderer.send(windowId, type, ...payload);
5
- };
3
+ interface RaydiantInternals {
4
+ sendMessage: (windowId: string, type: string, payload?: any) => void;
5
+ receiveMessage?: (message: string) => void;
6
+ }
6
7
 
7
- const messageHandler = (_: IpcRendererEvent, message: string) => {
8
- const receiveMessage = (window as any).receiveMessage;
9
- if (typeof receiveMessage === 'function') {
10
- receiveMessage(message);
11
- } else {
12
- throw new Error('receiveMessage is not defined');
8
+ const internals: RaydiantInternals = {
9
+ sendMessage: (windowId: string, type: string, payload?: any) => {
10
+ ipcRenderer.send(windowId, type, payload);
13
11
  }
14
12
  };
15
13
 
16
- // Add sendMessage and messageHandler to window so that app renderer knows we're
17
- // running in an Electron environment.
18
- (window as any).sendMessage = sendMessage;
19
- (window as any).messageHandler = messageHandler;
14
+ // Set __RAYDIANT_INTERNALS on window so it is accessible in to the running application.
15
+ (window as any).__RAYDIANT_INTERNALS = internals;
20
16
 
21
- ipcRenderer.on('electron-main', messageHandler);
17
+ // Forward messages from the main process to the app (render process) via receiveMessage.
18
+ // __RAYDIANT_INTERNALS.receiveMessage is required to be set by the app with the Raydiant SDK.
19
+ ipcRenderer.on('electron-main', (_: IpcRendererEvent, message: string) => {
20
+ if (typeof internals.receiveMessage === 'function') {
21
+ internals.receiveMessage(message);
22
+ } else {
23
+ console.error(
24
+ 'Failed to handle message from main process, receiveMessage is not defined'
25
+ );
26
+ }
27
+ });
package/src/types.ts CHANGED
@@ -70,6 +70,10 @@ export interface WindowBounds {
70
70
  height: number;
71
71
  }
72
72
 
73
+ export interface WorkerState {
74
+ [key: string]: any;
75
+ }
76
+
73
77
  export interface CreateWindowOptions {
74
78
  windowId: string;
75
79
  parentWindowId?: string;