@raydiant/playlist-window-manager 0.29.2 → 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
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
  };
@@ -154,6 +193,17 @@ var PresentationWindow = /** @class */ (function () {
154
193
  this.createObjectURLTimeouts = {};
155
194
  this.interruptLocks = {};
156
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';
157
207
  this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
158
208
  var windowId, workerState, lock, lock, lock;
159
209
  return __generator(this, function (_a) {
@@ -361,6 +411,11 @@ var PresentationWindow = /** @class */ (function () {
361
411
  }
362
412
  break;
363
413
  }
414
+ case 'open_video': {
415
+ // An embedded app is never offered a video surface.
416
+ this.refuseVideo('not_offered');
417
+ break;
418
+ }
364
419
  default: {
365
420
  this.logWarning("Received unknown embedded message type: ".concat(type));
366
421
  }
@@ -369,7 +424,7 @@ var PresentationWindow = /** @class */ (function () {
369
424
  });
370
425
  }); };
371
426
  this.handleLegacyMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
372
- var _a, windowId, onPresentationProperties;
427
+ var _a, windowId, onPresentationProperties, surface;
373
428
  return __generator(this, function (_b) {
374
429
  _a = this.options, windowId = _a.windowId, onPresentationProperties = _a.onPresentationProperties;
375
430
  if (type === 'loaded') {
@@ -411,6 +466,33 @@ var PresentationWindow = /** @class */ (function () {
411
466
  this.logWarning("Invalid payload received for 'open_child_window', ignoring");
412
467
  }
413
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
+ }
414
496
  if (type === 'close_child_window') {
415
497
  // TODO: Validate close_child_window message payload.
416
498
  this.logInfo("Received 'close_child_window', windowId=".concat(payload.windowId));
@@ -456,9 +538,40 @@ var PresentationWindow = /** @class */ (function () {
456
538
  }
457
539
  return this.loadLegacyApp();
458
540
  };
459
- 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) {
460
549
  var _this = this;
461
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;
462
575
  var _a = this.options, windowId = _a.windowId, renderer = _a.renderer;
463
576
  // Show the main window and focus it before any child windows. Showing a child
464
577
  // window that requires focus will move focus from the main window to the child.
@@ -495,7 +608,7 @@ var PresentationWindow = /** @class */ (function () {
495
608
  };
496
609
  PresentationWindow.prototype.remove = function () {
497
610
  return __awaiter(this, void 0, void 0, function () {
498
- var _a, windowId, renderer, err_1;
611
+ var _a, windowId, renderer, surface, surfaceBroker, err_1;
499
612
  return __generator(this, function (_b) {
500
613
  switch (_b.label) {
501
614
  case 0:
@@ -520,23 +633,40 @@ var PresentationWindow = /** @class */ (function () {
520
633
  // Release anything waiting on a paint signal so callers never hang on a
521
634
  // window that is going away.
522
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();
523
640
  // Remove any child window managers.
524
641
  Object.values(this.windowManagers).forEach(function (wm) {
525
642
  wm.destroy();
526
643
  });
527
- _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)];
528
652
  case 1:
529
- _b.trys.push([1, 3, , 4]);
530
- return [4 /*yield*/, renderer.removeWindow(windowId)];
653
+ _b.sent();
654
+ if (surface.holeOpen) {
655
+ surfaceBroker.onHole(false);
656
+ }
657
+ _b.label = 2;
531
658
  case 2:
659
+ _b.trys.push([2, 4, , 5]);
660
+ return [4 /*yield*/, renderer.removeWindow(windowId)];
661
+ case 3:
532
662
  _b.sent();
533
663
  this.logInfo("Removed window '".concat(windowId, "'"));
534
- return [3 /*break*/, 4];
535
- case 3:
664
+ return [3 /*break*/, 5];
665
+ case 4:
536
666
  err_1 = _b.sent();
537
667
  this.logError("Failed to remove window '".concat(windowId, "' with '").concat(err_1.message, "'"));
538
- return [3 /*break*/, 4];
539
- case 4: return [2 /*return*/];
668
+ return [3 /*break*/, 5];
669
+ case 5: return [2 /*return*/];
540
670
  }
541
671
  });
542
672
  });
@@ -550,6 +680,14 @@ var PresentationWindow = /** @class */ (function () {
550
680
  PresentationWindow.prototype.play = function () {
551
681
  var _this = this;
552
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;
553
691
  // Each play starts a new paint cycle; 'first_painted' arrives once the
554
692
  // window has presented a frame after this play.
555
693
  this.didPaintSinceLastPlay = false;
@@ -566,6 +704,20 @@ var PresentationWindow = /** @class */ (function () {
566
704
  }
567
705
  if (!this.isEmbeddedApp()) {
568
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
+ }
569
721
  // Play any child window managers.
570
722
  Object.values(this.windowManagers).forEach(function (wm) {
571
723
  // We only want to trigger play on child windows the first play of the main window or if
@@ -636,6 +788,26 @@ var PresentationWindow = /** @class */ (function () {
636
788
  var _a = this.options, windowId = _a.windowId, renderer = _a.renderer;
637
789
  renderer.muteWindow(windowId);
638
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
+ };
639
811
  PresentationWindow.prototype.resolveFirstPaintWaiters = function () {
640
812
  var resolvers = this.firstPaintResolvers;
641
813
  this.firstPaintResolvers = [];
@@ -666,6 +838,12 @@ var PresentationWindow = /** @class */ (function () {
666
838
  if (this.isEmbeddedApp()) {
667
839
  return this.didEmbeddedAppLoad;
668
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
+ }
669
847
  return this.didReceiveReady && this.allChildWindowManagersReady();
670
848
  };
671
849
  PresentationWindow.prototype.isComplete = function () {
@@ -906,12 +1084,12 @@ var PresentationWindow = /** @class */ (function () {
906
1084
  case 0:
907
1085
  _a = this.options, windowId = _a.windowId, _b = _a.appRendererUrl, appRendererUrl = _b === void 0 ? defaultAppRendererUrl : _b, appRendererOfflineSupport = _a.appRendererOfflineSupport, backgroundColor = _a.backgroundColor;
908
1086
  this.logInfo("Create window '".concat(windowId, "'"));
1087
+ this.frameBackground = backgroundColor || '#000000'; // Default to black bg for Raydiant apps
909
1088
  return [4 /*yield*/, this.createAndLoadWindow(this.getFullAppRendererUrl({
910
1089
  appRendererUrl: appRendererUrl,
911
1090
  windowId: windowId,
912
1091
  appRendererOfflineSupport: appRendererOfflineSupport,
913
- }), backgroundColor || '#000000', // Default to black bg for Raydiant apps
914
- this.handleLegacyMessage)];
1092
+ }), this.frameBackground, this.handleLegacyMessage)];
915
1093
  case 1:
916
1094
  _c.sent();
917
1095
  return [4 /*yield*/, this.waitForPesentationReady()];
@@ -1038,31 +1216,20 @@ var PresentationWindow = /** @class */ (function () {
1038
1216
  return nextTheme;
1039
1217
  };
1040
1218
  PresentationWindow.prototype.sendRenderMessage = function () {
1041
- 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 } : {};
1042
1223
  // IFrames do not have a mute/unmute API so we are overriding any mute properties
1043
1224
  // to support muting MZ zones when using the IFrameRenderer (SecondScreen).
1044
1225
  if (this.forceMute) {
1045
- this.sendMessageString('render', {
1046
- builderState: this.builderState,
1047
- 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), {
1048
1227
  // TODO: We need a better way to identify mute properties (ie. PropType.mute())
1049
1228
  // Force mute the presentation, existing apps use either mute or muted.
1050
- mute: true, muted: true }) }),
1051
- theme: this.buildTheme(),
1052
- applicationVersion: applicationVersion,
1053
- device: device,
1054
- isPreview: isPreview,
1055
- });
1229
+ mute: true, muted: true }) }), theme: this.buildTheme(), applicationVersion: applicationVersion, device: device, isPreview: isPreview }, surfaceOffer));
1056
1230
  }
1057
1231
  else {
1058
- this.sendMessageString('render', {
1059
- builderState: this.builderState,
1060
- presentation: this.presentation,
1061
- theme: this.buildTheme(),
1062
- applicationVersion: applicationVersion,
1063
- device: device,
1064
- isPreview: isPreview,
1065
- });
1232
+ this.sendMessageString('render', __assign({ builderState: this.builderState, presentation: this.presentation, theme: this.buildTheme(), applicationVersion: applicationVersion, device: device, isPreview: isPreview }, surfaceOffer));
1066
1233
  }
1067
1234
  };
1068
1235
  PresentationWindow.prototype.openChildWindow = function (windowName, playlistId, bounds, forceMute, isFocused, themeOverride) {
@@ -1247,6 +1414,347 @@ var PresentationWindow = /** @class */ (function () {
1247
1414
  appRendererOfflineSupport: appRendererOfflineSupport,
1248
1415
  });
1249
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
+ };
1250
1758
  PresentationWindow.prototype.handleReady = function () {
1251
1759
  this.didReceiveReady = true;
1252
1760
  this.checkIsReadyCallback();