@raydiant/playlist-window-manager 0.29.1 → 0.30.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.
@@ -80,8 +80,47 @@ var embeddedApps_1 = __importDefault(require("./embeddedApps"));
80
80
  var PlaylistWindowManager_1 = __importDefault(require("./PlaylistWindowManager"));
81
81
  var WindowTimeoutError_1 = __importDefault(require("./WindowTimeoutError"));
82
82
  var WindowLoadError_1 = __importDefault(require("./WindowLoadError"));
83
+ var SurfaceBroker_1 = require("./SurfaceBroker");
83
84
  // TODO: Automatically inject latest version when building in CI.
84
- var defaultAppRendererUrl = 'https://apps.raydiant.com/3.1.0/index.html';
85
+ var defaultAppRendererUrl = 'https://apps.raydiant.com/3.1.1/index.html';
86
+ // The urls a window may open a surface for besides its content url: the
87
+ // Videos app probes for these renditions beside the original.
88
+ var RENDITION_SUFFIXES = ['_4K', '_720p', '_480p'];
89
+ var isValidRect = function (value) {
90
+ return value &&
91
+ typeof value === 'object' &&
92
+ isFiniteNumber(value.x) &&
93
+ isFiniteNumber(value.y) &&
94
+ isFiniteNumber(value.width) &&
95
+ isFiniteNumber(value.height);
96
+ };
97
+ var isFiniteNumber = function (value) {
98
+ return typeof value === 'number' && isFinite(value);
99
+ };
100
+ var roundRect = function (rect) { return ({
101
+ x: Math.round(rect.x),
102
+ y: Math.round(rect.y),
103
+ width: Math.round(rect.width),
104
+ height: Math.round(rect.height),
105
+ }); };
106
+ // Every string held under a `url` key, at any depth.
107
+ var collectUrls = function (value, urls) {
108
+ if (urls === void 0) { urls = []; }
109
+ if (Array.isArray(value)) {
110
+ value.forEach(function (item) { return collectUrls(item, urls); });
111
+ }
112
+ else if (value && typeof value === 'object') {
113
+ Object.keys(value).forEach(function (key) {
114
+ if (key === 'url' && typeof value[key] === 'string') {
115
+ urls.push(value[key]);
116
+ }
117
+ else {
118
+ collectUrls(value[key], urls);
119
+ }
120
+ });
121
+ }
122
+ return urls;
123
+ };
85
124
  var isValidNumber = function (value) {
86
125
  return typeof value === 'number' && !isNaN(value);
87
126
  };
@@ -140,6 +179,8 @@ var PresentationWindow = /** @class */ (function () {
140
179
  this.disableInterruptLockCallback = null;
141
180
  this.analyticsEventCallback = null;
142
181
  this.chargeRequestCallback = null;
182
+ this.didPaintSinceLastPlay = false;
183
+ this.firstPaintResolvers = [];
143
184
  this.forceMute = false;
144
185
  this.error = null;
145
186
  this.workerState = {};
@@ -152,6 +193,17 @@ var PresentationWindow = /** @class */ (function () {
152
193
  this.createObjectURLTimeouts = {};
153
194
  this.interruptLocks = {};
154
195
  this.previouslyLocked = false;
196
+ this.surface = null;
197
+ // Once a window has been shown without a ready surface, or has given one
198
+ // up, it is offered none from then on and its app keeps <video>.
199
+ this.surfaceWithdrawn = false;
200
+ this.surfaceShow = null;
201
+ this.wakeSurfaceShow = [];
202
+ this.holeOpenWaiter = null;
203
+ this.playingWaiter = null;
204
+ this.isPlayHeld = false;
205
+ this.isRemoved = false;
206
+ this.frameBackground = '#000000';
155
207
  this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
156
208
  var windowId, workerState, lock, lock, lock;
157
209
  return __generator(this, function (_a) {
@@ -359,6 +411,11 @@ var PresentationWindow = /** @class */ (function () {
359
411
  }
360
412
  break;
361
413
  }
414
+ case 'open_video': {
415
+ // An embedded app is never offered a video surface.
416
+ this.refuseVideo('not_offered');
417
+ break;
418
+ }
362
419
  default: {
363
420
  this.logWarning("Received unknown embedded message type: ".concat(type));
364
421
  }
@@ -367,7 +424,7 @@ var PresentationWindow = /** @class */ (function () {
367
424
  });
368
425
  }); };
369
426
  this.handleLegacyMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
370
- var _a, windowId, onPresentationProperties;
427
+ var _a, windowId, onPresentationProperties, surface;
371
428
  return __generator(this, function (_b) {
372
429
  _a = this.options, windowId = _a.windowId, onPresentationProperties = _a.onPresentationProperties;
373
430
  if (type === 'loaded') {
@@ -382,6 +439,11 @@ var PresentationWindow = /** @class */ (function () {
382
439
  this.logInfo("Received 'presentation_complete' from window '".concat(windowId, "'"));
383
440
  this.handleComplete();
384
441
  }
442
+ if (type === 'first_painted') {
443
+ this.logDebug("Received 'first_painted' from window '".concat(windowId, "'"));
444
+ this.didPaintSinceLastPlay = true;
445
+ this.resolveFirstPaintWaiters();
446
+ }
385
447
  if (type === 'enable_interrupt_lock') {
386
448
  this.logInfo("Received 'enable_interrupt_lock' from window '".concat(windowId, "'"));
387
449
  this.enableInteruptLock();
@@ -404,6 +466,33 @@ var PresentationWindow = /** @class */ (function () {
404
466
  this.logWarning("Invalid payload received for 'open_child_window', ignoring");
405
467
  }
406
468
  }
469
+ if (type === 'open_video') {
470
+ this.handleOpenVideo(payload);
471
+ }
472
+ if (type === 'video_bounds') {
473
+ this.handleVideoBounds(payload);
474
+ }
475
+ if (type === 'video_hole_open') {
476
+ this.logInfo("Received 'video_hole_open' from window '".concat(windowId, "'"));
477
+ surface = this.surface;
478
+ if (this.holeOpenWaiter) {
479
+ this.holeOpenWaiter();
480
+ }
481
+ else if (surface &&
482
+ surface.firstFrame &&
483
+ !surface.holeOpen &&
484
+ !this.surfaceShow) {
485
+ // Opened before the window is shown, so a swap may play and reveal
486
+ // it before showing its surface.
487
+ surface.isHolePreopened = true;
488
+ }
489
+ }
490
+ if (type === 'close_video') {
491
+ this.logInfo("Received 'close_video' from window '".concat(windowId, "'"));
492
+ if (this.surface) {
493
+ this.dropSurface(this.surface);
494
+ }
495
+ }
407
496
  if (type === 'close_child_window') {
408
497
  // TODO: Validate close_child_window message payload.
409
498
  this.logInfo("Received 'close_child_window', windowId=".concat(payload.windowId));
@@ -449,9 +538,40 @@ var PresentationWindow = /** @class */ (function () {
449
538
  }
450
539
  return this.loadLegacyApp();
451
540
  };
452
- PresentationWindow.prototype.show = function (isFocused) {
541
+ // Shows the window. A window holding a surface with its first frame and
542
+ // bounds shows the surface first and reveals its frame only once the app
543
+ // has opened its hole; onReveal then runs in the reveal's task. Given
544
+ // showAfterRevealMs by a swap, a surface whose app opened its hole while
545
+ // hidden is played, then revealed, then shown instead. The promise
546
+ // resolves once the frame is revealed and any such show has settled, or
547
+ // the window is removed.
548
+ PresentationWindow.prototype.show = function (isFocused, onReveal, showAfterRevealMs) {
453
549
  var _this = this;
454
550
  if (isFocused === void 0) { isFocused = true; }
551
+ if (this.surfaceShow) {
552
+ return this.surfaceShow;
553
+ }
554
+ var surface = this.surface;
555
+ if (surface && surface.holeOpen) {
556
+ this.showFrame(isFocused);
557
+ return Promise.resolve();
558
+ }
559
+ if (surface && this.isSurfaceReady(surface)) {
560
+ var showing = showAfterRevealMs !== undefined && surface.isHolePreopened
561
+ ? this.showSurfaceAfterReveal(surface, isFocused, showAfterRevealMs, onReveal)
562
+ : this.showSurface(surface, isFocused, onReveal);
563
+ this.surfaceShow = showing.then(function () {
564
+ _this.surfaceShow = null;
565
+ });
566
+ return this.surfaceShow;
567
+ }
568
+ // Shown before its surface was ready: the app keeps <video>.
569
+ this.withdrawSurface();
570
+ this.showFrame(isFocused);
571
+ return Promise.resolve();
572
+ };
573
+ PresentationWindow.prototype.showFrame = function (isFocused) {
574
+ var _this = this;
455
575
  var _a = this.options, windowId = _a.windowId, renderer = _a.renderer;
456
576
  // Show the main window and focus it before any child windows. Showing a child
457
577
  // window that requires focus will move focus from the main window to the child.
@@ -488,7 +608,7 @@ var PresentationWindow = /** @class */ (function () {
488
608
  };
489
609
  PresentationWindow.prototype.remove = function () {
490
610
  return __awaiter(this, void 0, void 0, function () {
491
- var _a, windowId, renderer, err_1;
611
+ var _a, windowId, renderer, surface, surfaceBroker, err_1;
492
612
  return __generator(this, function (_b) {
493
613
  switch (_b.label) {
494
614
  case 0:
@@ -510,23 +630,43 @@ var PresentationWindow = /** @class */ (function () {
510
630
  clearTimeout(this.configurableDurationTimeout);
511
631
  this.configurableDurationTimeout = null;
512
632
  }
633
+ // Release anything waiting on a paint signal so callers never hang on a
634
+ // window that is going away.
635
+ this.resolveFirstPaintWaiters();
636
+ // Release a surface show in progress; a removed window is never revealed.
637
+ this.isRemoved = true;
638
+ this.isPlayHeld = false;
639
+ this.wakeSurfaceShowWaiters();
513
640
  // Remove any child window managers.
514
641
  Object.values(this.windowManagers).forEach(function (wm) {
515
642
  wm.destroy();
516
643
  });
517
- _b.label = 1;
644
+ surface = this.surface;
645
+ surfaceBroker = this.options.surfaceBroker;
646
+ if (!(surface && surfaceBroker)) return [3 /*break*/, 2];
647
+ this.surface = null;
648
+ if (surface.handle) {
649
+ this.sendVideoMessage('video_closing');
650
+ }
651
+ return [4 /*yield*/, surfaceBroker.close(windowId)];
518
652
  case 1:
519
- _b.trys.push([1, 3, , 4]);
520
- return [4 /*yield*/, renderer.removeWindow(windowId)];
653
+ _b.sent();
654
+ if (surface.holeOpen) {
655
+ surfaceBroker.onHole(false);
656
+ }
657
+ _b.label = 2;
521
658
  case 2:
659
+ _b.trys.push([2, 4, , 5]);
660
+ return [4 /*yield*/, renderer.removeWindow(windowId)];
661
+ case 3:
522
662
  _b.sent();
523
663
  this.logInfo("Removed window '".concat(windowId, "'"));
524
- return [3 /*break*/, 4];
525
- case 3:
664
+ return [3 /*break*/, 5];
665
+ case 4:
526
666
  err_1 = _b.sent();
527
667
  this.logError("Failed to remove window '".concat(windowId, "' with '").concat(err_1.message, "'"));
528
- return [3 /*break*/, 4];
529
- case 4: return [2 /*return*/];
668
+ return [3 /*break*/, 5];
669
+ case 5: return [2 /*return*/];
530
670
  }
531
671
  });
532
672
  });
@@ -540,6 +680,17 @@ var PresentationWindow = /** @class */ (function () {
540
680
  PresentationWindow.prototype.play = function () {
541
681
  var _this = this;
542
682
  var _a = this.options, applicationVersion = _a.applicationVersion, presentation = _a.presentation;
683
+ // A window holding a surface plays only once its frame is revealed over
684
+ // an open hole, so its first paint follows the hole opening.
685
+ var surface = this.surface;
686
+ if (surface && !surface.holeOpen) {
687
+ this.isPlayHeld = true;
688
+ return;
689
+ }
690
+ this.isPlayHeld = false;
691
+ // Each play starts a new paint cycle; 'first_painted' arrives once the
692
+ // window has presented a frame after this play.
693
+ this.didPaintSinceLastPlay = false;
543
694
  if (applicationVersion.configurableDuration) {
544
695
  var durationMs = Math.max(presentation.duration * 1000, 5000);
545
696
  if (this.configurableDurationTimeout) {
@@ -553,6 +704,20 @@ var PresentationWindow = /** @class */ (function () {
553
704
  }
554
705
  if (!this.isEmbeddedApp()) {
555
706
  this.sendMessageString('play');
707
+ // A replay seeks to the start rather than reopening, which would cost
708
+ // a new first frame with nothing in the hole meanwhile.
709
+ if (surface && surface.handle) {
710
+ if (surface.isPlayingAhead) {
711
+ surface.isPlayingAhead = false;
712
+ }
713
+ else {
714
+ if (surface.plays > 0) {
715
+ surface.handle.seek(0);
716
+ }
717
+ surface.handle.play();
718
+ surface.plays += 1;
719
+ }
720
+ }
556
721
  // Play any child window managers.
557
722
  Object.values(this.windowManagers).forEach(function (wm) {
558
723
  // We only want to trigger play on child windows the first play of the main window or if
@@ -579,6 +744,75 @@ var PresentationWindow = /** @class */ (function () {
579
744
  PresentationWindow.prototype.reset = function () {
580
745
  this.didReceiveComplete = false;
581
746
  };
747
+ // Resolves when the window reports its first painted frame after the most
748
+ // recent play, or after timeoutMs, whichever comes first. Only app-renderer
749
+ // windows emit the paint signal; anything else exits via the timeout.
750
+ PresentationWindow.prototype.waitForFirstPaint = function (timeoutMs) {
751
+ var _this = this;
752
+ if (this.didPaintSinceLastPlay) {
753
+ return Promise.resolve();
754
+ }
755
+ return new Promise(function (resolve) {
756
+ var settled = false;
757
+ var settle = function () {
758
+ if (settled)
759
+ return;
760
+ settled = true;
761
+ clearTimeout(timer);
762
+ var index = _this.firstPaintResolvers.indexOf(settle);
763
+ if (index !== -1) {
764
+ _this.firstPaintResolvers.splice(index, 1);
765
+ }
766
+ resolve();
767
+ };
768
+ var timer = setTimeout(settle, timeoutMs);
769
+ _this.firstPaintResolvers.push(settle);
770
+ });
771
+ };
772
+ // Stop delivering this window's events (complete, error, analytics, charge,
773
+ // interrupt locks) to the window manager. Used when the outgoing window is
774
+ // kept on screen only as a visual backdrop during a swap overlap — a late
775
+ // event from it must not be attributed to the incoming window.
776
+ PresentationWindow.prototype.detachCallbacks = function () {
777
+ this.readyCallback = null;
778
+ this.completeCallback = null;
779
+ this.errorCallback = null;
780
+ this.enableInterruptLockCallback = null;
781
+ this.disableInterruptLockCallback = null;
782
+ this.analyticsEventCallback = null;
783
+ this.chargeRequestCallback = null;
784
+ };
785
+ // Mute the window's audio channel. On the IFrameRenderer this is best-effort
786
+ // (iframes expose no mute API); BrowserWindow renderers mute for real.
787
+ PresentationWindow.prototype.mute = function () {
788
+ var _a = this.options, windowId = _a.windowId, renderer = _a.renderer;
789
+ renderer.muteWindow(windowId);
790
+ };
791
+ PresentationWindow.prototype.wasRemoved = function () {
792
+ return this.isRemoved;
793
+ };
794
+ // The window holds a video surface, opened or still opening.
795
+ PresentationWindow.prototype.holdsSurface = function () {
796
+ return this.surface !== null;
797
+ };
798
+ // A swap is taking this window off screen: mute its surface and tell the
799
+ // broker its close follows, or mute the window as today when it has none.
800
+ PresentationWindow.prototype.muteSurfaceOrFrame = function () {
801
+ var _a = this.options, windowId = _a.windowId, surfaceBroker = _a.surfaceBroker;
802
+ var surface = this.surface;
803
+ if (surface && surface.handle && surfaceBroker) {
804
+ surface.handle.setMuted(true);
805
+ surfaceBroker.leaving(windowId);
806
+ }
807
+ else {
808
+ this.mute();
809
+ }
810
+ };
811
+ PresentationWindow.prototype.resolveFirstPaintWaiters = function () {
812
+ var resolvers = this.firstPaintResolvers;
813
+ this.firstPaintResolvers = [];
814
+ resolvers.forEach(function (resolve) { return resolve(); });
815
+ };
582
816
  PresentationWindow.prototype.onComplete = function (callback) {
583
817
  this.completeCallback = callback;
584
818
  };
@@ -604,6 +838,12 @@ var PresentationWindow = /** @class */ (function () {
604
838
  if (this.isEmbeddedApp()) {
605
839
  return this.didEmbeddedAppLoad;
606
840
  }
841
+ // A window holding a surface is ready only once its first frame has
842
+ // arrived and the app has sized its hole from it.
843
+ var surface = this.surface;
844
+ if (surface && !this.isSurfaceReady(surface)) {
845
+ return false;
846
+ }
607
847
  return this.didReceiveReady && this.allChildWindowManagersReady();
608
848
  };
609
849
  PresentationWindow.prototype.isComplete = function () {
@@ -844,12 +1084,12 @@ var PresentationWindow = /** @class */ (function () {
844
1084
  case 0:
845
1085
  _a = this.options, windowId = _a.windowId, _b = _a.appRendererUrl, appRendererUrl = _b === void 0 ? defaultAppRendererUrl : _b, appRendererOfflineSupport = _a.appRendererOfflineSupport, backgroundColor = _a.backgroundColor;
846
1086
  this.logInfo("Create window '".concat(windowId, "'"));
1087
+ this.frameBackground = backgroundColor || '#000000'; // Default to black bg for Raydiant apps
847
1088
  return [4 /*yield*/, this.createAndLoadWindow(this.getFullAppRendererUrl({
848
1089
  appRendererUrl: appRendererUrl,
849
1090
  windowId: windowId,
850
1091
  appRendererOfflineSupport: appRendererOfflineSupport,
851
- }), backgroundColor || '#000000', // Default to black bg for Raydiant apps
852
- this.handleLegacyMessage)];
1092
+ }), this.frameBackground, this.handleLegacyMessage)];
853
1093
  case 1:
854
1094
  _c.sent();
855
1095
  return [4 /*yield*/, this.waitForPesentationReady()];
@@ -976,31 +1216,20 @@ var PresentationWindow = /** @class */ (function () {
976
1216
  return nextTheme;
977
1217
  };
978
1218
  PresentationWindow.prototype.sendRenderMessage = function () {
979
- var _a = this.options, applicationVersion = _a.applicationVersion, isPreview = _a.isPreview, device = _a.device;
1219
+ var _a = this.options, applicationVersion = _a.applicationVersion, isPreview = _a.isPreview, device = _a.device, surfaceBroker = _a.surfaceBroker;
1220
+ // The offer of a video surface; app renderer 3.2.0 gives the app its
1221
+ // miraVideo prop only when this is set.
1222
+ var surfaceOffer = surfaceBroker ? { nativeVideo: true } : {};
980
1223
  // IFrames do not have a mute/unmute API so we are overriding any mute properties
981
1224
  // to support muting MZ zones when using the IFrameRenderer (SecondScreen).
982
1225
  if (this.forceMute) {
983
- this.sendMessageString('render', {
984
- builderState: this.builderState,
985
- presentation: __assign(__assign({}, this.presentation), { applicationVariables: __assign(__assign({}, this.presentation.applicationVariables), {
1226
+ this.sendMessageString('render', __assign({ builderState: this.builderState, presentation: __assign(__assign({}, this.presentation), { applicationVariables: __assign(__assign({}, this.presentation.applicationVariables), {
986
1227
  // TODO: We need a better way to identify mute properties (ie. PropType.mute())
987
1228
  // Force mute the presentation, existing apps use either mute or muted.
988
- mute: true, muted: true }) }),
989
- theme: this.buildTheme(),
990
- applicationVersion: applicationVersion,
991
- device: device,
992
- isPreview: isPreview,
993
- });
1229
+ mute: true, muted: true }) }), theme: this.buildTheme(), applicationVersion: applicationVersion, device: device, isPreview: isPreview }, surfaceOffer));
994
1230
  }
995
1231
  else {
996
- this.sendMessageString('render', {
997
- builderState: this.builderState,
998
- presentation: this.presentation,
999
- theme: this.buildTheme(),
1000
- applicationVersion: applicationVersion,
1001
- device: device,
1002
- isPreview: isPreview,
1003
- });
1232
+ this.sendMessageString('render', __assign({ builderState: this.builderState, presentation: this.presentation, theme: this.buildTheme(), applicationVersion: applicationVersion, device: device, isPreview: isPreview }, surfaceOffer));
1004
1233
  }
1005
1234
  };
1006
1235
  PresentationWindow.prototype.openChildWindow = function (windowName, playlistId, bounds, forceMute, isFocused, themeOverride) {
@@ -1185,6 +1414,347 @@ var PresentationWindow = /** @class */ (function () {
1185
1414
  appRendererOfflineSupport: appRendererOfflineSupport,
1186
1415
  });
1187
1416
  };
1417
+ PresentationWindow.prototype.handleOpenVideo = function (payload) {
1418
+ var _this = this;
1419
+ var _a = this.options, windowId = _a.windowId, surfaceBroker = _a.surfaceBroker, presentation = _a.presentation;
1420
+ this.logInfo("Received 'open_video' from window '".concat(windowId, "', url=").concat(payload && payload.url));
1421
+ if (!surfaceBroker || this.surfaceWithdrawn || this.isEmbeddedApp()) {
1422
+ this.refuseVideo('not_offered');
1423
+ return;
1424
+ }
1425
+ if (this.surface) {
1426
+ this.logWarning("Ignoring 'open_video' from window '".concat(windowId, "', it already holds a surface"));
1427
+ return;
1428
+ }
1429
+ if (!payload ||
1430
+ typeof payload !== 'object' ||
1431
+ typeof payload.url !== 'string' ||
1432
+ !isValidRect(payload.bounds) ||
1433
+ !this.isOfferedVideoUrl(payload.url)) {
1434
+ this.refuseVideo('not_offered');
1435
+ return;
1436
+ }
1437
+ var surface = {
1438
+ handle: null,
1439
+ wantsSound: payload.muted === false,
1440
+ firstFrame: false,
1441
+ bounds: null,
1442
+ holeOpen: false,
1443
+ isHolePreopened: false,
1444
+ isPlayingAhead: false,
1445
+ plays: 0,
1446
+ };
1447
+ this.surface = surface;
1448
+ surfaceBroker
1449
+ .open(windowId, {
1450
+ url: payload.url,
1451
+ bounds: roundRect(payload.bounds),
1452
+ // Always opened muted; sound waits for the show.
1453
+ muted: true,
1454
+ label: presentation.id,
1455
+ }, function (event) { return _this.handleSurfaceEvent(surface, event); }, function (reason) {
1456
+ if (_this.surface !== surface)
1457
+ return;
1458
+ _this.sendVideoMessage('video_refused', { reason: reason });
1459
+ _this.dropSurface(surface);
1460
+ })
1461
+ .then(function (handle) {
1462
+ if (_this.surface !== surface)
1463
+ return;
1464
+ surface.handle = handle;
1465
+ _this.sendVideoMessage('video_opened');
1466
+ }, function (err) {
1467
+ if (_this.surface !== surface)
1468
+ return;
1469
+ var reason = err instanceof SurfaceBroker_1.SurfaceRefusal ? err.reason : 'unsupported';
1470
+ // The broker has logged its refusal.
1471
+ if (reason !== 'closed') {
1472
+ _this.sendVideoMessage('video_refused', { reason: reason });
1473
+ }
1474
+ _this.dropSurface(surface);
1475
+ });
1476
+ };
1477
+ PresentationWindow.prototype.handleVideoBounds = function (payload) {
1478
+ var surface = this.surface;
1479
+ if (!surface || !surface.handle)
1480
+ return;
1481
+ if (!payload || !isValidRect(payload.bounds)) {
1482
+ this.logWarning("Invalid payload received for 'video_bounds', ignoring");
1483
+ return;
1484
+ }
1485
+ var bounds = roundRect(payload.bounds);
1486
+ surface.handle.setBounds(bounds);
1487
+ if (surface.firstFrame) {
1488
+ var wasSized = surface.bounds !== null;
1489
+ surface.bounds = bounds;
1490
+ if (!wasSized) {
1491
+ this.checkIsReadyCallback();
1492
+ }
1493
+ }
1494
+ };
1495
+ PresentationWindow.prototype.handleSurfaceEvent = function (surface, event) {
1496
+ if (this.surface !== surface)
1497
+ return;
1498
+ // A stall is the host's to ride out, but it reaches the app: an app that
1499
+ // times its own complete holds that timer until the next 'playing'.
1500
+ if (event.type === 'first_frame') {
1501
+ surface.firstFrame = true;
1502
+ }
1503
+ this.sendVideoMessage('video_event', event);
1504
+ if (event.type === 'playing' && this.playingWaiter) {
1505
+ this.playingWaiter();
1506
+ }
1507
+ if (event.type === 'error') {
1508
+ this.dropSurface(surface);
1509
+ }
1510
+ };
1511
+ PresentationWindow.prototype.isSurfaceReady = function (surface) {
1512
+ return !!surface.handle && surface.firstFrame && surface.bounds !== null;
1513
+ };
1514
+ // Only an http(s) url the presentation holds, or one of its renditions,
1515
+ // opens a surface: it is the url the app would give <video>.
1516
+ PresentationWindow.prototype.isOfferedVideoUrl = function (url) {
1517
+ if (!/^https?:\/\//i.test(url))
1518
+ return false;
1519
+ return collectUrls(this.presentation.applicationVariables).some(function (offered) {
1520
+ return url === offered ||
1521
+ RENDITION_SUFFIXES.some(function (suffix) { return url === "".concat(offered).concat(suffix); });
1522
+ });
1523
+ };
1524
+ // Shows a ready surface: the host's show() is awaited, the app is told to
1525
+ // open its hole, and the frame is revealed once it has. One deadline covers
1526
+ // both waits; past it the app keeps <video> and the frame shows as today.
1527
+ PresentationWindow.prototype.showSurface = function (surface, isFocused, onReveal) {
1528
+ return __awaiter(this, void 0, void 0, function () {
1529
+ var _a, surfaceShowDeadlineMs, handle, passDeadline, deadline, timer, wake, woken, holeOpen, finish, outcome;
1530
+ var _this = this;
1531
+ return __generator(this, function (_b) {
1532
+ switch (_b.label) {
1533
+ case 0:
1534
+ _a = this.options.surfaceShowDeadlineMs, surfaceShowDeadlineMs = _a === void 0 ? SurfaceBroker_1.DEFAULT_SURFACE_SWAP_DELAY_MS : _a;
1535
+ handle = surface.handle;
1536
+ passDeadline = function () { return undefined; };
1537
+ deadline = new Promise(function (resolve) {
1538
+ passDeadline = resolve;
1539
+ });
1540
+ timer = setTimeout(function () { return passDeadline('deadline'); }, surfaceShowDeadlineMs);
1541
+ wake = function () { return undefined; };
1542
+ woken = new Promise(function (resolve) {
1543
+ wake = function () { return resolve('woken'); };
1544
+ });
1545
+ this.wakeSurfaceShow.push(wake);
1546
+ holeOpen = new Promise(function (resolve) {
1547
+ _this.holeOpenWaiter = function () { return resolve('hole_open'); };
1548
+ });
1549
+ finish = function () {
1550
+ clearTimeout(timer);
1551
+ _this.holeOpenWaiter = null;
1552
+ _this.wakeSurfaceShow = _this.wakeSurfaceShow.filter(function (w) { return w !== wake; });
1553
+ };
1554
+ return [4 /*yield*/, Promise.race([
1555
+ handle.show().then(function () { return 'shown'; }, function () { return 'failed'; }),
1556
+ deadline,
1557
+ woken,
1558
+ ])];
1559
+ case 1:
1560
+ outcome = _b.sent();
1561
+ if (!(outcome === 'shown' && this.surface === surface && !this.isRemoved)) return [3 /*break*/, 3];
1562
+ if (surface.wantsSound && !this.forceMute) {
1563
+ handle.setMuted(false);
1564
+ }
1565
+ this.sendVideoMessage('video_shown');
1566
+ return [4 /*yield*/, Promise.race([holeOpen, deadline, woken])];
1567
+ case 2:
1568
+ outcome = _b.sent();
1569
+ _b.label = 3;
1570
+ case 3:
1571
+ finish();
1572
+ if (this.isRemoved) {
1573
+ return [2 /*return*/];
1574
+ }
1575
+ if (outcome === 'hole_open' && this.surface === surface) {
1576
+ this.revealOverHole(surface, isFocused, onReveal);
1577
+ return [2 /*return*/];
1578
+ }
1579
+ // A show the host rejected falls back as a missed deadline does, under
1580
+ // the host's own answer.
1581
+ if ((outcome === 'deadline' || outcome === 'failed') &&
1582
+ this.surface === surface) {
1583
+ this.refuseVideo(outcome === 'failed' ? 'unsupported' : 'deadline');
1584
+ this.dropSurface(surface);
1585
+ }
1586
+ // The surface is gone: the frame shows and plays as today.
1587
+ this.showFrame(isFocused);
1588
+ this.releaseHeldPlay();
1589
+ return [2 /*return*/];
1590
+ }
1591
+ });
1592
+ });
1593
+ };
1594
+ // A swap's incoming surface whose app opened its hole while hidden. It
1595
+ // plays first, still hidden and muted, and the frame is revealed over the
1596
+ // outgoing surface once it reports playing, so the picture is moving when
1597
+ // it appears and no first frame is held. The surface is shown
1598
+ // showAfterRevealMs later: it reaches the screen sooner than the frame
1599
+ // does, so the two land together. Sound waits for the show, so it never
1600
+ // overlaps the outgoing surface's. A surface that never reports playing is
1601
+ // revealed at the show deadline; past a second deadline on the show the
1602
+ // app keeps <video> in the frame already revealed.
1603
+ PresentationWindow.prototype.showSurfaceAfterReveal = function (surface, isFocused, showAfterRevealMs, onReveal) {
1604
+ return __awaiter(this, void 0, void 0, function () {
1605
+ var _a, surfaceShowDeadlineMs, handle, timers, after, wake, woken, isHeld, playing, isRevealed, outcome;
1606
+ var _this = this;
1607
+ return __generator(this, function (_b) {
1608
+ switch (_b.label) {
1609
+ case 0:
1610
+ _a = this.options.surfaceShowDeadlineMs, surfaceShowDeadlineMs = _a === void 0 ? SurfaceBroker_1.DEFAULT_SURFACE_SWAP_DELAY_MS : _a;
1611
+ handle = surface.handle;
1612
+ timers = [];
1613
+ after = function (ms, value) {
1614
+ return new Promise(function (resolve) {
1615
+ timers.push(setTimeout(function () { return resolve(value); }, ms));
1616
+ });
1617
+ };
1618
+ wake = function () { return undefined; };
1619
+ woken = new Promise(function (resolve) {
1620
+ wake = function () { return resolve('woken'); };
1621
+ });
1622
+ this.wakeSurfaceShow.push(wake);
1623
+ isHeld = function () { return !_this.isRemoved && _this.surface === surface; };
1624
+ playing = new Promise(function (resolve) {
1625
+ _this.playingWaiter = function () { return resolve('playing'); };
1626
+ });
1627
+ handle.play();
1628
+ surface.plays += 1;
1629
+ surface.isPlayingAhead = true;
1630
+ isRevealed = false;
1631
+ return [4 /*yield*/, Promise.race([
1632
+ playing,
1633
+ after(surfaceShowDeadlineMs, 'no_playing'),
1634
+ woken,
1635
+ ])];
1636
+ case 1:
1637
+ outcome = _b.sent();
1638
+ this.playingWaiter = null;
1639
+ if (outcome === 'no_playing') {
1640
+ this.logWarning("No 'playing' from the surface of window '".concat(this.options.windowId, "', revealing it anyway"));
1641
+ }
1642
+ if (!((outcome === 'playing' || outcome === 'no_playing') && isHeld())) return [3 /*break*/, 3];
1643
+ isRevealed = true;
1644
+ this.revealOverHole(surface, isFocused, onReveal);
1645
+ return [4 /*yield*/, Promise.race([
1646
+ after(showAfterRevealMs, 'show'),
1647
+ woken,
1648
+ ])];
1649
+ case 2:
1650
+ outcome = _b.sent();
1651
+ _b.label = 3;
1652
+ case 3:
1653
+ if (!(outcome === 'show' && isHeld())) return [3 /*break*/, 5];
1654
+ return [4 /*yield*/, Promise.race([
1655
+ handle.show().then(function () { return 'shown'; }, function () { return 'failed'; }),
1656
+ after(surfaceShowDeadlineMs, 'deadline'),
1657
+ woken,
1658
+ ])];
1659
+ case 4:
1660
+ outcome = _b.sent();
1661
+ _b.label = 5;
1662
+ case 5:
1663
+ timers.forEach(function (timer) { return clearTimeout(timer); });
1664
+ this.wakeSurfaceShow = this.wakeSurfaceShow.filter(function (w) { return w !== wake; });
1665
+ if (this.isRemoved) {
1666
+ return [2 /*return*/];
1667
+ }
1668
+ if (outcome === 'shown' && this.surface === surface) {
1669
+ if (surface.wantsSound && !this.forceMute) {
1670
+ handle.setMuted(false);
1671
+ }
1672
+ this.sendVideoMessage('video_shown');
1673
+ return [2 /*return*/];
1674
+ }
1675
+ if ((outcome === 'deadline' || outcome === 'failed') &&
1676
+ this.surface === surface) {
1677
+ this.refuseVideo(outcome === 'failed' ? 'unsupported' : 'deadline');
1678
+ this.dropSurface(surface);
1679
+ }
1680
+ // The surface went before its reveal: the frame shows and plays as today.
1681
+ if (!isRevealed) {
1682
+ this.showFrame(isFocused);
1683
+ this.releaseHeldPlay();
1684
+ }
1685
+ return [2 /*return*/];
1686
+ }
1687
+ });
1688
+ });
1689
+ };
1690
+ // Reveals the frame over its surface's open hole, and runs the caller's
1691
+ // onReveal and any held play in that same task.
1692
+ PresentationWindow.prototype.revealOverHole = function (surface, isFocused, onReveal) {
1693
+ var _a = this.options, windowId = _a.windowId, renderer = _a.renderer, surfaceBroker = _a.surfaceBroker;
1694
+ surface.holeOpen = true;
1695
+ surfaceBroker.onHole(true);
1696
+ if (renderer.setWindowBackground) {
1697
+ renderer.setWindowBackground(windowId, 'transparent');
1698
+ }
1699
+ this.showFrame(isFocused);
1700
+ if (onReveal)
1701
+ onReveal();
1702
+ this.releaseHeldPlay();
1703
+ };
1704
+ // Shown before its surface was ready: any surface it holds closes and the
1705
+ // window is offered none from then on.
1706
+ PresentationWindow.prototype.withdrawSurface = function () {
1707
+ this.surfaceWithdrawn = true;
1708
+ var surface = this.surface;
1709
+ if (surface) {
1710
+ this.refuseVideo('not_offered');
1711
+ this.dropSurface(surface);
1712
+ }
1713
+ };
1714
+ // The window gives its surface up: closed through the broker, its hole
1715
+ // reported closed and its frame background restored. It holds none from
1716
+ // then on, and counts ready and swaps as any other window.
1717
+ PresentationWindow.prototype.dropSurface = function (surface) {
1718
+ var _a = this.options, windowId = _a.windowId, renderer = _a.renderer, surfaceBroker = _a.surfaceBroker;
1719
+ if (this.surface !== surface || !surfaceBroker)
1720
+ return;
1721
+ this.surface = null;
1722
+ this.surfaceWithdrawn = true;
1723
+ surfaceBroker.close(windowId);
1724
+ if (surface.holeOpen) {
1725
+ surface.holeOpen = false;
1726
+ surfaceBroker.onHole(false);
1727
+ if (renderer.setWindowBackground) {
1728
+ renderer.setWindowBackground(windowId, this.frameBackground);
1729
+ }
1730
+ }
1731
+ this.wakeSurfaceShowWaiters();
1732
+ this.checkIsReadyCallback();
1733
+ };
1734
+ PresentationWindow.prototype.wakeSurfaceShowWaiters = function () {
1735
+ var waiters = this.wakeSurfaceShow;
1736
+ this.wakeSurfaceShow = [];
1737
+ waiters.forEach(function (wake) { return wake(); });
1738
+ };
1739
+ PresentationWindow.prototype.releaseHeldPlay = function () {
1740
+ if (this.isPlayHeld) {
1741
+ this.isPlayHeld = false;
1742
+ this.play();
1743
+ }
1744
+ };
1745
+ PresentationWindow.prototype.refuseVideo = function (reason) {
1746
+ this.logWarning("Sending 'video_refused' to window '".concat(this.options.windowId, "', reason=").concat(reason));
1747
+ this.sendVideoMessage('video_refused', { reason: reason });
1748
+ };
1749
+ // Every video message rides the string channel the app renderer reads.
1750
+ PresentationWindow.prototype.sendVideoMessage = function (type, payload) {
1751
+ try {
1752
+ this.sendMessageString(type, payload);
1753
+ }
1754
+ catch (err) {
1755
+ this.logWarning("Failed to send '".concat(type, "' with '").concat(err.message, "'"));
1756
+ }
1757
+ };
1188
1758
  PresentationWindow.prototype.handleReady = function () {
1189
1759
  this.didReceiveReady = true;
1190
1760
  this.checkIsReadyCallback();