@xibosignage/xibo-layout-renderer 1.0.25 → 1.0.27

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.
@@ -707,6 +707,7 @@ var initialLayout = {
707
707
  return Promise.resolve([]);
708
708
  },
709
709
  removeLayout: function removeLayout() {},
710
+ discardLayout: function discardLayout() {},
710
711
  getXlf: function getXlf() {
711
712
  return '';
712
713
  },
@@ -717,6 +718,13 @@ var initialLayout = {
717
718
  html: null
718
719
  };
719
720
 
721
+ var MediaState = {
722
+ IDLE: 'idle',
723
+ PLAYING: 'playing',
724
+ ENDED: 'ended',
725
+ CANCELLED: 'cancelled'
726
+ };
727
+
720
728
  var initialRegion = {
721
729
  complete: false,
722
730
  containerName: '',
@@ -748,6 +756,7 @@ var initialRegion = {
748
756
  options: {},
749
757
  playNextMedia: function playNextMedia() {},
750
758
  playPreviousMedia: function playPreviousMedia() {},
759
+ prepareMedia: function prepareMedia(_media) {},
751
760
  prepareMediaObjects: function prepareMediaObjects() {},
752
761
  prepareRegion: function prepareRegion() {},
753
762
  ready: false,
@@ -765,56 +774,6 @@ var initialRegion = {
765
774
  xlr: {}
766
775
  };
767
776
 
768
- var MediaState = {
769
- IDLE: 'idle',
770
- PLAYING: 'playing',
771
- ENDED: 'ended',
772
- CANCELLED: 'cancelled'
773
- };
774
- var initialMedia = {
775
- attachedAudio: false,
776
- checkIframeStatus: false,
777
- containerName: '',
778
- divHeight: 0,
779
- divWidth: 0,
780
- duration: 0,
781
- emitter: {},
782
- enableStat: false,
783
- fileId: '',
784
- finished: false,
785
- html: null,
786
- id: '',
787
- idCounter: 0,
788
- iframe: null,
789
- iframeName: '',
790
- index: 0,
791
- loadIframeOnRun: false,
792
- loop: false,
793
- mediaId: '',
794
- mediaType: '',
795
- muted: false,
796
- options: {},
797
- player: undefined,
798
- ready: true,
799
- region: initialRegion,
800
- render: 'html',
801
- run: function run() {},
802
- schemaVersion: '1',
803
- singlePlay: false,
804
- state: MediaState.IDLE,
805
- stop: function stop() {
806
- return Promise.resolve();
807
- },
808
- tempSrc: '',
809
- timeoutId: setTimeout(function () {}, 0),
810
- type: '',
811
- uri: '',
812
- url: null,
813
- useDuration: Boolean(0),
814
- xml: null,
815
- mediaTimer: undefined
816
- };
817
-
818
777
  var OverlayLayoutManager = /*#__PURE__*/function () {
819
778
  function OverlayLayoutManager() {
820
779
  _classCallCheck(this, OverlayLayoutManager);
@@ -1111,6 +1070,7 @@ var initialXlr = {
1111
1070
  isLayoutInDOM: function isLayoutInDOM(containerName, layoutId) {
1112
1071
  return false;
1113
1072
  },
1073
+ cleanupOrphanedLayouts: function cleanupOrphanedLayouts(_keepCurrent, _keepNext) {},
1114
1074
  isSspEnabled: false,
1115
1075
  isUpdatingLoop: false,
1116
1076
  isUpdatingOverlays: false,
@@ -72531,6 +72491,10 @@ if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) {
72531
72491
  function composeVideoSource($media, media) {
72532
72492
  // const videoSrc = await preloadMediaBlob(media.url as string, media.mediaType as MediaTypes);
72533
72493
  var vidType = videoFileType(getFileExt(media.uri));
72494
+ if (!vidType) {
72495
+ console.warn("XLR >> VideoMedia: Unsupported video type for media ".concat(media.id, " with uri ").concat(media.uri));
72496
+ return $media;
72497
+ }
72534
72498
  // Only add one source per type
72535
72499
  if ($media.querySelectorAll("source[type=\"".concat(vidType, "\"]")).length === 0) {
72536
72500
  var $videoSource = document.createElement('source');
@@ -72557,8 +72521,38 @@ var vjsDefaultOptions = function vjsDefaultOptions(opts) {
72557
72521
  };
72558
72522
  var reportToPlayerPlatform = [exports.ConsumerPlatform.CHROMEOS, exports.ConsumerPlatform.ELECTRON];
72559
72523
  function VideoMedia(media, xlr) {
72524
+ var stopped = false;
72560
72525
  var mediaId = getMediaId(media);
72526
+ // ── Stall watchdog (closure-level so stop() can cancel it) ───────────────
72527
+ // 'waiting' and 'stalled' fire when the browser stops receiving data.
72528
+ // Unlike codec or source errors they do NOT fire the 'error' event, so
72529
+ // without a watchdog the video silently freezes for its entire duration.
72530
+ var stallWatchdog;
72531
+ var STALL_TIMEOUT_MS = 10000;
72532
+ var clearStallWatchdog = function clearStallWatchdog() {
72533
+ if (stallWatchdog !== undefined) {
72534
+ clearTimeout(stallWatchdog);
72535
+ stallWatchdog = undefined;
72536
+ }
72537
+ };
72538
+ // ─────────────────────────────────────────────────────────────────────────
72539
+ // ── Unified error → report → stop helper (closure-level) ─────────────────
72540
+ // Used by both the 'error' event and the play Promise catch.
72541
+ // playerReportFault only fires for platforms that report faults (Electron,
72542
+ // ChromeOS). All other platforms just advance to the next media via stop().
72543
+ var reportAndStop = function reportAndStop(reason, code) {
72544
+ if (stopped) return;
72545
+ if (reportToPlayerPlatform.includes(xlr.config.platform)) {
72546
+ playerReportFault(reason, media, code).then(function () {
72547
+ return videoPlayer.stop();
72548
+ });
72549
+ } else {
72550
+ videoPlayer.stop();
72551
+ }
72552
+ };
72553
+ // ─────────────────────────────────────────────────────────────────────────
72561
72554
  var videoPlayer = {
72555
+ player: undefined,
72562
72556
  duration: 0,
72563
72557
  init: function init() {
72564
72558
  var _this = this;
@@ -72566,9 +72560,37 @@ function VideoMedia(media, xlr) {
72566
72560
  videoPlayer.duration = media.duration;
72567
72561
  var vjsPlayer = videojs(mediaId);
72568
72562
  if (vjsPlayer) {
72569
- vjsPlayer.on('loadstart', function () {
72570
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has started loading data . . ."));
72571
- });
72563
+ videoPlayer.player = vjsPlayer;
72564
+ // ── Early source check ────────────────────────────────────────────────
72565
+ // Two-step check before video.js tries to load anything:
72566
+ // 1. Is the file extension one we map to a MIME type?
72567
+ // 2. Can the browser actually play that MIME type?
72568
+ // Failing either step skips the media immediately so video.js
72569
+ // never renders its "No compatible source" error overlay.
72570
+ var vidType = videoFileType(getFileExt(media.uri));
72571
+ if (!vidType) {
72572
+ console.warn("XLR >> VideoMedia: unrecognised file extension for media ".concat(media.id, " (uri: ").concat(media.uri, ")"));
72573
+ reportAndStop("Unsupported video file extension for media ".concat(media.id), exports.FaultCodes.FaultVideoSource);
72574
+ return;
72575
+ }
72576
+ if (document.createElement('video').canPlayType(vidType) === '') {
72577
+ console.warn("XLR >> VideoMedia: browser cannot play type \"".concat(vidType, "\" for media ").concat(media.id));
72578
+ reportAndStop("Browser cannot play video type \"".concat(vidType, "\" for media ").concat(media.id), exports.FaultCodes.FaultVideoSource);
72579
+ return;
72580
+ }
72581
+ // ─────────────────────────────────────────────────────────────────────
72582
+ var armStallWatchdog = function armStallWatchdog() {
72583
+ clearStallWatchdog();
72584
+ stallWatchdog = setTimeout(function () {
72585
+ if (stopped) return;
72586
+ console.warn("XLR >> VideoMedia: stall timeout on media ".concat(media.id));
72587
+ reportAndStop('Video stall timeout', exports.FaultCodes.FaultVideoUnexpected);
72588
+ }, STALL_TIMEOUT_MS);
72589
+ };
72590
+ vjsPlayer.on('waiting', armStallWatchdog);
72591
+ vjsPlayer.on('stalled', armStallWatchdog);
72592
+ vjsPlayer.on('playing', clearStallWatchdog);
72593
+ vjsPlayer.on('ended', clearStallWatchdog);
72572
72594
  vjsPlayer.on('loadedmetadata', function () {
72573
72595
  if (media.duration === 0) {
72574
72596
  videoPlayer.duration = vjsPlayer.duration();
@@ -72639,34 +72661,20 @@ function VideoMedia(media, xlr) {
72639
72661
  }, 5000);
72640
72662
  })]).then(function () {
72641
72663
  console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Autoplay started"));
72642
- })["catch"]( /*#__PURE__*/function () {
72643
- var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(error) {
72644
- return _regeneratorRuntime().wrap(function _callee2$(_context2) {
72645
- while (1) switch (_context2.prev = _context2.next) {
72646
- case 0:
72647
- if (error === 'Timeout') {
72648
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Promise not resolved within 5 seconds. Move to next media"));
72649
- _this.stop();
72650
- } else {
72651
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Autoplay error: ").concat(error));
72652
- if (reportToPlayerPlatform.includes(xlr.config.platform)) {
72653
- playerReportFault('Media autoplay error', media).then(function () {
72654
- _this.stop();
72655
- });
72656
- }
72657
- }
72658
- case 1:
72659
- case "end":
72660
- return _context2.stop();
72661
- }
72662
- }, _callee2);
72663
- }));
72664
- return function (_x) {
72665
- return _ref2.apply(this, arguments);
72666
- };
72667
- }());
72664
+ })["catch"](function (error) {
72665
+ if (stopped) return;
72666
+ if (error === 'Timeout') {
72667
+ console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Promise not resolved within 5 seconds. Move to next media"));
72668
+ // Timeout is a scheduling issue, not a media fault — just advance
72669
+ videoPlayer.stop();
72670
+ } else {
72671
+ console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Autoplay error: ").concat(error));
72672
+ reportAndStop('Media autoplay error', exports.FaultCodes.FaultVideoUnexpected);
72673
+ }
72674
+ });
72668
72675
  // Optional: Reset the flag automatically when a new video loads or the source changes
72669
72676
  vjsPlayer.on('loadstart', function () {
72677
+ console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has started loading data . . ."));
72670
72678
  triggerTimeUpdate = false;
72671
72679
  });
72672
72680
  if (media.duration === 0) {
@@ -72679,13 +72687,9 @@ function VideoMedia(media, xlr) {
72679
72687
  if (mediaDuration !== undefined && currentTime !== undefined) {
72680
72688
  remainingTimeMs = (mediaDuration - currentTime) * 1000;
72681
72689
  }
72682
- if (regionHasMultipleMedia && remainingTimeMs === 0 && !triggerTimeUpdate) {
72683
- // We don't have data yet and we must immediately prepare next media
72684
- media.region.prepareNextMedia();
72685
- } else if (regionHasMultipleMedia && remainingTimeMs <= preloadBufferTimeMs && !triggerTimeUpdate) {
72690
+ if (regionHasMultipleMedia && !triggerTimeUpdate && (remainingTimeMs === 0 || remainingTimeMs <= preloadBufferTimeMs)) {
72686
72691
  // Check if remaining time is less than preloadBufferTimeMs and the action hasn't been triggered yet
72687
72692
  console.log('Less than preloadBufferTimeMs remaining! Do something now.');
72688
- // Prepare next media in region
72689
72693
  media.region.prepareNextMedia();
72690
72694
  triggerTimeUpdate = true; // Set the flag to prevent re-triggering
72691
72695
  }
@@ -72697,33 +72701,16 @@ function VideoMedia(media, xlr) {
72697
72701
  }
72698
72702
  }
72699
72703
  });
72700
- vjsPlayer.on('error', /*#__PURE__*/function () {
72701
- var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(err) {
72702
- return _regeneratorRuntime().wrap(function _callee3$(_context3) {
72703
- while (1) switch (_context3.prev = _context3.next) {
72704
- case 0:
72705
- console.debug("??? XLR.debug >> VideoMedia: Media Error: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id));
72706
- if (reportToPlayerPlatform.includes(xlr.config.platform)) {
72707
- playerReportFault('Video file source not supported', media).then(function () {
72708
- _this.stop();
72709
- });
72710
- } else {
72711
- // End media after 5 seconds
72712
- setTimeout(function () {
72713
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has ended . . ."));
72714
- _this.stop();
72715
- }, 5000);
72716
- }
72717
- case 2:
72718
- case "end":
72719
- return _context3.stop();
72720
- }
72721
- }, _callee3);
72722
- }));
72723
- return function (_x2) {
72724
- return _ref3.apply(this, arguments);
72725
- };
72726
- }());
72704
+ vjsPlayer.on('error', function () {
72705
+ if (stopped) return;
72706
+ clearStallWatchdog();
72707
+ // Extract the actual MediaError so the fault message is
72708
+ // meaningful: code 2 = network, 3 = decode, 4 = not supported.
72709
+ var vjsError = vjsPlayer.error();
72710
+ var reason = vjsError ? "Video error (code ".concat(vjsError.code, "): ").concat(vjsError.message) : 'Unknown video error';
72711
+ console.warn("XLR >> VideoMedia: error on media ".concat(media.id), vjsError);
72712
+ reportAndStop(reason, exports.FaultCodes.FaultVideoUnexpected);
72713
+ });
72727
72714
  if (media.duration === 0) {
72728
72715
  vjsPlayer.on('ended', function () {
72729
72716
  console.debug("??? XLR.debug >> VideoMedia: onended: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has ended playing . . ."));
@@ -72733,26 +72720,37 @@ function VideoMedia(media, xlr) {
72733
72720
  }
72734
72721
  },
72735
72722
  stop: function stop() {
72723
+ var _videoPlayer$player;
72736
72724
  var disposeOnly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
72737
- var vjsPlayer = media.player;
72725
+ clearStallWatchdog();
72726
+ // videoPlayer.player is where init() stores the vjs instance;
72727
+ // media.player is a legacy path kept for backward compat but is
72728
+ // no longer set by init(), so always prefer videoPlayer.player.
72729
+ var vjsPlayer = (_videoPlayer$player = videoPlayer.player) !== null && _videoPlayer$player !== void 0 ? _videoPlayer$player : media.player;
72738
72730
  console.debug('??? XLR.debug >> VideoMedia::stop', {
72739
72731
  vjsPlayer: vjsPlayer,
72740
- isDisposed: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.isDisposed_,
72741
- el: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.el_
72732
+ isDisposed: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.isDisposed(),
72733
+ el: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.el()
72742
72734
  });
72743
72735
  // Expire the media and dispose the video
72744
- if (vjsPlayer !== undefined && !vjsPlayer.isDisposed_) {
72736
+ if (vjsPlayer !== undefined && !vjsPlayer.isDisposed()) {
72745
72737
  if (!disposeOnly) {
72746
72738
  media.emitter.emit('end', media);
72747
72739
  }
72748
72740
  vjsPlayer.dispose();
72749
72741
  // Clear up media player
72742
+ videoPlayer.player = undefined;
72750
72743
  media.player = undefined;
72744
+ media.html = null;
72751
72745
  } else {
72746
+ videoPlayer.player = undefined;
72752
72747
  media.player = undefined;
72753
72748
  media.html = null;
72754
- media.emitter.emit('end', media);
72749
+ if (!disposeOnly) {
72750
+ media.emitter.emit('end', media);
72751
+ }
72755
72752
  }
72753
+ stopped = true;
72756
72754
  },
72757
72755
  play: function play() {
72758
72756
  var _this2 = this;
@@ -72760,9 +72758,9 @@ function VideoMedia(media, xlr) {
72760
72758
  if (vjsPlayer !== undefined) {
72761
72759
  var _vjsPlayer$play;
72762
72760
  (_vjsPlayer$play = vjsPlayer.play()) === null || _vjsPlayer$play === void 0 || _vjsPlayer$play["catch"]( /*#__PURE__*/function () {
72763
- var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(error) {
72764
- return _regeneratorRuntime().wrap(function _callee4$(_context4) {
72765
- while (1) switch (_context4.prev = _context4.next) {
72761
+ var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(error) {
72762
+ return _regeneratorRuntime().wrap(function _callee2$(_context2) {
72763
+ while (1) switch (_context2.prev = _context2.next) {
72766
72764
  case 0:
72767
72765
  if (error === 'Timeout') {
72768
72766
  console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Promise not resolved within 5 seconds. Move to next media"));
@@ -72777,12 +72775,12 @@ function VideoMedia(media, xlr) {
72777
72775
  }
72778
72776
  case 1:
72779
72777
  case "end":
72780
- return _context4.stop();
72778
+ return _context2.stop();
72781
72779
  }
72782
- }, _callee4);
72780
+ }, _callee2);
72783
72781
  }));
72784
- return function (_x3) {
72785
- return _ref4.apply(this, arguments);
72782
+ return function (_x) {
72783
+ return _ref2.apply(this, arguments);
72786
72784
  };
72787
72785
  }());
72788
72786
  }
@@ -73194,11 +73192,11 @@ function getDataBlob(_x, _x2) {
73194
73192
  return _getDataBlob.apply(this, arguments);
73195
73193
  }
73196
73194
  function _getDataBlob() {
73197
- _getDataBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(src, jwtToken) {
73198
- return _regeneratorRuntime().wrap(function _callee3$(_context3) {
73199
- while (1) switch (_context3.prev = _context3.next) {
73195
+ _getDataBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(src, jwtToken) {
73196
+ return _regeneratorRuntime().wrap(function _callee2$(_context2) {
73197
+ while (1) switch (_context2.prev = _context2.next) {
73200
73198
  case 0:
73201
- return _context3.abrupt("return", fetch(src, {
73199
+ return _context2.abrupt("return", fetch(src, {
73202
73200
  method: 'GET',
73203
73201
  headers: {
73204
73202
  'X-PREVIEW-JWT': jwtToken || ''
@@ -73217,9 +73215,9 @@ function _getDataBlob() {
73217
73215
  }));
73218
73216
  case 1:
73219
73217
  case "end":
73220
- return _context3.stop();
73218
+ return _context2.stop();
73221
73219
  }
73222
- }, _callee3);
73220
+ }, _callee2);
73223
73221
  }));
73224
73222
  return _getDataBlob.apply(this, arguments);
73225
73223
  }
@@ -73227,12 +73225,12 @@ function preloadMediaBlob(_x3, _x4, _x5) {
73227
73225
  return _preloadMediaBlob.apply(this, arguments);
73228
73226
  }
73229
73227
  function _preloadMediaBlob() {
73230
- _preloadMediaBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(src, type, jwtToken) {
73228
+ _preloadMediaBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(src, type, jwtToken) {
73231
73229
  var res, blob, data;
73232
- return _regeneratorRuntime().wrap(function _callee4$(_context4) {
73233
- while (1) switch (_context4.prev = _context4.next) {
73230
+ return _regeneratorRuntime().wrap(function _callee3$(_context3) {
73231
+ while (1) switch (_context3.prev = _context3.next) {
73234
73232
  case 0:
73235
- _context4.next = 2;
73233
+ _context3.next = 2;
73236
73234
  return fetch(src, {
73237
73235
  method: 'GET',
73238
73236
  headers: {
@@ -73240,45 +73238,45 @@ function _preloadMediaBlob() {
73240
73238
  }
73241
73239
  });
73242
73240
  case 2:
73243
- res = _context4.sent;
73241
+ res = _context3.sent;
73244
73242
  blob = new Blob();
73245
73243
  if (!(type === 'image')) {
73246
- _context4.next = 8;
73244
+ _context3.next = 8;
73247
73245
  break;
73248
73246
  }
73249
73247
  blob = new Blob();
73250
- _context4.next = 19;
73248
+ _context3.next = 19;
73251
73249
  break;
73252
73250
  case 8:
73253
73251
  if (!(type === 'video')) {
73254
- _context4.next = 14;
73252
+ _context3.next = 14;
73255
73253
  break;
73256
73254
  }
73257
- _context4.next = 11;
73255
+ _context3.next = 11;
73258
73256
  return res.blob();
73259
73257
  case 11:
73260
- blob = _context4.sent;
73261
- _context4.next = 19;
73258
+ blob = _context3.sent;
73259
+ _context3.next = 19;
73262
73260
  break;
73263
73261
  case 14:
73264
73262
  if (!(type === 'audio')) {
73265
- _context4.next = 19;
73263
+ _context3.next = 19;
73266
73264
  break;
73267
73265
  }
73268
- _context4.next = 17;
73266
+ _context3.next = 17;
73269
73267
  return res.arrayBuffer();
73270
73268
  case 17:
73271
- data = _context4.sent;
73269
+ data = _context3.sent;
73272
73270
  blob = new Blob([data], {
73273
73271
  type: audioFileType(getFileExt(src))
73274
73272
  });
73275
73273
  case 19:
73276
- return _context4.abrupt("return", URL.createObjectURL(blob));
73274
+ return _context3.abrupt("return", URL.createObjectURL(blob));
73277
73275
  case 20:
73278
73276
  case "end":
73279
- return _context4.stop();
73277
+ return _context3.stop();
73280
73278
  }
73281
- }, _callee4);
73279
+ }, _callee3);
73282
73280
  }));
73283
73281
  return _preloadMediaBlob.apply(this, arguments);
73284
73282
  }
@@ -73286,11 +73284,11 @@ function fetchJSON(_x6, _x7) {
73286
73284
  return _fetchJSON.apply(this, arguments);
73287
73285
  }
73288
73286
  function _fetchJSON() {
73289
- _fetchJSON = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(url, jwtToken) {
73290
- return _regeneratorRuntime().wrap(function _callee5$(_context5) {
73291
- while (1) switch (_context5.prev = _context5.next) {
73287
+ _fetchJSON = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(url, jwtToken) {
73288
+ return _regeneratorRuntime().wrap(function _callee4$(_context4) {
73289
+ while (1) switch (_context4.prev = _context4.next) {
73292
73290
  case 0:
73293
- return _context5.abrupt("return", fetch(url, {
73291
+ return _context4.abrupt("return", fetch(url, {
73294
73292
  method: 'GET',
73295
73293
  headers: {
73296
73294
  'X-PREVIEW-JWT': jwtToken || ''
@@ -73302,9 +73300,9 @@ function _fetchJSON() {
73302
73300
  }));
73303
73301
  case 1:
73304
73302
  case "end":
73305
- return _context5.stop();
73303
+ return _context4.stop();
73306
73304
  }
73307
- }, _callee5);
73305
+ }, _callee4);
73308
73306
  }));
73309
73307
  return _fetchJSON.apply(this, arguments);
73310
73308
  }
@@ -73312,11 +73310,11 @@ function fetchText(_x8, _x9) {
73312
73310
  return _fetchText.apply(this, arguments);
73313
73311
  }
73314
73312
  function _fetchText() {
73315
- _fetchText = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6(url, jwtToken) {
73316
- return _regeneratorRuntime().wrap(function _callee6$(_context6) {
73317
- while (1) switch (_context6.prev = _context6.next) {
73313
+ _fetchText = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(url, jwtToken) {
73314
+ return _regeneratorRuntime().wrap(function _callee5$(_context5) {
73315
+ while (1) switch (_context5.prev = _context5.next) {
73318
73316
  case 0:
73319
- return _context6.abrupt("return", fetch(url, {
73317
+ return _context5.abrupt("return", fetch(url, {
73320
73318
  method: 'GET',
73321
73319
  headers: {
73322
73320
  'X-PREVIEW-JWT': jwtToken || ''
@@ -73335,9 +73333,9 @@ function _fetchText() {
73335
73333
  }));
73336
73334
  case 1:
73337
73335
  case "end":
73338
- return _context6.stop();
73336
+ return _context5.stop();
73339
73337
  }
73340
- }, _callee6);
73338
+ }, _callee5);
73341
73339
  }));
73342
73340
  return _fetchText.apply(this, arguments);
73343
73341
  }
@@ -73454,6 +73452,32 @@ function setExpiry(numDays) {
73454
73452
  var today = new Date();
73455
73453
  return new Date(today.setHours(24 * numDays || 1)).toJSON();
73456
73454
  }
73455
+ /**
73456
+ * Check whether a media item is currently within its valid date window.
73457
+ * Returns true when the media should be shown, false when it should be skipped.
73458
+ *
73459
+ * Rules:
73460
+ * - Empty / invalid fromDt → treat as "no start restriction"
73461
+ * - Empty / invalid toDt → treat as "no expiry"
73462
+ * - now < fromDt → not yet active → skip
73463
+ * - now > toDt → expired → skip
73464
+ */
73465
+ function isMediaActive(fromDt, toDt) {
73466
+ var now = Date.now();
73467
+ if (fromDt) {
73468
+ var from = new Date(fromDt).getTime();
73469
+ if (!isNaN(from) && now < from) {
73470
+ return false;
73471
+ }
73472
+ }
73473
+ if (toDt) {
73474
+ var to = new Date(toDt).getTime();
73475
+ if (!isNaN(to) && now > to) {
73476
+ return false;
73477
+ }
73478
+ }
73479
+ return true;
73480
+ }
73457
73481
  /**
73458
73482
  * Check if given layout exists in the loop using layoutId
73459
73483
  * @param layouts Schedule loop unique layouts (uniqueLayouts)
@@ -73662,7 +73686,7 @@ function prepareVideoMedia(media, region) {
73662
73686
  var $layout = region.layout.html;
73663
73687
  var layoutSelector = '#' + region.layout.containerName + '[data-sequence="' + region.layout.index + '"]';
73664
73688
  var $layoutWithIndex = document.querySelector(layoutSelector);
73665
- var $region = document.querySelector('#' + region.containerName);
73689
+ var $region = region.html;
73666
73690
  var mediaInRegion = $region === null || $region === void 0 ? void 0 : $region.querySelector('.' + mediaId);
73667
73691
  console.debug('??? XLR.debug >> [Generators::prepareVideoMedia]', {
73668
73692
  layoutSelector: layoutSelector,
@@ -73681,7 +73705,7 @@ function prepareVideoMedia(media, region) {
73681
73705
  media.html = createMediaElement(media);
73682
73706
  }
73683
73707
  // Append fresh copy of the media into the region
73684
- $region !== null && $region.appendChild(media.html);
73708
+ region.html.appendChild(media.html);
73685
73709
  var isMediaInDOM = document.body.contains(media.html);
73686
73710
  console.debug('??? XLR.debug >> [Generators::prepareVideoMedia]', {
73687
73711
  isMediaInDOM: isMediaInDOM,
@@ -73690,29 +73714,9 @@ function prepareVideoMedia(media, region) {
73690
73714
  });
73691
73715
  // Initialize video.js
73692
73716
  media.player = videojs(mediaId, _objectSpread2(_objectSpread2({}, defaultVjsOpts), {}, {
73693
- errorDisplay: region.xlr.config.platform !== exports.ConsumerPlatform.CHROMEOS,
73717
+ errorDisplay: !reportToPlayerPlatform.includes(region.xlr.config.platform),
73694
73718
  loop: media.loop
73695
73719
  }));
73696
- media.player.on('error', /*#__PURE__*/function () {
73697
- var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(err) {
73698
- return _regeneratorRuntime().wrap(function _callee2$(_context2) {
73699
- while (1) switch (_context2.prev = _context2.next) {
73700
- case 0:
73701
- if (media.region.xlr.config.platform === exports.ConsumerPlatform.CHROMEOS) {
73702
- playerReportFault('Video file not supported', media).then(function () {
73703
- media.emitter.emit('end', media);
73704
- });
73705
- }
73706
- case 1:
73707
- case "end":
73708
- return _context2.stop();
73709
- }
73710
- }, _callee2);
73711
- }));
73712
- return function (_x10) {
73713
- return _ref3.apply(this, arguments);
73714
- };
73715
- }());
73716
73720
  media.player.el().style.setProperty('visibility', 'hidden');
73717
73721
  media.player.el().style.setProperty('opacity', '0');
73718
73722
  media.player.el().style.setProperty('z-index', '-99');
@@ -73727,9 +73731,9 @@ function prepareImageMedia(media, region) {
73727
73731
  if (mediaInRegion) {
73728
73732
  mediaInRegion.remove();
73729
73733
  }
73730
- // Append media to its region
73731
- var $region = document.querySelector('#' + region.containerName);
73732
- $region !== null && $region.appendChild(media.html);
73734
+ // Append media to its region using the direct reference to avoid
73735
+ // global querySelector finding a same-named region in another layout
73736
+ region.html.appendChild(media.html);
73733
73737
  }
73734
73738
  function prepareAudioMedia(media, region) {
73735
73739
  var mediaId = getMediaId(media);
@@ -73742,9 +73746,8 @@ function prepareAudioMedia(media, region) {
73742
73746
  if (mediaInRegion) {
73743
73747
  mediaInRegion.remove();
73744
73748
  }
73745
- // Append media to its region
73746
- var $region = document.querySelector('#' + region.containerName);
73747
- $region !== null && $region.appendChild(media.html);
73749
+ // Append media to its region using the direct reference
73750
+ region.html.appendChild(media.html);
73748
73751
  }
73749
73752
  function prepareHtmlMedia(media, region) {
73750
73753
  // Set state as false ( for now )
@@ -73764,57 +73767,92 @@ function prepareHtmlMedia(media, region) {
73764
73767
  media.html.innerHTML = '';
73765
73768
  media.html.appendChild(media.iframe);
73766
73769
  if (!mediaInRegion) {
73767
- // Add fresh copy of the media into the region
73768
- var _$region = document.querySelector('#' + region.containerName);
73769
- _$region !== null && _$region.appendChild(media.html);
73770
+ // Add fresh copy of the media into the region using the direct reference
73771
+ region.html.appendChild(media.html);
73770
73772
  media.ready = true;
73771
73773
  }
73772
73774
  }
73773
73775
  }
73774
- function playerReportFault(_x11, _x12) {
73776
+ exports.FaultCodes = void 0;
73777
+ (function (FaultCodes) {
73778
+ FaultCodes[FaultCodes["FaultVideoSource"] = 2001] = "FaultVideoSource";
73779
+ FaultCodes[FaultCodes["FaultVideoUnexpected"] = 2099] = "FaultVideoUnexpected";
73780
+ })(exports.FaultCodes || (exports.FaultCodes = {}));
73781
+ function playerReportFault(_x10, _x11) {
73775
73782
  return _playerReportFault.apply(this, arguments);
73776
73783
  }
73777
73784
  function _playerReportFault() {
73778
- _playerReportFault = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee7(msg, media) {
73779
- var playerSW, hasSW;
73780
- return _regeneratorRuntime().wrap(function _callee7$(_context7) {
73781
- while (1) switch (_context7.prev = _context7.next) {
73785
+ _playerReportFault = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6(msg, media) {
73786
+ var code,
73787
+ platform,
73788
+ playerSW,
73789
+ hasSW,
73790
+ mediaFault,
73791
+ channel,
73792
+ _args6 = arguments;
73793
+ return _regeneratorRuntime().wrap(function _callee6$(_context6) {
73794
+ while (1) switch (_context6.prev = _context6.next) {
73782
73795
  case 0:
73796
+ code = _args6.length > 2 && _args6[2] !== undefined ? _args6[2] : exports.FaultCodes.FaultVideoUnexpected;
73783
73797
  // Immediately expire media and report a fault
73798
+ platform = media.region.xlr.config.platform;
73784
73799
  playerSW = PwaSW();
73785
- _context7.next = 3;
73800
+ _context6.next = 5;
73786
73801
  return playerSW.getSW();
73787
- case 3:
73788
- hasSW = _context7.sent;
73789
- if (hasSW) {
73790
- playerSW.postMsg({
73791
- type: 'MEDIA_FAULT',
73792
- code: 5002,
73793
- reason: msg,
73802
+ case 5:
73803
+ hasSW = _context6.sent;
73804
+ mediaFault = {
73805
+ type: 'MEDIA_FAULT',
73806
+ code: code,
73807
+ reason: msg,
73808
+ mediaId: media.id,
73809
+ regionId: media.region.id,
73810
+ layoutId: media.region.layout.id,
73811
+ date: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
73812
+ // Temporary setting
73813
+ expires: format(new Date(setExpiry(1)), 'yyyy-MM-dd HH:mm:ss')
73814
+ };
73815
+ console.debug('playerReportFault >> Reporting media fault', {
73816
+ mediaFault: mediaFault,
73817
+ platform: platform,
73818
+ hasSW: hasSW
73819
+ });
73820
+ if (!(platform === exports.ConsumerPlatform.CHROMEOS && hasSW)) {
73821
+ _context6.next = 12;
73822
+ break;
73823
+ }
73824
+ return _context6.abrupt("return", playerSW.postMsg(mediaFault).then(function () {
73825
+ // We try to prepare next media if we have more than 1 media
73826
+ if (media.region.totalMediaObjects > 1) {
73827
+ media.region.prepareNextMedia();
73828
+ }
73829
+ })["finally"](function () {
73830
+ // Stopping media as we have reported the error as fault
73831
+ console.debug('??? XLR.debug >> VideoMedia - Done reporting media fault', {
73794
73832
  mediaId: media.id,
73795
- regionId: media.region.id,
73796
- layoutId: media.region.layout.id,
73797
- date: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
73798
- // Temporary setting
73799
- expires: format(new Date(setExpiry(1)), 'yyyy-MM-dd HH:mm:ss')
73800
- }).then(function () {
73801
- // We try to prepare next media if we have more than 1 media
73802
- if (media.region.totalMediaObjects > 1) {
73803
- media.region.prepareNextMedia();
73804
- }
73805
- })["finally"](function () {
73806
- // Stopping media as we have reported the error as fault
73807
- console.debug('??? XLR.debug >> VideoMedia - Done reporting media fault', {
73808
- mediaId: media.id,
73809
- regionItems: media.region.totalMediaObjects
73810
- });
73833
+ regionItems: media.region.totalMediaObjects
73811
73834
  });
73835
+ }));
73836
+ case 12:
73837
+ if (!(platform === exports.ConsumerPlatform.ELECTRON)) {
73838
+ _context6.next = 17;
73839
+ break;
73812
73840
  }
73813
- case 5:
73841
+ // Create a broadcast channel to report media fault to the main process
73842
+ channel = new BroadcastChannel('player-faults-bc');
73843
+ channel.postMessage(mediaFault);
73844
+ console.debug('playerReportFault >> Electron platform - posted media fault to channel', {
73845
+ mediaFault: mediaFault
73846
+ });
73847
+ // channel.close();
73848
+ return _context6.abrupt("return", Promise.resolve());
73849
+ case 17:
73850
+ return _context6.abrupt("return", Promise.resolve());
73851
+ case 18:
73814
73852
  case "end":
73815
- return _context7.stop();
73853
+ return _context6.stop();
73816
73854
  }
73817
- }, _callee7);
73855
+ }, _callee6);
73818
73856
  }));
73819
73857
  return _playerReportFault.apply(this, arguments);
73820
73858
  }
@@ -73842,7 +73880,7 @@ let nanoid = (size = 21) => {
73842
73880
  function AudioMedia(media) {
73843
73881
  var audioMediaObject = {
73844
73882
  init: function init() {
73845
- var $audioMedia = document.getElementById(getMediaId(media));
73883
+ var $audioMedia = media.html;
73846
73884
  var $playBtn = null;
73847
73885
  if ($audioMedia) {
73848
73886
  $audioMedia.onloadstart = function () {
@@ -73905,6 +73943,8 @@ var Media = /*#__PURE__*/function () {
73905
73943
  _this$xml3,
73906
73944
  _this$xml4,
73907
73945
  _this$xml5,
73946
+ _this$xml6,
73947
+ _this$xml7,
73908
73948
  _this = this;
73909
73949
  _classCallCheck(this, Media);
73910
73950
  _defineProperty(this, "attachedAudio", false);
@@ -73917,6 +73957,7 @@ var Media = /*#__PURE__*/function () {
73917
73957
  _defineProperty(this, "enableStat", false);
73918
73958
  _defineProperty(this, "fileId", '');
73919
73959
  _defineProperty(this, "finished", false);
73960
+ _defineProperty(this, "fromDt", '');
73920
73961
  _defineProperty(this, "html", null);
73921
73962
  _defineProperty(this, "id", '');
73922
73963
  _defineProperty(this, "idCounter", 0);
@@ -73938,6 +73979,7 @@ var Media = /*#__PURE__*/function () {
73938
73979
  _defineProperty(this, "state", MediaState.IDLE);
73939
73980
  _defineProperty(this, "tempSrc", '');
73940
73981
  _defineProperty(this, "timeoutId", setTimeout(function () {}, 0));
73982
+ _defineProperty(this, "toDt", '');
73941
73983
  _defineProperty(this, "type", '');
73942
73984
  _defineProperty(this, "uri", '');
73943
73985
  _defineProperty(this, "url", null);
@@ -73945,6 +73987,9 @@ var Media = /*#__PURE__*/function () {
73945
73987
  _defineProperty(this, "xml", null);
73946
73988
  _defineProperty(this, "videoHandler", void 0);
73947
73989
  _defineProperty(this, "mediaTimer", void 0);
73990
+ _defineProperty(this, "sspImpressionUrls", undefined);
73991
+ _defineProperty(this, "sspErrorUrls", undefined);
73992
+ _defineProperty(this, "isSspWidget", false);
73948
73993
  _defineProperty(this, "mediaTimeCount", 0);
73949
73994
  _defineProperty(this, "xlr", {});
73950
73995
  _defineProperty(this, "statsBC", new BroadcastChannel('statsBC'));
@@ -73964,12 +74009,14 @@ var Media = /*#__PURE__*/function () {
73964
74009
  this.duration = parseInt((_this$xml4 = this.xml) === null || _this$xml4 === void 0 ? void 0 : _this$xml4.getAttribute('duration')) || 0;
73965
74010
  this.enableStat = Boolean(((_this$xml5 = this.xml) === null || _this$xml5 === void 0 ? void 0 : _this$xml5.getAttribute('enableStat')) || false);
73966
74011
  this.hasCommandExecuted = false;
74012
+ this.fromDt = ((_this$xml6 = this.xml) === null || _this$xml6 === void 0 ? void 0 : _this$xml6.getAttribute('fromDt')) || '';
74013
+ this.toDt = ((_this$xml7 = this.xml) === null || _this$xml7 === void 0 ? void 0 : _this$xml7.getAttribute('toDt')) || '';
73967
74014
  this.on('start', function (media) {
73968
74015
  if (media.state === MediaState.PLAYING) return;
73969
74016
  media.state = MediaState.PLAYING;
73970
74017
  if (media.mediaType === 'video') {
73971
- var videoMedia = VideoMedia(media, _this.xlr);
73972
- videoMedia.init();
74018
+ media.videoHandler = VideoMedia(media, _this.xlr);
74019
+ media.videoHandler.init();
73973
74020
  if (media.duration > 0) {
73974
74021
  _this.startMediaTimer(media);
73975
74022
  }
@@ -74028,6 +74075,10 @@ var Media = /*#__PURE__*/function () {
74028
74075
  layoutId: media.region.layout.id
74029
74076
  });
74030
74077
  _this.xlr.emitter.emit('widgetEnd', parseInt(media.id));
74078
+ if (_this.isSspWidget) {
74079
+ var _this$sspImpressionUr, _this$sspErrorUrls;
74080
+ _this.xlr.emitter.emit('sspWidgetEnd', (_this$sspImpressionUr = _this.sspImpressionUrls) !== null && _this$sspImpressionUr !== void 0 ? _this$sspImpressionUr : [], (_this$sspErrorUrls = _this.sspErrorUrls) !== null && _this$sspErrorUrls !== void 0 ? _this$sspErrorUrls : [], _this.sspImpressionUrls ? _this.duration : 0);
74081
+ }
74031
74082
  media.region.playNextMedia();
74032
74083
  });
74033
74084
  this.on('cancelled', function (media) {
@@ -74054,6 +74105,10 @@ var Media = /*#__PURE__*/function () {
74054
74105
  layoutId: media.region.layout.id
74055
74106
  });
74056
74107
  _this.xlr.emitter.emit('widgetEnd', parseInt(media.id));
74108
+ if (_this.isSspWidget) {
74109
+ var _this$sspImpressionUr2, _this$sspErrorUrls2;
74110
+ _this.xlr.emitter.emit('sspWidgetEnd', (_this$sspImpressionUr2 = _this.sspImpressionUrls) !== null && _this$sspImpressionUr2 !== void 0 ? _this$sspImpressionUr2 : [], (_this$sspErrorUrls2 = _this.sspErrorUrls) !== null && _this$sspErrorUrls2 !== void 0 ? _this$sspErrorUrls2 : [], _this.sspImpressionUrls ? _this.duration : 0);
74111
+ }
74057
74112
  media.region.playNextMedia();
74058
74113
  });
74059
74114
  // Initialize Media object
@@ -74096,8 +74151,8 @@ var Media = /*#__PURE__*/function () {
74096
74151
  if (media.mediaType === 'video') {
74097
74152
  // Dispose the video media
74098
74153
  console.debug("??? XLR.debug >> VideoMedia::stop - ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has ended playing . . ."));
74099
- if (media.player !== undefined) {
74100
- VideoMedia(media, _this2.xlr).stop(true);
74154
+ if (media.videoHandler !== undefined) {
74155
+ media.videoHandler.stop(true);
74101
74156
  }
74102
74157
  }
74103
74158
  }
@@ -74112,8 +74167,8 @@ var Media = /*#__PURE__*/function () {
74112
74167
  }, {
74113
74168
  key: "init",
74114
74169
  value: function init() {
74115
- var _this$xml6;
74116
- var mediaOptions = (_this$xml6 = this.xml) === null || _this$xml6 === void 0 ? void 0 : _this$xml6.getElementsByTagName('options');
74170
+ var _this$xml8;
74171
+ var mediaOptions = (_this$xml8 = this.xml) === null || _this$xml8 === void 0 ? void 0 : _this$xml8.getElementsByTagName('options');
74117
74172
  if (mediaOptions) {
74118
74173
  for (var _i = 0, _Array$from = Array.from(mediaOptions); _i < _Array$from.length; _i++) {
74119
74174
  var _options = _Array$from[_i];
@@ -74154,22 +74209,33 @@ var Media = /*#__PURE__*/function () {
74154
74209
  if (this.mediaType === 'image' || this.mediaType === 'video') {
74155
74210
  resourceUrlParams.mediaType = this.mediaType;
74156
74211
  }
74157
- var tmpUrl = '';
74158
- if (this.xlr.config.platform === exports.ConsumerPlatform.CMS) {
74159
- tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74160
- } else if (this.xlr.config.platform === exports.ConsumerPlatform.CHROMEOS) {
74161
- tmpUrl = composeResourceUrl(this.xlr.config, resourceUrlParams);
74162
- if (this.mediaType === 'image' || this.mediaType === 'video' || this.mediaType === 'audio') {
74163
- tmpUrl = composeMediaUrl(resourceUrlParams);
74212
+ // SSP widget: URL is not known until the consumer resolves an ad at play-time.
74213
+ // Skip all URL composition and leave url as null.
74214
+ if (this.mediaType === 'ssp') {
74215
+ this.url = null;
74216
+ this.isSspWidget = true;
74217
+ } else {
74218
+ var tmpUrl = '';
74219
+ if (this.xlr.config.platform === exports.ConsumerPlatform.CMS) {
74220
+ tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74221
+ } else if (this.xlr.config.platform === exports.ConsumerPlatform.CHROMEOS) {
74222
+ tmpUrl = composeResourceUrl(this.xlr.config, resourceUrlParams);
74223
+ if (this.mediaType === 'image' || this.mediaType === 'video' || this.mediaType === 'audio') {
74224
+ tmpUrl = composeMediaUrl(resourceUrlParams);
74225
+ // this is an SSP Layout
74226
+ if (this.region.layout.layoutId === -1) {
74227
+ tmpUrl = this.uri;
74228
+ }
74229
+ }
74230
+ } else if (this.xlr.config.platform === exports.ConsumerPlatform.ELECTRON) {
74231
+ tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74164
74232
  // this is an SSP Layout
74165
74233
  if (this.region.layout.layoutId === -1) {
74166
74234
  tmpUrl = this.uri;
74167
74235
  }
74168
74236
  }
74169
- } else if (this.xlr.config.platform === exports.ConsumerPlatform.ELECTRON) {
74170
- tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74237
+ this.url = tmpUrl;
74171
74238
  }
74172
- this.url = tmpUrl;
74173
74239
  // Loop if media has loop, or if region has loop and a single media
74174
74240
  this.loop = this.options['loop'] == '1' || this.region.options['loop'] == '1' && this.region.totalMediaObjects == 1;
74175
74241
  this.html = createMediaElement(this);
@@ -74210,8 +74276,8 @@ var Media = /*#__PURE__*/function () {
74210
74276
  mediaType: _this3.mediaType,
74211
74277
  containerName: _this3.containerName
74212
74278
  });
74213
- var $region = document.querySelector('#' + _this3.region.containerName);
74214
- var $media = $region !== null && $region.querySelector('.' + mediaId);
74279
+ var $region = _this3.region.html;
74280
+ var $media = $region.querySelector('.' + mediaId);
74215
74281
  if (!$media) {
74216
74282
  $media = getNewMedia();
74217
74283
  }
@@ -74275,40 +74341,66 @@ var Media = /*#__PURE__*/function () {
74275
74341
  }
74276
74342
  };
74277
74343
  var getNewMedia = function getNewMedia() {
74278
- var $region = document.getElementById("".concat(_this3.region.containerName));
74344
+ var $region = _this3.region.html;
74279
74345
  // This function is for checking whether
74280
74346
  // the region still has to show a media item
74281
74347
  // when another region is not finished yet
74282
74348
  if (_this3.region.complete && !_this3.region.layout.allEnded) {
74283
74349
  // Add currentMedia to the region
74284
- $region && $region.insertBefore(_this3.html, $region.lastElementChild);
74350
+ $region.insertBefore(_this3.html, $region.lastElementChild);
74285
74351
  return _this3.html;
74286
74352
  }
74287
74353
  return null;
74288
74354
  };
74355
+ // SSP widget: if the consumer did not resolve an ad during the preload window
74356
+ // (i.e. setSspAdUrl was never called), skip this widget and advance normally.
74357
+ if (this.mediaType === 'ssp') {
74358
+ console.debug('??? XLR.debug >> Media.run() > SSP widget: no ad resolved during preload, skipping');
74359
+ this.emitter.emit('end', this);
74360
+ return;
74361
+ }
74289
74362
  showCurrentMedia();
74290
74363
  }
74364
+ }, {
74365
+ key: "setSspAdUrl",
74366
+ value: function setSspAdUrl(url, adMediaType, impressionUrls, errorUrls) {
74367
+ // Ignore if the media has already been skipped or cancelled before the ad arrived.
74368
+ if (this.state !== MediaState.IDLE) {
74369
+ console.debug('??? XLR.debug >> Media::setSspAdUrl - ignoring, media is no longer idle', {
74370
+ state: this.state
74371
+ });
74372
+ return;
74373
+ }
74374
+ // Remove the placeholder <div> so the correct element type can take its place.
74375
+ if (this.html) {
74376
+ this.html.remove();
74377
+ this.html = null;
74378
+ }
74379
+ this.url = url;
74380
+ this.mediaType = adMediaType;
74381
+ this.sspImpressionUrls = impressionUrls;
74382
+ this.sspErrorUrls = errorUrls;
74383
+ // Re-create the element now that mediaType is known, then prepare and append to region DOM.
74384
+ // Visibility and playback are handled by run() when this media's turn comes.
74385
+ this.html = createMediaElement(this);
74386
+ this.region.prepareMedia(this);
74387
+ }
74291
74388
  }, {
74292
74389
  key: "stop",
74293
74390
  value: function () {
74294
74391
  var _stop = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
74295
- var $media;
74296
74392
  return _regeneratorRuntime().wrap(function _callee$(_context) {
74297
74393
  while (1) switch (_context.prev = _context.next) {
74298
74394
  case 0:
74299
- $media = document.getElementById(getMediaId({
74300
- mediaType: this.mediaType,
74301
- containerName: this.containerName
74302
- }));
74303
- if ($media) {
74304
- $media.style.display = 'none';
74305
- $media.remove();
74395
+ if (this.html) {
74396
+ this.html.style.display = 'none';
74397
+ this.html.remove();
74306
74398
  }
74307
74399
  // Release blob URLs for image media to prevent memory leaks on long-running signage
74308
74400
  if (this.mediaType === 'image' && this.url) {
74309
74401
  BlobLoader.release(this.url);
74310
74402
  }
74311
- case 3:
74403
+ case 2:
74312
74404
  case "end":
74313
74405
  return _context.stop();
74314
74406
  }
@@ -74481,18 +74573,30 @@ var Region = /*#__PURE__*/function () {
74481
74573
  this.html = $region;
74482
74574
  /* Parse region media objects */
74483
74575
  var regionMediaItems = Array.from(this.xml.getElementsByTagName('media'));
74484
- this.totalMediaObjects = regionMediaItems.length;
74485
74576
  $layout && $layout.appendChild(this.html);
74486
74577
  Array.from(regionMediaItems).forEach( /*#__PURE__*/function () {
74487
74578
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(mediaXml, indx) {
74488
- var mediaObj;
74579
+ var fromDt, toDt, mediaObj;
74489
74580
  return _regeneratorRuntime().wrap(function _callee$(_context) {
74490
74581
  while (1) switch (_context.prev = _context.next) {
74491
74582
  case 0:
74583
+ fromDt = mediaXml.getAttribute('fromDt') || '';
74584
+ toDt = mediaXml.getAttribute('toDt') || '';
74585
+ if (isMediaActive(fromDt, toDt)) {
74586
+ _context.next = 5;
74587
+ break;
74588
+ }
74589
+ console.debug('??? XLR.debug >> Region::prepareRegion - skipping expired/inactive media', {
74590
+ mediaId: mediaXml.getAttribute('id'),
74591
+ fromDt: fromDt,
74592
+ toDt: toDt
74593
+ });
74594
+ return _context.abrupt("return");
74595
+ case 5:
74492
74596
  mediaObj = new Media(_this, (mediaXml === null || mediaXml === void 0 ? void 0 : mediaXml.getAttribute('id')) || '', mediaXml, _this.options, _this.xlr);
74493
74597
  mediaObj.index = indx;
74494
74598
  _this.mediaObjects.push(mediaObj);
74495
- case 3:
74599
+ case 8:
74496
74600
  case "end":
74497
74601
  return _context.stop();
74498
74602
  }
@@ -74502,6 +74606,7 @@ var Region = /*#__PURE__*/function () {
74502
74606
  return _ref.apply(this, arguments);
74503
74607
  };
74504
74608
  }());
74609
+ this.totalMediaObjects = this.mediaObjects.length;
74505
74610
  console.debug('??? XLR.debug >> Region - done looping through media', {
74506
74611
  mediaObjects: this.mediaObjects
74507
74612
  });
@@ -74532,6 +74637,13 @@ var Region = /*#__PURE__*/function () {
74532
74637
  }, {
74533
74638
  key: "prepareMedia",
74534
74639
  value: function prepareMedia(media) {
74640
+ // SSP widget: signal the consumer to fetch an ad before this media's turn to play.
74641
+ // The consumer calls media.setSspAdUrl() to resolve it; if it never arrives,
74642
+ // Media.run() skips the widget and advances normally.
74643
+ if (media.mediaType === 'ssp') {
74644
+ this.xlr.emitter.emit('sspWidgetRequest', media);
74645
+ return;
74646
+ }
74535
74647
  if (media.mediaType === 'video') {
74536
74648
  prepareVideoMedia(media, this);
74537
74649
  } else if (media.mediaType === 'image' && media.url !== null) {
@@ -74557,6 +74669,23 @@ var Region = /*#__PURE__*/function () {
74557
74669
  key: "prepareNextMedia",
74558
74670
  value: function prepareNextMedia() {
74559
74671
  var nextMediaIndex = (this.currentMediaIndex + 1) % this.totalMediaObjects;
74672
+ // Skip over any media items that are no longer within their active date window
74673
+ var skippedCount = 0;
74674
+ while (skippedCount < this.totalMediaObjects) {
74675
+ var candidate = this.mediaObjects[nextMediaIndex];
74676
+ if (isMediaActive(candidate.fromDt, candidate.toDt)) {
74677
+ break;
74678
+ }
74679
+ skippedCount++;
74680
+ nextMediaIndex = (nextMediaIndex + 1) % this.totalMediaObjects;
74681
+ }
74682
+ // Nothing to pre-load when:
74683
+ // - every item is expired, OR
74684
+ // - the only active item is the one currently playing (skip loop wrapped back to it)
74685
+ if (skippedCount >= this.totalMediaObjects || nextMediaIndex === this.currentMediaIndex) {
74686
+ console.debug('<><> XLR.debug >> [Region::prepareNextMedia()] - no active next media to preload');
74687
+ return;
74688
+ }
74560
74689
  var nextMedia = this.mediaObjects[nextMediaIndex];
74561
74690
  console.debug('<><> XLR.debug >> [Media] - [Region::prepareNextMedia()] - Preparing next media', {
74562
74691
  currentMediaIndex: this.currentMediaIndex,
@@ -74597,9 +74726,21 @@ var Region = /*#__PURE__*/function () {
74597
74726
  }, {
74598
74727
  key: "run",
74599
74728
  value: function run() {
74729
+ var _this$currMedia, _this$oldMedia2;
74600
74730
  console.debug('??? XLR.debug >> Region Called Region::run > ', this.id);
74601
74731
  // Reset region states
74602
74732
  this.reset();
74733
+ // All media were filtered out (all expired/inactive before this run started)
74734
+ if (this.mediaObjects.length === 0) {
74735
+ console.debug('??? XLR.debug >> Region::run - no active media, finishing region', this.id);
74736
+ this.finished();
74737
+ return;
74738
+ }
74739
+ console.debug('??? XLR.debug >> Region Called Region::run - after reset > ', {
74740
+ regionId: this.id,
74741
+ currMedia: (_this$currMedia = this.currMedia) === null || _this$currMedia === void 0 ? void 0 : _this$currMedia.containerName,
74742
+ oldMedia: (_this$oldMedia2 = this.oldMedia) === null || _this$oldMedia2 === void 0 ? void 0 : _this$oldMedia2.containerName
74743
+ });
74603
74744
  if (this.currMedia) {
74604
74745
  this.transitionNodes(this.oldMedia, this.currMedia);
74605
74746
  }
@@ -74648,6 +74789,7 @@ var Region = /*#__PURE__*/function () {
74648
74789
  // Hide oldMedia
74649
74790
  if (oldMedia) {
74650
74791
  var $layout = document.querySelector("#".concat(_this2.layout.containerName, "[data-sequence=\"").concat(_this2.layout.index, "\"]"));
74792
+ if (!$layout) return;
74651
74793
  var $region = $layout.querySelector('#' + _this2.containerName);
74652
74794
  var $oldMedia = $region ? $region.querySelector('.' + getMediaId(oldMedia)) : null;
74653
74795
  if ($oldMedia) {
@@ -74668,7 +74810,7 @@ var Region = /*#__PURE__*/function () {
74668
74810
  $videoWrapper.style.setProperty('visibility', 'hidden');
74669
74811
  $videoWrapper.style.setProperty('z-index', '-999');
74670
74812
  $videoWrapper.style.setProperty('opacity', '0');
74671
- if (oldMedia.player && oldMedia.videoHandler) {
74813
+ if (oldMedia.videoHandler) {
74672
74814
  oldMedia.videoHandler.stop(true);
74673
74815
  }
74674
74816
  }
@@ -74718,7 +74860,12 @@ var Region = /*#__PURE__*/function () {
74718
74860
  }
74719
74861
  };
74720
74862
  if (oldMedia) {
74721
- hideOldMedia();
74863
+ // Skip hiding old media when it is the same object as new media
74864
+ // (single-media loop): removing it would also remove the element
74865
+ // that is about to be shown, leaving the region blank.
74866
+ if (oldMedia !== newMedia) {
74867
+ hideOldMedia();
74868
+ }
74722
74869
  newMedia.run();
74723
74870
  } else {
74724
74871
  newMedia.run();
@@ -74728,13 +74875,13 @@ var Region = /*#__PURE__*/function () {
74728
74875
  }, {
74729
74876
  key: "playNextMedia",
74730
74877
  value: function playNextMedia() {
74731
- var _this$oldMedia2, _this$currMedia, _this$nxtMedia, _this$currMedia2, _this$currMedia3, _this$currMedia4, _this$currMedia5, _this$currMedia6, _this$oldMedia3, _this$currMedia7, _this$nxtMedia2;
74878
+ var _this$oldMedia3, _this$currMedia2, _this$nxtMedia, _this$currMedia3, _this$currMedia4, _this$currMedia5, _this$currMedia6, _this$oldMedia4, _this$currMedia7, _this$nxtMedia2;
74732
74879
  console.debug('??? XLR.debug Region playing next media', {
74733
74880
  regionId: this.id,
74734
74881
  currentMediaIndex: this.currentMediaIndex,
74735
74882
  mediaItemsLn: this.mediaObjects.length,
74736
- oldMedia: (_this$oldMedia2 = this.oldMedia) === null || _this$oldMedia2 === void 0 ? void 0 : _this$oldMedia2.containerName,
74737
- currMedia: (_this$currMedia = this.currMedia) === null || _this$currMedia === void 0 ? void 0 : _this$currMedia.containerName,
74883
+ oldMedia: (_this$oldMedia3 = this.oldMedia) === null || _this$oldMedia3 === void 0 ? void 0 : _this$oldMedia3.containerName,
74884
+ currMedia: (_this$currMedia2 = this.currMedia) === null || _this$currMedia2 === void 0 ? void 0 : _this$currMedia2.containerName,
74738
74885
  nxtMedia: (_this$nxtMedia = this.nxtMedia) === null || _this$nxtMedia === void 0 ? void 0 : _this$nxtMedia.containerName
74739
74886
  });
74740
74887
  /* The current media has finished running */
@@ -74744,33 +74891,20 @@ var Region = /*#__PURE__*/function () {
74744
74891
  });
74745
74892
  return;
74746
74893
  }
74747
- // Are we in a playlist, and has the playlist completed a full cycle?
74748
- var isLastMediaInPlaylist = this.currentMediaIndex === this.mediaObjects.length - 1 && this.mediaObjects.length > 1;
74749
- // If yes, enable shell command widgets again (if any), so they execute on the next playlist cycle
74750
- if (isLastMediaInPlaylist) {
74751
- this.mediaObjects.forEach(function (media) {
74752
- if (media.mediaType === 'shellcommand') {
74753
- // reset per-playlist-cycle execution state
74754
- media.hasCommandExecuted = false;
74755
- }
74756
- });
74757
- }
74758
- if (!this.layout.isOverlay && this.currentMediaIndex === this.mediaObjects.length - 1) {
74759
- this.finished();
74760
- if (this.layout.allEnded) {
74761
- console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74762
- return;
74763
- }
74764
- }
74894
+ // Snapshot the index of the media that just ended so we can detect
74895
+ // cycle completion after the skip loop runs.
74896
+ var origIndex = this.currentMediaIndex;
74765
74897
  // When the region has completed and when currentMedia is html
74766
74898
  // Then, preserve the currentMedia state
74767
- if (this.complete && ((_this$currMedia2 = this.currMedia) === null || _this$currMedia2 === void 0 ? void 0 : _this$currMedia2.render) === 'html') {
74899
+ if (this.complete && ((_this$currMedia3 = this.currMedia) === null || _this$currMedia3 === void 0 ? void 0 : _this$currMedia3.render) === 'html') {
74768
74900
  return;
74769
74901
  }
74770
74902
  // When the region has completed and mediaObjects.length = 1
74771
- // and curMedia.loop = false, then put the media on
74772
- // its current state
74773
- if (this.complete && this.mediaObjects.length === 1 && ((_this$currMedia3 = this.currMedia) === null || _this$currMedia3 === void 0 ? void 0 : _this$currMedia3.render) !== 'html' && (((_this$currMedia4 = this.currMedia) === null || _this$currMedia4 === void 0 ? void 0 : _this$currMedia4.mediaType) === 'image' || ((_this$currMedia5 = this.currMedia) === null || _this$currMedia5 === void 0 ? void 0 : _this$currMedia5.mediaType) === 'video') && !((_this$currMedia6 = this.currMedia) !== null && _this$currMedia6 !== void 0 && _this$currMedia6.loop)) {
74903
+ // and render is not html, preserve the current media state without
74904
+ // calling transitionNodes (which would remove the media from DOM for 1s).
74905
+ // Do NOT restart the media timer here the layout will end naturally when
74906
+ // regionExpired() detects all regions complete on the next cycle.
74907
+ if (this.complete && this.mediaObjects.length === 1 && ((_this$currMedia4 = this.currMedia) === null || _this$currMedia4 === void 0 ? void 0 : _this$currMedia4.render) !== 'html' && (((_this$currMedia5 = this.currMedia) === null || _this$currMedia5 === void 0 ? void 0 : _this$currMedia5.mediaType) === 'image' || ((_this$currMedia6 = this.currMedia) === null || _this$currMedia6 === void 0 ? void 0 : _this$currMedia6.mediaType) === 'video')) {
74774
74908
  return;
74775
74909
  }
74776
74910
  if (this.currMedia) {
@@ -74778,17 +74912,51 @@ var Region = /*#__PURE__*/function () {
74778
74912
  } else {
74779
74913
  this.oldMedia = undefined;
74780
74914
  }
74781
- this.currentMediaIndex = (this.currentMediaIndex + 1) % this.totalMediaObjects;
74915
+ this.currentMediaIndex = (origIndex + 1) % this.totalMediaObjects;
74782
74916
  this.currMedia = this.mediaObjects[this.currentMediaIndex];
74917
+ // Skip media items that are no longer within their active date window
74918
+ var skippedCount = 0;
74919
+ while (this.currMedia && !isMediaActive(this.currMedia.fromDt, this.currMedia.toDt)) {
74920
+ skippedCount++;
74921
+ if (skippedCount >= this.totalMediaObjects) {
74922
+ // Every item in the playlist has expired; finish this region
74923
+ console.debug('??? XLR.debug >> Region::playNextMedia - all media expired, finishing region', this.id);
74924
+ this.finished();
74925
+ return;
74926
+ }
74927
+ this.currentMediaIndex = (this.currentMediaIndex + 1) % this.totalMediaObjects;
74928
+ this.currMedia = this.mediaObjects[this.currentMediaIndex];
74929
+ }
74783
74930
  this.nxtMedia = this.mediaObjects[(this.currentMediaIndex + 1) % this.totalMediaObjects];
74931
+ // A full playlist cycle has been traversed when the total advancement
74932
+ // (the initial +1 step plus any skips over expired items) reaches or
74933
+ // crosses the end of the array.
74934
+ var crossedEnd = origIndex + 1 + skippedCount >= this.totalMediaObjects;
74935
+ // Re-enable shell command widgets at the end of each full cycle so they
74936
+ // execute again on the next pass.
74937
+ if (crossedEnd && this.mediaObjects.length > 1) {
74938
+ this.mediaObjects.forEach(function (media) {
74939
+ if (media.mediaType === 'shellcommand') {
74940
+ // reset per-playlist-cycle execution state
74941
+ media.hasCommandExecuted = false;
74942
+ }
74943
+ });
74944
+ }
74784
74945
  console.debug('??? XLR.debug >> End Region::playNextMedia > execute transitionNodes', {
74785
74946
  regionId: this.id,
74786
74947
  currentMediaIndex: this.currentMediaIndex,
74787
74948
  mediaItemsLn: this.mediaObjects.length,
74788
- oldMedia: (_this$oldMedia3 = this.oldMedia) === null || _this$oldMedia3 === void 0 ? void 0 : _this$oldMedia3.containerName,
74949
+ oldMedia: (_this$oldMedia4 = this.oldMedia) === null || _this$oldMedia4 === void 0 ? void 0 : _this$oldMedia4.containerName,
74789
74950
  currMedia: (_this$currMedia7 = this.currMedia) === null || _this$currMedia7 === void 0 ? void 0 : _this$currMedia7.containerName,
74790
74951
  nxtMedia: (_this$nxtMedia2 = this.nxtMedia) === null || _this$nxtMedia2 === void 0 ? void 0 : _this$nxtMedia2.containerName
74791
74952
  });
74953
+ if (!this.layout.isOverlay && crossedEnd) {
74954
+ this.finished();
74955
+ if (this.layout.allEnded) {
74956
+ console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74957
+ return;
74958
+ }
74959
+ }
74792
74960
  this.transitionNodes(this.oldMedia, this.currMedia);
74793
74961
  }
74794
74962
  }, {
@@ -74817,8 +74985,7 @@ var Region = /*#__PURE__*/function () {
74817
74985
  }, {
74818
74986
  key: "exitTransition",
74819
74987
  value: function exitTransition() {
74820
- /* TODO: Actually implement region exit transitions */
74821
- document.getElementById("".concat(this.containerName));
74988
+ /* TODO: Actually implement region exit transitions using this.html */
74822
74989
  console.debug('Called Region::exitTransition ', this.id);
74823
74990
  this.exitTransitionComplete();
74824
74991
  }
@@ -75118,7 +75285,7 @@ var ActionController = /*#__PURE__*/function () {
75118
75285
  if (dataset.source === 'region') {
75119
75286
  // Try to find the region
75120
75287
  if (regionObj.id === dataset.sourceid) {
75121
- $sourceObj = document.getElementById(regionObj.containerName);
75288
+ $sourceObj = regionObj.html;
75122
75289
  break;
75123
75290
  }
75124
75291
  } else if (dataset.source === 'widget') {
@@ -75127,7 +75294,7 @@ var ActionController = /*#__PURE__*/function () {
75127
75294
  for (var _i2 = 0, _mediaObjects = mediaObjects; _i2 < _mediaObjects.length; _i2++) {
75128
75295
  var mediaObject = _mediaObjects[_i2];
75129
75296
  if (mediaObject.id === dataset.sourceid) {
75130
- $sourceObj = document.getElementById(mediaObject.containerName);
75297
+ $sourceObj = mediaObject.html;
75131
75298
  break;
75132
75299
  }
75133
75300
  }
@@ -75392,7 +75559,11 @@ var Layout = /*#__PURE__*/function () {
75392
75559
  this.on('start', function (layout) {
75393
75560
  layout.done = false;
75394
75561
  layout.state = exports.ELayoutState.RUNNING;
75395
- console.debug('>>>> XLR.debug Layout start emitted > Layout ID > ', layout.id);
75562
+ console.debug('>>>> XLR.debug Layout start emitted > Layout > ', {
75563
+ layoutId: layout.id,
75564
+ layoutIndex: layout.index,
75565
+ layoutState: layout.state
75566
+ });
75396
75567
  // Check if stats are enabled for the layout
75397
75568
  if (layout.enableStat) {
75398
75569
  _this.statsBC.postMessage({
@@ -75413,12 +75584,21 @@ var Layout = /*#__PURE__*/function () {
75413
75584
  while (1) switch (_context2.prev = _context2.next) {
75414
75585
  case 0:
75415
75586
  if (!(layout.state === exports.ELayoutState.CANCELLED)) {
75416
- _context2.next = 2;
75587
+ _context2.next = 3;
75417
75588
  break;
75418
75589
  }
75590
+ console.debug('>>>> XLR.debug Layout end emitted but layout is already cancelled > Layout ID > ', {
75591
+ layoutId: layout.id,
75592
+ layoutIndex: layout.index,
75593
+ layoutState: layout.state
75594
+ });
75419
75595
  return _context2.abrupt("return");
75420
- case 2:
75421
- console.debug('>>>> XLR.debug Ending layout with ID of > ', layout.layoutId);
75596
+ case 3:
75597
+ console.debug('>>>> XLR.debug Ending layout', {
75598
+ layoutId: layout.id,
75599
+ layoutIndex: layout.index,
75600
+ layoutState: layout.state
75601
+ });
75422
75602
  /* Remove layout that has ended */
75423
75603
  $layout = document.querySelector("#".concat(layout.containerName, "[data-sequence=\"").concat(layout.index, "\"]")); // Only update layout.state when last state === RUNNING
75424
75604
  if (layout.state === exports.ELayoutState.RUNNING) {
@@ -75429,6 +75609,8 @@ var Layout = /*#__PURE__*/function () {
75429
75609
  console.debug('>>> XLR.debug Layout end emitted > Layout ID > ', {
75430
75610
  $layout: $layout,
75431
75611
  layoutId: layout.id,
75612
+ layoutIndex: layout.index,
75613
+ layoutState: layout.state,
75432
75614
  isOverlay: layout.isOverlay,
75433
75615
  isInterrupt: layout.isInterrupt()
75434
75616
  });
@@ -75455,9 +75637,9 @@ var Layout = /*#__PURE__*/function () {
75455
75637
  }
75456
75638
  // Emit layout end event
75457
75639
  console.debug('>>>>> XLR.debug Awaited XLR::emitSync > End - Calling layoutEnd event');
75458
- _context2.next = 12;
75640
+ _context2.next = 13;
75459
75641
  return layout.xlr.emitSync('layoutEnd', layout);
75460
- case 12:
75642
+ case 13:
75461
75643
  if (_this.xlr.config.platform !== exports.ConsumerPlatform.CMS && layout.inLoop) {
75462
75644
  // Transition next layout to current layout and prepare next layout if exist
75463
75645
  _this.xlr.prepareLayouts().then( /*#__PURE__*/function () {
@@ -75487,7 +75669,7 @@ var Layout = /*#__PURE__*/function () {
75487
75669
  };
75488
75670
  }());
75489
75671
  }
75490
- case 13:
75672
+ case 14:
75491
75673
  case "end":
75492
75674
  return _context2.stop();
75493
75675
  }
@@ -75500,6 +75682,34 @@ var Layout = /*#__PURE__*/function () {
75500
75682
  this.on('cancelled', function (layout) {
75501
75683
  console.debug('>>>>> XLR.debug / Layout cancelled > Layout ID > ', layout.id);
75502
75684
  layout.state = exports.ELayoutState.CANCELLED;
75685
+ layout.inLoop = false;
75686
+ // Dispose video handlers immediately so their stall watchdogs and error
75687
+ // callbacks can't fire against a layout whose DOM is about to be removed.
75688
+ var _iterator = _createForOfIteratorHelper(layout.regions),
75689
+ _step;
75690
+ try {
75691
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
75692
+ var region = _step.value;
75693
+ var _iterator2 = _createForOfIteratorHelper(region.mediaObjects),
75694
+ _step2;
75695
+ try {
75696
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
75697
+ var media = _step2.value;
75698
+ if (media.videoHandler) {
75699
+ media.videoHandler.stop(true);
75700
+ }
75701
+ }
75702
+ } catch (err) {
75703
+ _iterator2.e(err);
75704
+ } finally {
75705
+ _iterator2.f();
75706
+ }
75707
+ }
75708
+ } catch (err) {
75709
+ _iterator.e(err);
75710
+ } finally {
75711
+ _iterator.f();
75712
+ }
75503
75713
  });
75504
75714
  }
75505
75715
  return _createClass(Layout, [{
@@ -75641,6 +75851,14 @@ var Layout = /*#__PURE__*/function () {
75641
75851
  if ($splashScreen) {
75642
75852
  $splashScreen.style.display = 'none';
75643
75853
  }
75854
+ // Check if $layoutContainer is still in the DOM before trying to play regions, as the layout may have been removed while waiting for a long-running region to finish.
75855
+ console.debug('??? XLR.debug >> Layout::run() - Checking if layout container is still in the DOM before playing regions...', {
75856
+ layoutId: this.id,
75857
+ layoutContainerExists: !!$layoutContainer,
75858
+ $layoutContainer: $layoutContainer,
75859
+ layoutIndex: this.index,
75860
+ shouldParse: false
75861
+ });
75644
75862
  if ($layoutContainer) {
75645
75863
  $layoutContainer.style.setProperty('visibility', 'visible');
75646
75864
  $layoutContainer.style.setProperty('opacity', '1');
@@ -75671,19 +75889,19 @@ var Layout = /*#__PURE__*/function () {
75671
75889
  key: "regionExpired",
75672
75890
  value: function regionExpired() {
75673
75891
  this.allExpired = true;
75674
- var _iterator = _createForOfIteratorHelper(this.regions),
75675
- _step;
75892
+ var _iterator3 = _createForOfIteratorHelper(this.regions),
75893
+ _step3;
75676
75894
  try {
75677
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
75678
- var layoutRegion = _step.value;
75895
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
75896
+ var layoutRegion = _step3.value;
75679
75897
  if (!layoutRegion.complete) {
75680
75898
  this.allExpired = false;
75681
75899
  }
75682
75900
  }
75683
75901
  } catch (err) {
75684
- _iterator.e(err);
75902
+ _iterator3.e(err);
75685
75903
  } finally {
75686
- _iterator.f();
75904
+ _iterator3.f();
75687
75905
  }
75688
75906
  if (this.allExpired) {
75689
75907
  this.end();
@@ -75694,17 +75912,17 @@ var Layout = /*#__PURE__*/function () {
75694
75912
  value: function end() {
75695
75913
  console.debug('Executing Layout::end and Calling Region::end ', this);
75696
75914
  /* Ask the layout to gracefully stop running now */
75697
- var _iterator2 = _createForOfIteratorHelper(this.regions),
75698
- _step2;
75915
+ var _iterator4 = _createForOfIteratorHelper(this.regions),
75916
+ _step4;
75699
75917
  try {
75700
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
75701
- var layoutRegion = _step2.value;
75918
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
75919
+ var layoutRegion = _step4.value;
75702
75920
  layoutRegion.end();
75703
75921
  }
75704
75922
  } catch (err) {
75705
- _iterator2.e(err);
75923
+ _iterator4.e(err);
75706
75924
  } finally {
75707
- _iterator2.f();
75925
+ _iterator4.f();
75708
75926
  }
75709
75927
  }
75710
75928
  }, {
@@ -75717,7 +75935,11 @@ var Layout = /*#__PURE__*/function () {
75717
75935
  }
75718
75936
  }
75719
75937
  if (this.allEnded) {
75720
- console.debug('starting to end layout . . .');
75938
+ console.debug('starting to end layout . . .', {
75939
+ layoutId: this.layoutId,
75940
+ layoutIndex: this.index,
75941
+ layoutState: this.state
75942
+ });
75721
75943
  if (this.xlr.config.platform === exports.ConsumerPlatform.CMS) {
75722
75944
  var $end = document.getElementById('play_ended');
75723
75945
  var $preview = document.getElementById('screen_container');
@@ -75816,6 +76038,42 @@ var Layout = /*#__PURE__*/function () {
75816
76038
  (_$layout$parentElemen2 = $layout.parentElement) === null || _$layout$parentElemen2 === void 0 || _$layout$parentElemen2.removeChild($layout);
75817
76039
  }
75818
76040
  }
76041
+ }, {
76042
+ key: "discardLayout",
76043
+ value: function discardLayout() {
76044
+ var caller = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : exports.LayoutPlaybackType.NEXT;
76045
+ // Dispose any video.js players that were initialized during prepareVideoMedia
76046
+ // but never played. The isDisposed() guard makes this safe to call on
76047
+ // layouts that were fully played as well.
76048
+ var _iterator5 = _createForOfIteratorHelper(this.regions),
76049
+ _step5;
76050
+ try {
76051
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
76052
+ var region = _step5.value;
76053
+ var _iterator6 = _createForOfIteratorHelper(region.mediaObjects),
76054
+ _step6;
76055
+ try {
76056
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
76057
+ var media = _step6.value;
76058
+ if (media.player && !media.player.isDisposed()) {
76059
+ media.player.dispose();
76060
+ media.player = undefined;
76061
+ media.html = null;
76062
+ }
76063
+ }
76064
+ } catch (err) {
76065
+ _iterator6.e(err);
76066
+ } finally {
76067
+ _iterator6.f();
76068
+ }
76069
+ }
76070
+ } catch (err) {
76071
+ _iterator5.e(err);
76072
+ } finally {
76073
+ _iterator5.f();
76074
+ }
76075
+ this.removeLayout(caller);
76076
+ }
75819
76077
  }, {
75820
76078
  key: "getXlf",
75821
76079
  value: function getXlf() {
@@ -76058,6 +76316,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76058
76316
  }());
76059
76317
  xlrObject.on('updateLoop', /*#__PURE__*/function () {
76060
76318
  var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(inputLayouts) {
76319
+ var _xlrObject$currentLay;
76061
76320
  return _regeneratorRuntime().wrap(function _callee3$(_context3) {
76062
76321
  while (1) switch (_context3.prev = _context3.next) {
76063
76322
  case 0:
@@ -76066,7 +76325,17 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76066
76325
  return xlrObject.updateLoop(inputLayouts);
76067
76326
  case 3:
76068
76327
  xlrObject.isUpdatingLoop = false;
76069
- case 4:
76328
+ // If the running layout finished while isUpdatingLoop was true, the
76329
+ // layout end-handler bailed out of prepareLayouts() early and the
76330
+ // subsequent playLayouts() call saw currentLayout.done === true and
76331
+ // skipped run() — leaving a black screen. Catch up now that the flag
76332
+ // is clear.
76333
+ if ((_xlrObject$currentLay = xlrObject.currentLayout) !== null && _xlrObject$currentLay !== void 0 && _xlrObject$currentLay.done) {
76334
+ xlrObject.prepareLayouts().then(function (xlr) {
76335
+ return xlrObject.playLayouts(xlr);
76336
+ });
76337
+ }
76338
+ case 5:
76070
76339
  case "end":
76071
76340
  return _context3.stop();
76072
76341
  }
@@ -76096,40 +76365,35 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76096
76365
  return _ref4.apply(this, arguments);
76097
76366
  };
76098
76367
  }());
76099
- /**
76100
- * Asynchronous event emitter. Extended nanoevents event emitter.
76101
- *
76102
- * NOTE: Known limitation — nanoevents emit() is synchronous.
76103
- * Any async event handlers registered via .on() are fire-and-forget;
76104
- * emitSync does NOT await them. The returned Promise resolves
76105
- * immediately after handlers are invoked, not after they complete.
76106
- *
76107
- * @param eventName
76108
- * @param args
76109
- */
76110
- xlrObject.emitSync = function (eventName) {
76111
- for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
76112
- args[_key - 1] = arguments[_key];
76113
- }
76114
- return new Promise( /*#__PURE__*/function () {
76115
- var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(resolve) {
76116
- var _xlrObject$emitter;
76117
- return _regeneratorRuntime().wrap(function _callee5$(_context5) {
76118
- while (1) switch (_context5.prev = _context5.next) {
76119
- case 0:
76120
- (_xlrObject$emitter = xlrObject.emitter).emit.apply(_xlrObject$emitter, [eventName].concat(args));
76121
- resolve();
76122
- case 2:
76123
- case "end":
76124
- return _context5.stop();
76125
- }
76126
- }, _callee5);
76127
- }));
76128
- return function (_x4) {
76129
- return _ref5.apply(this, arguments);
76130
- };
76131
- }());
76132
- };
76368
+ xlrObject.emitSync = /*#__PURE__*/function () {
76369
+ var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(eventName) {
76370
+ var _xlrObject$emitter$ev;
76371
+ var _len,
76372
+ args,
76373
+ _key,
76374
+ handlers,
76375
+ _args5 = arguments;
76376
+ return _regeneratorRuntime().wrap(function _callee5$(_context5) {
76377
+ while (1) switch (_context5.prev = _context5.next) {
76378
+ case 0:
76379
+ for (_len = _args5.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
76380
+ args[_key - 1] = _args5[_key];
76381
+ }
76382
+ handlers = (_xlrObject$emitter$ev = xlrObject.emitter.events[eventName]) !== null && _xlrObject$emitter$ev !== void 0 ? _xlrObject$emitter$ev : [];
76383
+ _context5.next = 4;
76384
+ return Promise.all(handlers.map(function (handler) {
76385
+ return handler.apply(void 0, args);
76386
+ }));
76387
+ case 4:
76388
+ case "end":
76389
+ return _context5.stop();
76390
+ }
76391
+ }, _callee5);
76392
+ }));
76393
+ return function (_x4) {
76394
+ return _ref5.apply(this, arguments);
76395
+ };
76396
+ }();
76133
76397
  xlrObject.bootstrap = function () {
76134
76398
  // Place to set configurations and initialize required props
76135
76399
  var self = this;
@@ -76164,12 +76428,21 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76164
76428
  if ($splashScreen && $splashScreen.style.display === 'block') {
76165
76429
  $splashScreen === null || $splashScreen === void 0 || $splashScreen.hide();
76166
76430
  }
76431
+ console.debug('>>>> XLR.debug XLR::playLayouts > currentLayout', {
76432
+ layoutId: xlr.currentLayout.layoutId,
76433
+ layoutIndex: xlr.currentLayout.index,
76434
+ layoutState: xlr.currentLayout.state
76435
+ });
76167
76436
  if (!xlr.currentLayout.done) {
76168
76437
  // Hide overlays when current layout is interrupt
76169
76438
  if (xlr.currentLayout.isInterrupt()) {
76170
76439
  xlrObject.overlayLayoutManager.stopOverlays();
76171
76440
  }
76172
- console.log('>>>> XLR.debug XLR::playSchedules > Running currentLayout', xlr.currentLayout);
76441
+ console.debug('>>>> XLR.debug XLR::playLayouts > Running currentLayout', {
76442
+ layoutId: xlr.currentLayout.layoutId,
76443
+ layoutIndex: xlr.currentLayout.index,
76444
+ layoutState: xlr.currentLayout.state
76445
+ });
76173
76446
  xlr.currentLayout.run();
76174
76447
  }
76175
76448
  } else {
@@ -76247,7 +76520,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76247
76520
  _context7.next = 10;
76248
76521
  return _overlay.finishAllRegions();
76249
76522
  case 10:
76250
- _overlay.removeLayout();
76523
+ _overlay.removeLayout(exports.LayoutPlaybackType.OVERLAY);
76251
76524
  case 11:
76252
76525
  _context7.next = 14;
76253
76526
  break;
@@ -76313,6 +76586,31 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76313
76586
  var $layout = document.querySelector("#".concat(containerName, "[data-sequence=\"").concat(layoutIndex, "\"]"));
76314
76587
  return $layout !== null;
76315
76588
  };
76589
+ // Scans screen_container for non-overlay layout divs and removes any that
76590
+ // are not the current or next active layout. Prevents DOM accumulation when
76591
+ // prepareLayouts() races with updateLoop and multiple same-layoutId elements
76592
+ // end up in screen_container (e.g. transitioning from a 1-layout loop where
76593
+ // two elements exist for the same layout to a multi-layout schedule).
76594
+ // keepCurrent / keepNext:
76595
+ // undefined → fall back to this.currentLayout / this.nextLayout
76596
+ // null → keep nothing for that slot (explicit "no layout to preserve")
76597
+ // ILayout → keep exactly that instance
76598
+ xlrObject.cleanupOrphanedLayouts = function (keepCurrent, keepNext) {
76599
+ var $screen = document.getElementById('screen_container');
76600
+ if (!$screen) return;
76601
+ var current = keepCurrent !== undefined ? keepCurrent : this.currentLayout;
76602
+ var next = keepNext !== undefined ? keepNext : this.nextLayout;
76603
+ Array.from($screen.querySelectorAll(':scope > div:not(.is-overlay)')).forEach(function (el) {
76604
+ var div = el;
76605
+ var isCurrentLayout = current && div.id === current.containerName && div.dataset.sequence === String(current.index);
76606
+ var isNextLayout = next && div.id === next.containerName && div.dataset.sequence === String(next.index);
76607
+ if (!isCurrentLayout && !isNextLayout) {
76608
+ var _div$parentElement;
76609
+ console.debug('XLR::cleanupOrphanedLayouts - removing orphaned layout element', div.id);
76610
+ (_div$parentElement = div.parentElement) === null || _div$parentElement === void 0 || _div$parentElement.removeChild(div);
76611
+ }
76612
+ });
76613
+ };
76316
76614
  xlrObject.updateLoop = /*#__PURE__*/function () {
76317
76615
  var _ref10 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee11(inputLayouts) {
76318
76616
  var _this$currentLayout,
@@ -76359,11 +76657,11 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76359
76657
  };
76360
76658
  }();
76361
76659
  if (isCurrentLayoutValid) {
76362
- _context11.next = 54;
76660
+ _context11.next = 55;
76363
76661
  break;
76364
76662
  }
76365
76663
  if (!playback.hasDefaultOnly) {
76366
- _context11.next = 33;
76664
+ _context11.next = 34;
76367
76665
  break;
76368
76666
  }
76369
76667
  if (!(this.currentLayout && playback.currentLayout && this.currentLayout.layoutId !== playback.currentLayout.layoutId)) {
@@ -76376,82 +76674,102 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76376
76674
  case 19:
76377
76675
  this.currentLayout.removeLayout();
76378
76676
  case 20:
76379
- _context11.next = 22;
76677
+ // Discard old nextLayout before replacing it — same as the
76678
+ // other two branches do, otherwise the prepared DOM element
76679
+ // and any video.js players are orphaned.
76680
+ if (this.nextLayout && this.isLayoutInDOM(this.nextLayout.containerName, this.nextLayout.index)) {
76681
+ this.nextLayout.discardLayout(exports.LayoutPlaybackType.NEXT);
76682
+ }
76683
+ _context11.next = 23;
76380
76684
  return this.prepareLayoutXlf(playback.currentLayout);
76381
- case 22:
76685
+ case 23:
76382
76686
  this.currentLayout = _context11.sent;
76383
76687
  this.currentLayoutId = this.currentLayout.layoutId;
76384
76688
  _context11.t0 = this;
76385
- _context11.next = 27;
76689
+ _context11.next = 28;
76386
76690
  return this.prepareLayoutXlf(playback.nextLayout);
76387
- case 27:
76691
+ case 28:
76388
76692
  _context11.t1 = _context11.sent;
76389
- _context11.next = 30;
76693
+ _context11.next = 31;
76390
76694
  return _context11.t0.prepareForSsp.call(_context11.t0, _context11.t1);
76391
- case 30:
76695
+ case 31:
76392
76696
  this.nextLayout = _context11.sent;
76393
- _context11.next = 50;
76697
+ _context11.next = 51;
76394
76698
  break;
76395
- case 33:
76699
+ case 34:
76396
76700
  if (!(this.currentLayout && this.isLayoutInDOM(this.currentLayout.containerName, this.currentLayout.index))) {
76397
- _context11.next = 38;
76701
+ _context11.next = 39;
76398
76702
  break;
76399
76703
  }
76400
76704
  this.currentLayout.inLoop = false;
76401
- _context11.next = 37;
76705
+ _context11.next = 38;
76402
76706
  return this.currentLayout.finishAllRegions();
76403
- case 37:
76404
- this.currentLayout.removeLayout();
76405
76707
  case 38:
76708
+ this.currentLayout.removeLayout();
76709
+ case 39:
76406
76710
  if (this.nextLayout && this.isLayoutInDOM(this.nextLayout.containerName, this.nextLayout.index)) {
76407
- this.nextLayout.removeLayout();
76711
+ this.nextLayout.discardLayout(exports.LayoutPlaybackType.NEXT);
76408
76712
  }
76409
76713
  if (!playback.currentLayout) {
76410
- _context11.next = 42;
76714
+ _context11.next = 43;
76411
76715
  break;
76412
76716
  }
76413
- _context11.next = 42;
76717
+ _context11.next = 43;
76414
76718
  return prepareNewCurrentLayout();
76415
- case 42:
76719
+ case 43:
76416
76720
  if (!playback.nextLayout) {
76417
- _context11.next = 50;
76721
+ _context11.next = 51;
76418
76722
  break;
76419
76723
  }
76420
76724
  _context11.t2 = this;
76421
- _context11.next = 46;
76725
+ _context11.next = 47;
76422
76726
  return this.prepareLayoutXlf(playback.nextLayout);
76423
- case 46:
76727
+ case 47:
76424
76728
  _context11.t3 = _context11.sent;
76425
- _context11.next = 49;
76729
+ _context11.next = 50;
76426
76730
  return _context11.t2.prepareForSsp.call(_context11.t2, _context11.t3);
76427
- case 49:
76428
- this.nextLayout = _context11.sent;
76429
76731
  case 50:
76430
- _context11.next = 52;
76732
+ this.nextLayout = _context11.sent;
76733
+ case 51:
76734
+ _context11.next = 53;
76431
76735
  return this.playSchedules(this);
76432
- case 52:
76736
+ case 53:
76433
76737
  _context11.next = 67;
76434
76738
  break;
76435
- case 54:
76739
+ case 55:
76436
76740
  // Remove next layout if it is in the DOM
76437
76741
  if (this.nextLayout && this.isLayoutInDOM(this.nextLayout.containerName, this.nextLayout.index)) {
76438
- this.nextLayout.removeLayout();
76742
+ this.nextLayout.discardLayout(exports.LayoutPlaybackType.NEXT);
76439
76743
  }
76440
- // Prepare new current layout
76744
+ // Purge any other orphaned layouts from screen_container that belong
76745
+ // to the old single-layout loop. When there was only one layout in the
76746
+ // loop, prepareLayouts() kept two DOM elements alive (current + next,
76747
+ // both the same layoutId but different containerNames). On a schedule
76748
+ // change the this.nextLayout check above only discards the element
76749
+ // currently referenced by this.nextLayout, but concurrent
76750
+ // prepareLayouts() calls can leave earlier same-layoutId elements
76751
+ // behind.
76752
+ // Pass null (not undefined) for keepNext: undefined would fall back to
76753
+ // this.nextLayout which may still reference the just-discarded layout
76754
+ // or — if isLayoutInDOM returned false and discardLayout was skipped —
76755
+ // the orphan itself, causing cleanupOrphanedLayouts to preserve it.
76756
+ // null means "no next to keep"; we are about to prepare a fresh one.
76757
+ this.cleanupOrphanedLayouts(this.currentLayout, null);
76758
+ // The current layout is still valid and running — do NOT replace the
76759
+ // live currentLayout object. Only refresh the queued nextLayout so
76760
+ // that when the running layout finishes it transitions to the correct
76761
+ // position in the updated loop. Using playback.currentLayout (the
76762
+ // slot that follows the running layout in the new queue) as the new
76763
+ // nextLayout keeps the cycle in order; the slot after that will be
76764
+ // prepared by the normal prepareLayouts() call at transition time.
76441
76765
  if (!playback.currentLayout) {
76442
- _context11.next = 58;
76443
- break;
76444
- }
76445
- _context11.next = 58;
76446
- return prepareNewCurrentLayout();
76447
- case 58:
76448
- if (!playback.nextLayout) {
76449
76766
  _context11.next = 66;
76450
76767
  break;
76451
76768
  }
76769
+ this.currentLayoutIndex = playback.currentLayoutIndex;
76452
76770
  _context11.t4 = this;
76453
76771
  _context11.next = 62;
76454
- return this.prepareLayoutXlf(playback.nextLayout);
76772
+ return this.prepareLayoutXlf(playback.currentLayout);
76455
76773
  case 62:
76456
76774
  _context11.t5 = _context11.sent;
76457
76775
  _context11.next = 65;
@@ -76557,8 +76875,21 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76557
76875
  _nextLayout = setLayoutIndex(_nextLayout, _nextLayoutIndex);
76558
76876
  }
76559
76877
  } else {
76878
+ var _this$currentLayout4, _this$currentLayout5;
76560
76879
  _currentLayout = this.nextLayout;
76561
76880
  _currentLayoutIndex = _currentLayout.index;
76881
+ // updateLoop can re-queue the same index that is currently
76882
+ // playing (e.g. it fires while nextLayout.index === currentLayout.index).
76883
+ // When that layout then ends, the catch-up prepareLayouts() would
76884
+ // replay the same slot instead of advancing. Detect this by checking
76885
+ // whether the queued next-to-current is at the same index as the
76886
+ // layout that just finished, and advance past it so the following
76887
+ // slot (e.g. an SSP that now has an ad) becomes current instead.
76888
+ if (this.inputLayouts.length > 1 && (_this$currentLayout4 = this.currentLayout) !== null && _this$currentLayout4 !== void 0 && _this$currentLayout4.done && _currentLayoutIndex === ((_this$currentLayout5 = this.currentLayout) === null || _this$currentLayout5 === void 0 ? void 0 : _this$currentLayout5.index)) {
76889
+ _currentLayoutIndex = (_currentLayoutIndex + 1) % this.inputLayouts.length;
76890
+ _currentLayout = this.getLayout(this.inputLayouts[_currentLayoutIndex]);
76891
+ _currentLayout = setLayoutIndex(_currentLayout, _currentLayoutIndex);
76892
+ }
76562
76893
  _nextLayoutIndex = (_currentLayoutIndex + 1) % this.inputLayouts.length;
76563
76894
  _nextLayout = this.getLayout(this.inputLayouts[_nextLayoutIndex]);
76564
76895
  _nextLayout = setLayoutIndex(_nextLayout, _nextLayoutIndex);
@@ -76614,6 +76945,10 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76614
76945
  var activeLayout = inputLayout;
76615
76946
  if (isCMS) {
76616
76947
  activeLayout.index = 0;
76948
+ // id stays null without this — setLayoutIndex returns undefined for CMS layouts
76949
+ if (activeLayout.id == null) {
76950
+ activeLayout.id = activeLayout.layoutId;
76951
+ }
76617
76952
  } else {
76618
76953
  activeLayout = _objectSpread2({}, this.uniqueLayouts[inputLayout.layoutId]);
76619
76954
  }
@@ -76644,7 +76979,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76644
76979
  };
76645
76980
  xlrObject.prepareLayouts = /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee14() {
76646
76981
  var _layoutPlayback$curre, _layoutPlayback$curre2;
76647
- var self, layoutPlayback, currentLayoutXlf, nextLayoutXlf, layouts;
76982
+ var self, layoutPlayback, currentLayoutXlf, wasCurrentReused, nextLayoutXlf, layouts;
76648
76983
  return _regeneratorRuntime().wrap(function _callee14$(_context14) {
76649
76984
  while (1) switch (_context14.prev = _context14.next) {
76650
76985
  case 0:
@@ -76667,7 +77002,12 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76667
77002
  shouldParse: false
76668
77003
  });
76669
77004
  self.currentLayoutId = (_layoutPlayback$curre = layoutPlayback.currentLayout) === null || _layoutPlayback$curre === void 0 ? void 0 : _layoutPlayback$curre.layoutId;
76670
- if (!((_layoutPlayback$curre2 = layoutPlayback.currentLayout) !== null && _layoutPlayback$curre2 !== void 0 && _layoutPlayback$curre2.layoutNode)) {
77005
+ // Only reuse the existing Layout instance if it is fully healthy
77006
+ // a done=true instance was removed from the DOM (e.g. an SSP slot that
77007
+ // had no ad), and an empty-XLF instance has no regions so it can never
77008
+ // advance the cycle. In either case re-prepare from scratch so we get
77009
+ // a fresh request (which may now have a valid ad / XLF).
77010
+ if (!((_layoutPlayback$curre2 = layoutPlayback.currentLayout) !== null && _layoutPlayback$curre2 !== void 0 && _layoutPlayback$curre2.layoutNode && !layoutPlayback.currentLayout.done && layoutPlayback.currentLayout.xlfString !== '')) {
76671
77011
  _context14.next = 12;
76672
77012
  break;
76673
77013
  }
@@ -76681,27 +77021,40 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76681
77021
  _context14.t0 = _context14.sent;
76682
77022
  case 15:
76683
77023
  currentLayoutXlf = _context14.t0;
76684
- _context14.next = 18;
77024
+ // True when the same object was returned (reused); false when a fresh
77025
+ // Layout was constructed by prepareLayoutXlf above.
77026
+ wasCurrentReused = currentLayoutXlf === layoutPlayback.currentLayout;
77027
+ _context14.next = 19;
76685
77028
  return self.prepareLayoutXlf(layoutPlayback.nextLayout);
76686
- case 18:
77029
+ case 19:
76687
77030
  nextLayoutXlf = _context14.sent;
76688
77031
  _context14.t1 = Promise;
76689
77032
  _context14.t2 = currentLayoutXlf;
76690
- _context14.next = 23;
77033
+ _context14.next = 24;
76691
77034
  return self.prepareForSsp(nextLayoutXlf);
76692
- case 23:
77035
+ case 24:
76693
77036
  _context14.t3 = _context14.sent;
76694
77037
  _context14.t4 = [_context14.t2, _context14.t3];
76695
- _context14.next = 27;
77038
+ _context14.next = 28;
76696
77039
  return _context14.t1.all.call(_context14.t1, _context14.t4);
76697
- case 27:
77040
+ case 28:
76698
77041
  layouts = _context14.sent;
76699
- // Return early when layout loop is updating
76700
- if (self.isUpdatingLoop) {
76701
- if (layoutPlayback.nextLayout && nextLayoutXlf && this.isLayoutInDOM(nextLayoutXlf.containerName, nextLayoutXlf.index)) {
76702
- nextLayoutXlf.removeLayout();
76703
- }
77042
+ if (!(self.isUpdatingLoop || layouts[0].done)) {
77043
+ _context14.next = 33;
77044
+ break;
77045
+ }
77046
+ // If currentLayout was freshly prepared (not reused from nextLayout),
77047
+ // its DOM element was just appended — discard it now so it does not
77048
+ // accumulate in screen_container. Also disposes any video.js players
77049
+ // that were initialized during prepareVideoMedia but never played.
77050
+ if (!wasCurrentReused && this.isLayoutInDOM(currentLayoutXlf.containerName, currentLayoutXlf.index)) {
77051
+ currentLayoutXlf.discardLayout(exports.LayoutPlaybackType.NEXT);
76704
77052
  }
77053
+ if (layoutPlayback.nextLayout && nextLayoutXlf && this.isLayoutInDOM(nextLayoutXlf.containerName, nextLayoutXlf.index)) {
77054
+ nextLayoutXlf.discardLayout(exports.LayoutPlaybackType.NEXT);
77055
+ }
77056
+ return _context14.abrupt("return", Promise.resolve(self));
77057
+ case 33:
76705
77058
  console.debug('>>>>> XLR.debug prepared layout XLF', layouts);
76706
77059
  return _context14.abrupt("return", new Promise( /*#__PURE__*/function () {
76707
77060
  var _ref14 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee13(resolve) {
@@ -76718,8 +77071,15 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76718
77071
  self.currentLayout = self.layouts.current;
76719
77072
  self.currentLayoutId = self.currentLayout.layoutId;
76720
77073
  self.nextLayout = self.layouts.next;
77074
+ // Evict any orphaned layout DOM elements that aren't the current
77075
+ // or next layout. Concurrent prepareLayouts() calls can each append
77076
+ // a freshly-prepared nextLayout to screen_container and then
77077
+ // overwrite this.nextLayout, leaving earlier elements behind.
77078
+ // Calling this here — with explicit references — ensures every
77079
+ // completed prepare cycle leaves the DOM in a clean state.
77080
+ self.cleanupOrphanedLayouts(self.currentLayout, self.nextLayout);
76721
77081
  resolve(xlrObject);
76722
- case 8:
77082
+ case 9:
76723
77083
  case "end":
76724
77084
  return _context13.stop();
76725
77085
  }
@@ -76729,7 +77089,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76729
77089
  return _ref14.apply(this, arguments);
76730
77090
  };
76731
77091
  }()));
76732
- case 31:
77092
+ case 35:
76733
77093
  case "end":
76734
77094
  return _context14.stop();
76735
77095
  }
@@ -76757,34 +77117,38 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76757
77117
  newOptions.xlfUrl = inputLayout.path;
76758
77118
  }
76759
77119
  if (!(inputLayout && inputLayout.layoutNode === undefined)) {
76760
- _context15.next = 21;
77120
+ _context15.next = 22;
76761
77121
  break;
76762
77122
  }
76763
77123
  if (!(inputLayout.layoutId === -1)) {
76764
- _context15.next = 14;
77124
+ _context15.next = 15;
76765
77125
  break;
76766
77126
  }
76767
77127
  _context15.next = 10;
76768
77128
  return self.emitSync('adRequest', inputLayout.index);
76769
77129
  case 10:
76770
77130
  sspInputLayout = self.inputLayouts[inputLayout.index];
77131
+ console.debug('XLR::prepareLayoutXlf > SSP input layout', {
77132
+ sspInputLayout: sspInputLayout,
77133
+ inputLayout: inputLayout
77134
+ });
76771
77135
  // @ts-ignore
76772
- layoutXlf = ((_sspInputLayout = sspInputLayout) === null || _sspInputLayout === void 0 ? void 0 : _sspInputLayout.getXlf()) || '';
76773
- _context15.next = 17;
77136
+ layoutXlf = typeof ((_sspInputLayout = sspInputLayout) === null || _sspInputLayout === void 0 ? void 0 : _sspInputLayout.getXlf) === 'function' ? sspInputLayout.getXlf() : '';
77137
+ _context15.next = 18;
76774
77138
  break;
76775
- case 14:
76776
- _context15.next = 16;
77139
+ case 15:
77140
+ _context15.next = 17;
76777
77141
  return getXlf(newOptions);
76778
- case 16:
76779
- layoutXlf = _context15.sent;
76780
77142
  case 17:
77143
+ layoutXlf = _context15.sent;
77144
+ case 18:
76781
77145
  parser = new window.DOMParser();
76782
77146
  layoutXlfNode = parser.parseFromString(layoutXlf, 'text/xml');
76783
- _context15.next = 22;
77147
+ _context15.next = 23;
76784
77148
  break;
76785
- case 21:
76786
- layoutXlfNode = inputLayout && inputLayout.layoutNode;
76787
77149
  case 22:
77150
+ layoutXlfNode = inputLayout && inputLayout.layoutNode;
77151
+ case 23:
76788
77152
  isOverlayLayout = !!(inputLayout !== null && inputLayout !== void 0 && inputLayout.isOverlay);
76789
77153
  return _context15.abrupt("return", new Promise(function (resolve) {
76790
77154
  var _inputLayout$ad;
@@ -76809,13 +77173,22 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76809
77173
  xlrLayoutObj.duration = sspInputLayout.duration || 0;
76810
77174
  xlrLayoutObj.ad = sspInputLayout.ad;
76811
77175
  }
77176
+ var xlrLayout;
76812
77177
  if (isOverlayLayout) {
76813
- resolve(new OverlayLayout(xlrLayoutObj, newOptions, self, layoutXlfNode));
77178
+ xlrLayout = new OverlayLayout(xlrLayoutObj, newOptions, self, layoutXlfNode);
76814
77179
  } else {
76815
- resolve(new Layout(xlrLayoutObj, newOptions, self, layoutXlfNode));
77180
+ xlrLayout = new Layout(xlrLayoutObj, newOptions, self, layoutXlfNode);
77181
+ }
77182
+ // Advance the shared counter so the next prepareLayoutXlf() call
77183
+ // starts from where this layout left off — prevents every layout
77184
+ // instance from reusing idCounter=1 and colliding on the same
77185
+ // containerName / DOM element.
77186
+ if (props.options) {
77187
+ props.options.idCounter = newOptions.idCounter;
76816
77188
  }
77189
+ resolve(xlrLayout);
76817
77190
  }));
76818
- case 24:
77191
+ case 25:
76819
77192
  case "end":
76820
77193
  return _context15.stop();
76821
77194
  }
@@ -76827,46 +77200,49 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76827
77200
  }();
76828
77201
  xlrObject.prepareForSsp = /*#__PURE__*/function () {
76829
77202
  var _ref16 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee16(nextLayout) {
76830
- var self, _nextLayout, nextIndex, inputLayout, nextLayoutObj;
77203
+ var self, _nextLayout, iterations, maxIterations, nextIndex, inputLayout, nextLayoutObj;
76831
77204
  return _regeneratorRuntime().wrap(function _callee16$(_context16) {
76832
77205
  while (1) switch (_context16.prev = _context16.next) {
76833
77206
  case 0:
76834
77207
  self = this;
76835
77208
  _nextLayout = nextLayout;
76836
- case 2:
76837
- if (!(_nextLayout && _nextLayout.xlfString === '')) {
76838
- _context16.next = 17;
76839
- break;
76840
- }
76841
- // Remove skipped layout
76842
- _nextLayout.removeLayout();
76843
- // Get next valid layout
76844
- // We will skip next layout that has no valid xlf
76845
- nextIndex = _nextLayout.index + 1;
76846
- if (!(nextIndex >= self.inputLayouts.length)) {
76847
- _context16.next = 7;
77209
+ iterations = 0;
77210
+ maxIterations = self.inputLayouts.length;
77211
+ case 4:
77212
+ if (!(_nextLayout && _nextLayout.xlfString === '' && iterations < maxIterations)) {
77213
+ _context16.next = 20;
76848
77214
  break;
76849
77215
  }
76850
- return _context16.abrupt("break", 17);
76851
- case 7:
77216
+ // Remove the empty slot's DOM element before skipping past it
77217
+ _nextLayout.removeLayout(exports.LayoutPlaybackType.NEXT);
77218
+ iterations++;
77219
+ // Advance to the next slot, wrapping around so a trailing SSP slot
77220
+ // with no ad does not strand the queue at the end of the array.
77221
+ nextIndex = (_nextLayout.index + 1) % self.inputLayouts.length;
76852
77222
  inputLayout = self.inputLayouts[nextIndex];
76853
77223
  if (inputLayout) {
76854
- _context16.next = 10;
77224
+ _context16.next = 11;
76855
77225
  break;
76856
77226
  }
76857
- return _context16.abrupt("break", 17);
76858
- case 10:
77227
+ return _context16.abrupt("break", 20);
77228
+ case 11:
76859
77229
  nextLayoutObj = self.getLayout(inputLayout);
76860
77230
  nextLayoutObj = setLayoutIndex(nextLayoutObj, nextIndex);
76861
- _context16.next = 14;
77231
+ if (nextLayoutObj) {
77232
+ _context16.next = 15;
77233
+ break;
77234
+ }
77235
+ return _context16.abrupt("break", 20);
77236
+ case 15:
77237
+ _context16.next = 17;
76862
77238
  return self.prepareLayoutXlf(nextLayoutObj);
76863
- case 14:
77239
+ case 17:
76864
77240
  _nextLayout = _context16.sent;
76865
- _context16.next = 2;
77241
+ _context16.next = 4;
76866
77242
  break;
76867
- case 17:
77243
+ case 20:
76868
77244
  return _context16.abrupt("return", _nextLayout);
76869
- case 18:
77245
+ case 21:
76870
77246
  case "end":
76871
77247
  return _context16.stop();
76872
77248
  }
@@ -76878,7 +77254,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76878
77254
  }();
76879
77255
  xlrObject.gotoPrevLayout = /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee18() {
76880
77256
  var _this4 = this;
76881
- var _currentLayoutIndex, _assumedPrevIndex, _this$currentLayout4;
77257
+ var _currentLayoutIndex, _assumedPrevIndex, _this$currentLayout6;
76882
77258
  return _regeneratorRuntime().wrap(function _callee18$(_context18) {
76883
77259
  while (1) switch (_context18.prev = _context18.next) {
76884
77260
  case 0:
@@ -76901,7 +77277,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76901
77277
  break;
76902
77278
  }
76903
77279
  _context18.next = 8;
76904
- return (_this$currentLayout4 = this.currentLayout) === null || _this$currentLayout4 === void 0 ? void 0 : _this$currentLayout4.finishAllRegions();
77280
+ return (_this$currentLayout6 = this.currentLayout) === null || _this$currentLayout6 === void 0 ? void 0 : _this$currentLayout6.finishAllRegions();
76905
77281
  case 8:
76906
77282
  // and set the previous layout as current layout
76907
77283
  this.currentLayoutIndex = _assumedPrevIndex;
@@ -76929,7 +77305,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76929
77305
  }, _callee18, this);
76930
77306
  }));
76931
77307
  xlrObject.gotoNextLayout = /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee19() {
76932
- var _xlrObject$currentLay;
77308
+ var _xlrObject$currentLay2;
76933
77309
  return _regeneratorRuntime().wrap(function _callee19$(_context19) {
76934
77310
  while (1) switch (_context19.prev = _context19.next) {
76935
77311
  case 0:
@@ -76939,7 +77315,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76939
77315
  shouldParse: false
76940
77316
  });
76941
77317
  _context19.next = 3;
76942
- return (_xlrObject$currentLay = xlrObject.currentLayout) === null || _xlrObject$currentLay === void 0 ? void 0 : _xlrObject$currentLay.finishAllRegions();
77318
+ return (_xlrObject$currentLay2 = xlrObject.currentLayout) === null || _xlrObject$currentLay2 === void 0 ? void 0 : _xlrObject$currentLay2.finishAllRegions();
76943
77319
  case 3:
76944
77320
  case "end":
76945
77321
  return _context19.stop();
@@ -76951,6 +77327,11 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76951
77327
  if (layout !== null) {
76952
77328
  layout.index = xlrInputLayout.index;
76953
77329
  }
77330
+ console.debug('XLR::updateInputLayout', {
77331
+ layoutIndex: layoutIndex,
77332
+ layout: layout,
77333
+ xlrInputLayout: xlrInputLayout
77334
+ });
76954
77335
  this.inputLayouts[layoutIndex] = layout || xlrInputLayout;
76955
77336
  };
76956
77337
  xlrObject.bootstrap();
@@ -76994,11 +77375,11 @@ exports.hasDefaultOnly = hasDefaultOnly;
76994
77375
  exports.hasSspLayout = hasSspLayout;
76995
77376
  exports.initRenderingDOM = initRenderingDOM;
76996
77377
  exports.initialLayout = initialLayout;
76997
- exports.initialMedia = initialMedia;
76998
77378
  exports.initialRegion = initialRegion;
76999
77379
  exports.initialXlr = initialXlr;
77000
77380
  exports.isEmpty = isEmpty;
77001
77381
  exports.isLayoutValid = isLayoutValid;
77382
+ exports.isMediaActive = isMediaActive;
77002
77383
  exports.nextId = nextId;
77003
77384
  exports.playerReportFault = playerReportFault;
77004
77385
  exports.preloadMediaBlob = preloadMediaBlob;
@@ -77006,6 +77387,7 @@ exports.prepareAudioMedia = prepareAudioMedia;
77006
77387
  exports.prepareHtmlMedia = prepareHtmlMedia;
77007
77388
  exports.prepareImageMedia = prepareImageMedia;
77008
77389
  exports.prepareVideoMedia = prepareVideoMedia;
77390
+ exports.reportToPlayerPlatform = reportToPlayerPlatform;
77009
77391
  exports.setExpiry = setExpiry;
77010
77392
  exports.transitionElement = transitionElement;
77011
77393
  exports.videoFileType = videoFileType;