@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.
@@ -703,6 +703,7 @@ var initialLayout = {
703
703
  return Promise.resolve([]);
704
704
  },
705
705
  removeLayout: function removeLayout() {},
706
+ discardLayout: function discardLayout() {},
706
707
  getXlf: function getXlf() {
707
708
  return '';
708
709
  },
@@ -713,6 +714,13 @@ var initialLayout = {
713
714
  html: null
714
715
  };
715
716
 
717
+ var MediaState = {
718
+ IDLE: 'idle',
719
+ PLAYING: 'playing',
720
+ ENDED: 'ended',
721
+ CANCELLED: 'cancelled'
722
+ };
723
+
716
724
  var initialRegion = {
717
725
  complete: false,
718
726
  containerName: '',
@@ -744,6 +752,7 @@ var initialRegion = {
744
752
  options: {},
745
753
  playNextMedia: function playNextMedia() {},
746
754
  playPreviousMedia: function playPreviousMedia() {},
755
+ prepareMedia: function prepareMedia(_media) {},
747
756
  prepareMediaObjects: function prepareMediaObjects() {},
748
757
  prepareRegion: function prepareRegion() {},
749
758
  ready: false,
@@ -761,56 +770,6 @@ var initialRegion = {
761
770
  xlr: {}
762
771
  };
763
772
 
764
- var MediaState = {
765
- IDLE: 'idle',
766
- PLAYING: 'playing',
767
- ENDED: 'ended',
768
- CANCELLED: 'cancelled'
769
- };
770
- var initialMedia = {
771
- attachedAudio: false,
772
- checkIframeStatus: false,
773
- containerName: '',
774
- divHeight: 0,
775
- divWidth: 0,
776
- duration: 0,
777
- emitter: {},
778
- enableStat: false,
779
- fileId: '',
780
- finished: false,
781
- html: null,
782
- id: '',
783
- idCounter: 0,
784
- iframe: null,
785
- iframeName: '',
786
- index: 0,
787
- loadIframeOnRun: false,
788
- loop: false,
789
- mediaId: '',
790
- mediaType: '',
791
- muted: false,
792
- options: {},
793
- player: undefined,
794
- ready: true,
795
- region: initialRegion,
796
- render: 'html',
797
- run: function run() {},
798
- schemaVersion: '1',
799
- singlePlay: false,
800
- state: MediaState.IDLE,
801
- stop: function stop() {
802
- return Promise.resolve();
803
- },
804
- tempSrc: '',
805
- timeoutId: setTimeout(function () {}, 0),
806
- type: '',
807
- uri: '',
808
- url: null,
809
- useDuration: Boolean(0),
810
- xml: null,
811
- mediaTimer: undefined
812
- };
813
-
814
773
  var OverlayLayoutManager = /*#__PURE__*/function () {
815
774
  function OverlayLayoutManager() {
816
775
  _classCallCheck(this, OverlayLayoutManager);
@@ -1107,6 +1066,7 @@ var initialXlr = {
1107
1066
  isLayoutInDOM: function isLayoutInDOM(containerName, layoutId) {
1108
1067
  return false;
1109
1068
  },
1069
+ cleanupOrphanedLayouts: function cleanupOrphanedLayouts(_keepCurrent, _keepNext) {},
1110
1070
  isSspEnabled: false,
1111
1071
  isUpdatingLoop: false,
1112
1072
  isUpdatingOverlays: false,
@@ -72527,6 +72487,10 @@ if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) {
72527
72487
  function composeVideoSource($media, media) {
72528
72488
  // const videoSrc = await preloadMediaBlob(media.url as string, media.mediaType as MediaTypes);
72529
72489
  var vidType = videoFileType(getFileExt(media.uri));
72490
+ if (!vidType) {
72491
+ console.warn("XLR >> VideoMedia: Unsupported video type for media ".concat(media.id, " with uri ").concat(media.uri));
72492
+ return $media;
72493
+ }
72530
72494
  // Only add one source per type
72531
72495
  if ($media.querySelectorAll("source[type=\"".concat(vidType, "\"]")).length === 0) {
72532
72496
  var $videoSource = document.createElement('source');
@@ -72553,8 +72517,38 @@ var vjsDefaultOptions = function vjsDefaultOptions(opts) {
72553
72517
  };
72554
72518
  var reportToPlayerPlatform = [ConsumerPlatform.CHROMEOS, ConsumerPlatform.ELECTRON];
72555
72519
  function VideoMedia(media, xlr) {
72520
+ var stopped = false;
72556
72521
  var mediaId = getMediaId(media);
72522
+ // ── Stall watchdog (closure-level so stop() can cancel it) ───────────────
72523
+ // 'waiting' and 'stalled' fire when the browser stops receiving data.
72524
+ // Unlike codec or source errors they do NOT fire the 'error' event, so
72525
+ // without a watchdog the video silently freezes for its entire duration.
72526
+ var stallWatchdog;
72527
+ var STALL_TIMEOUT_MS = 10000;
72528
+ var clearStallWatchdog = function clearStallWatchdog() {
72529
+ if (stallWatchdog !== undefined) {
72530
+ clearTimeout(stallWatchdog);
72531
+ stallWatchdog = undefined;
72532
+ }
72533
+ };
72534
+ // ─────────────────────────────────────────────────────────────────────────
72535
+ // ── Unified error → report → stop helper (closure-level) ─────────────────
72536
+ // Used by both the 'error' event and the play Promise catch.
72537
+ // playerReportFault only fires for platforms that report faults (Electron,
72538
+ // ChromeOS). All other platforms just advance to the next media via stop().
72539
+ var reportAndStop = function reportAndStop(reason, code) {
72540
+ if (stopped) return;
72541
+ if (reportToPlayerPlatform.includes(xlr.config.platform)) {
72542
+ playerReportFault(reason, media, code).then(function () {
72543
+ return videoPlayer.stop();
72544
+ });
72545
+ } else {
72546
+ videoPlayer.stop();
72547
+ }
72548
+ };
72549
+ // ─────────────────────────────────────────────────────────────────────────
72557
72550
  var videoPlayer = {
72551
+ player: undefined,
72558
72552
  duration: 0,
72559
72553
  init: function init() {
72560
72554
  var _this = this;
@@ -72562,9 +72556,37 @@ function VideoMedia(media, xlr) {
72562
72556
  videoPlayer.duration = media.duration;
72563
72557
  var vjsPlayer = videojs(mediaId);
72564
72558
  if (vjsPlayer) {
72565
- vjsPlayer.on('loadstart', function () {
72566
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has started loading data . . ."));
72567
- });
72559
+ videoPlayer.player = vjsPlayer;
72560
+ // ── Early source check ────────────────────────────────────────────────
72561
+ // Two-step check before video.js tries to load anything:
72562
+ // 1. Is the file extension one we map to a MIME type?
72563
+ // 2. Can the browser actually play that MIME type?
72564
+ // Failing either step skips the media immediately so video.js
72565
+ // never renders its "No compatible source" error overlay.
72566
+ var vidType = videoFileType(getFileExt(media.uri));
72567
+ if (!vidType) {
72568
+ console.warn("XLR >> VideoMedia: unrecognised file extension for media ".concat(media.id, " (uri: ").concat(media.uri, ")"));
72569
+ reportAndStop("Unsupported video file extension for media ".concat(media.id), FaultCodes.FaultVideoSource);
72570
+ return;
72571
+ }
72572
+ if (document.createElement('video').canPlayType(vidType) === '') {
72573
+ console.warn("XLR >> VideoMedia: browser cannot play type \"".concat(vidType, "\" for media ").concat(media.id));
72574
+ reportAndStop("Browser cannot play video type \"".concat(vidType, "\" for media ").concat(media.id), FaultCodes.FaultVideoSource);
72575
+ return;
72576
+ }
72577
+ // ─────────────────────────────────────────────────────────────────────
72578
+ var armStallWatchdog = function armStallWatchdog() {
72579
+ clearStallWatchdog();
72580
+ stallWatchdog = setTimeout(function () {
72581
+ if (stopped) return;
72582
+ console.warn("XLR >> VideoMedia: stall timeout on media ".concat(media.id));
72583
+ reportAndStop('Video stall timeout', FaultCodes.FaultVideoUnexpected);
72584
+ }, STALL_TIMEOUT_MS);
72585
+ };
72586
+ vjsPlayer.on('waiting', armStallWatchdog);
72587
+ vjsPlayer.on('stalled', armStallWatchdog);
72588
+ vjsPlayer.on('playing', clearStallWatchdog);
72589
+ vjsPlayer.on('ended', clearStallWatchdog);
72568
72590
  vjsPlayer.on('loadedmetadata', function () {
72569
72591
  if (media.duration === 0) {
72570
72592
  videoPlayer.duration = vjsPlayer.duration();
@@ -72635,34 +72657,20 @@ function VideoMedia(media, xlr) {
72635
72657
  }, 5000);
72636
72658
  })]).then(function () {
72637
72659
  console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Autoplay started"));
72638
- })["catch"]( /*#__PURE__*/function () {
72639
- var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(error) {
72640
- return _regeneratorRuntime().wrap(function _callee2$(_context2) {
72641
- while (1) switch (_context2.prev = _context2.next) {
72642
- case 0:
72643
- if (error === 'Timeout') {
72644
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Promise not resolved within 5 seconds. Move to next media"));
72645
- _this.stop();
72646
- } else {
72647
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Autoplay error: ").concat(error));
72648
- if (reportToPlayerPlatform.includes(xlr.config.platform)) {
72649
- playerReportFault('Media autoplay error', media).then(function () {
72650
- _this.stop();
72651
- });
72652
- }
72653
- }
72654
- case 1:
72655
- case "end":
72656
- return _context2.stop();
72657
- }
72658
- }, _callee2);
72659
- }));
72660
- return function (_x) {
72661
- return _ref2.apply(this, arguments);
72662
- };
72663
- }());
72660
+ })["catch"](function (error) {
72661
+ if (stopped) return;
72662
+ if (error === 'Timeout') {
72663
+ console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Promise not resolved within 5 seconds. Move to next media"));
72664
+ // Timeout is a scheduling issue, not a media fault — just advance
72665
+ videoPlayer.stop();
72666
+ } else {
72667
+ console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Autoplay error: ").concat(error));
72668
+ reportAndStop('Media autoplay error', FaultCodes.FaultVideoUnexpected);
72669
+ }
72670
+ });
72664
72671
  // Optional: Reset the flag automatically when a new video loads or the source changes
72665
72672
  vjsPlayer.on('loadstart', function () {
72673
+ console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has started loading data . . ."));
72666
72674
  triggerTimeUpdate = false;
72667
72675
  });
72668
72676
  if (media.duration === 0) {
@@ -72675,13 +72683,9 @@ function VideoMedia(media, xlr) {
72675
72683
  if (mediaDuration !== undefined && currentTime !== undefined) {
72676
72684
  remainingTimeMs = (mediaDuration - currentTime) * 1000;
72677
72685
  }
72678
- if (regionHasMultipleMedia && remainingTimeMs === 0 && !triggerTimeUpdate) {
72679
- // We don't have data yet and we must immediately prepare next media
72680
- media.region.prepareNextMedia();
72681
- } else if (regionHasMultipleMedia && remainingTimeMs <= preloadBufferTimeMs && !triggerTimeUpdate) {
72686
+ if (regionHasMultipleMedia && !triggerTimeUpdate && (remainingTimeMs === 0 || remainingTimeMs <= preloadBufferTimeMs)) {
72682
72687
  // Check if remaining time is less than preloadBufferTimeMs and the action hasn't been triggered yet
72683
72688
  console.log('Less than preloadBufferTimeMs remaining! Do something now.');
72684
- // Prepare next media in region
72685
72689
  media.region.prepareNextMedia();
72686
72690
  triggerTimeUpdate = true; // Set the flag to prevent re-triggering
72687
72691
  }
@@ -72693,33 +72697,16 @@ function VideoMedia(media, xlr) {
72693
72697
  }
72694
72698
  }
72695
72699
  });
72696
- vjsPlayer.on('error', /*#__PURE__*/function () {
72697
- var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(err) {
72698
- return _regeneratorRuntime().wrap(function _callee3$(_context3) {
72699
- while (1) switch (_context3.prev = _context3.next) {
72700
- case 0:
72701
- console.debug("??? XLR.debug >> VideoMedia: Media Error: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id));
72702
- if (reportToPlayerPlatform.includes(xlr.config.platform)) {
72703
- playerReportFault('Video file source not supported', media).then(function () {
72704
- _this.stop();
72705
- });
72706
- } else {
72707
- // End media after 5 seconds
72708
- setTimeout(function () {
72709
- console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has ended . . ."));
72710
- _this.stop();
72711
- }, 5000);
72712
- }
72713
- case 2:
72714
- case "end":
72715
- return _context3.stop();
72716
- }
72717
- }, _callee3);
72718
- }));
72719
- return function (_x2) {
72720
- return _ref3.apply(this, arguments);
72721
- };
72722
- }());
72700
+ vjsPlayer.on('error', function () {
72701
+ if (stopped) return;
72702
+ clearStallWatchdog();
72703
+ // Extract the actual MediaError so the fault message is
72704
+ // meaningful: code 2 = network, 3 = decode, 4 = not supported.
72705
+ var vjsError = vjsPlayer.error();
72706
+ var reason = vjsError ? "Video error (code ".concat(vjsError.code, "): ").concat(vjsError.message) : 'Unknown video error';
72707
+ console.warn("XLR >> VideoMedia: error on media ".concat(media.id), vjsError);
72708
+ reportAndStop(reason, FaultCodes.FaultVideoUnexpected);
72709
+ });
72723
72710
  if (media.duration === 0) {
72724
72711
  vjsPlayer.on('ended', function () {
72725
72712
  console.debug("??? XLR.debug >> VideoMedia: onended: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has ended playing . . ."));
@@ -72729,26 +72716,37 @@ function VideoMedia(media, xlr) {
72729
72716
  }
72730
72717
  },
72731
72718
  stop: function stop() {
72719
+ var _videoPlayer$player;
72732
72720
  var disposeOnly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
72733
- var vjsPlayer = media.player;
72721
+ clearStallWatchdog();
72722
+ // videoPlayer.player is where init() stores the vjs instance;
72723
+ // media.player is a legacy path kept for backward compat but is
72724
+ // no longer set by init(), so always prefer videoPlayer.player.
72725
+ var vjsPlayer = (_videoPlayer$player = videoPlayer.player) !== null && _videoPlayer$player !== void 0 ? _videoPlayer$player : media.player;
72734
72726
  console.debug('??? XLR.debug >> VideoMedia::stop', {
72735
72727
  vjsPlayer: vjsPlayer,
72736
- isDisposed: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.isDisposed_,
72737
- el: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.el_
72728
+ isDisposed: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.isDisposed(),
72729
+ el: vjsPlayer === null || vjsPlayer === void 0 ? void 0 : vjsPlayer.el()
72738
72730
  });
72739
72731
  // Expire the media and dispose the video
72740
- if (vjsPlayer !== undefined && !vjsPlayer.isDisposed_) {
72732
+ if (vjsPlayer !== undefined && !vjsPlayer.isDisposed()) {
72741
72733
  if (!disposeOnly) {
72742
72734
  media.emitter.emit('end', media);
72743
72735
  }
72744
72736
  vjsPlayer.dispose();
72745
72737
  // Clear up media player
72738
+ videoPlayer.player = undefined;
72746
72739
  media.player = undefined;
72740
+ media.html = null;
72747
72741
  } else {
72742
+ videoPlayer.player = undefined;
72748
72743
  media.player = undefined;
72749
72744
  media.html = null;
72750
- media.emitter.emit('end', media);
72745
+ if (!disposeOnly) {
72746
+ media.emitter.emit('end', media);
72747
+ }
72751
72748
  }
72749
+ stopped = true;
72752
72750
  },
72753
72751
  play: function play() {
72754
72752
  var _this2 = this;
@@ -72756,9 +72754,9 @@ function VideoMedia(media, xlr) {
72756
72754
  if (vjsPlayer !== undefined) {
72757
72755
  var _vjsPlayer$play;
72758
72756
  (_vjsPlayer$play = vjsPlayer.play()) === null || _vjsPlayer$play === void 0 || _vjsPlayer$play["catch"]( /*#__PURE__*/function () {
72759
- var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(error) {
72760
- return _regeneratorRuntime().wrap(function _callee4$(_context4) {
72761
- while (1) switch (_context4.prev = _context4.next) {
72757
+ var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(error) {
72758
+ return _regeneratorRuntime().wrap(function _callee2$(_context2) {
72759
+ while (1) switch (_context2.prev = _context2.next) {
72762
72760
  case 0:
72763
72761
  if (error === 'Timeout') {
72764
72762
  console.debug("??? XLR.debug >> VideoMedia: ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " : Promise not resolved within 5 seconds. Move to next media"));
@@ -72773,12 +72771,12 @@ function VideoMedia(media, xlr) {
72773
72771
  }
72774
72772
  case 1:
72775
72773
  case "end":
72776
- return _context4.stop();
72774
+ return _context2.stop();
72777
72775
  }
72778
- }, _callee4);
72776
+ }, _callee2);
72779
72777
  }));
72780
- return function (_x3) {
72781
- return _ref4.apply(this, arguments);
72778
+ return function (_x) {
72779
+ return _ref2.apply(this, arguments);
72782
72780
  };
72783
72781
  }());
72784
72782
  }
@@ -73190,11 +73188,11 @@ function getDataBlob(_x, _x2) {
73190
73188
  return _getDataBlob.apply(this, arguments);
73191
73189
  }
73192
73190
  function _getDataBlob() {
73193
- _getDataBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(src, jwtToken) {
73194
- return _regeneratorRuntime().wrap(function _callee3$(_context3) {
73195
- while (1) switch (_context3.prev = _context3.next) {
73191
+ _getDataBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(src, jwtToken) {
73192
+ return _regeneratorRuntime().wrap(function _callee2$(_context2) {
73193
+ while (1) switch (_context2.prev = _context2.next) {
73196
73194
  case 0:
73197
- return _context3.abrupt("return", fetch(src, {
73195
+ return _context2.abrupt("return", fetch(src, {
73198
73196
  method: 'GET',
73199
73197
  headers: {
73200
73198
  'X-PREVIEW-JWT': jwtToken || ''
@@ -73213,9 +73211,9 @@ function _getDataBlob() {
73213
73211
  }));
73214
73212
  case 1:
73215
73213
  case "end":
73216
- return _context3.stop();
73214
+ return _context2.stop();
73217
73215
  }
73218
- }, _callee3);
73216
+ }, _callee2);
73219
73217
  }));
73220
73218
  return _getDataBlob.apply(this, arguments);
73221
73219
  }
@@ -73223,12 +73221,12 @@ function preloadMediaBlob(_x3, _x4, _x5) {
73223
73221
  return _preloadMediaBlob.apply(this, arguments);
73224
73222
  }
73225
73223
  function _preloadMediaBlob() {
73226
- _preloadMediaBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(src, type, jwtToken) {
73224
+ _preloadMediaBlob = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(src, type, jwtToken) {
73227
73225
  var res, blob, data;
73228
- return _regeneratorRuntime().wrap(function _callee4$(_context4) {
73229
- while (1) switch (_context4.prev = _context4.next) {
73226
+ return _regeneratorRuntime().wrap(function _callee3$(_context3) {
73227
+ while (1) switch (_context3.prev = _context3.next) {
73230
73228
  case 0:
73231
- _context4.next = 2;
73229
+ _context3.next = 2;
73232
73230
  return fetch(src, {
73233
73231
  method: 'GET',
73234
73232
  headers: {
@@ -73236,45 +73234,45 @@ function _preloadMediaBlob() {
73236
73234
  }
73237
73235
  });
73238
73236
  case 2:
73239
- res = _context4.sent;
73237
+ res = _context3.sent;
73240
73238
  blob = new Blob();
73241
73239
  if (!(type === 'image')) {
73242
- _context4.next = 8;
73240
+ _context3.next = 8;
73243
73241
  break;
73244
73242
  }
73245
73243
  blob = new Blob();
73246
- _context4.next = 19;
73244
+ _context3.next = 19;
73247
73245
  break;
73248
73246
  case 8:
73249
73247
  if (!(type === 'video')) {
73250
- _context4.next = 14;
73248
+ _context3.next = 14;
73251
73249
  break;
73252
73250
  }
73253
- _context4.next = 11;
73251
+ _context3.next = 11;
73254
73252
  return res.blob();
73255
73253
  case 11:
73256
- blob = _context4.sent;
73257
- _context4.next = 19;
73254
+ blob = _context3.sent;
73255
+ _context3.next = 19;
73258
73256
  break;
73259
73257
  case 14:
73260
73258
  if (!(type === 'audio')) {
73261
- _context4.next = 19;
73259
+ _context3.next = 19;
73262
73260
  break;
73263
73261
  }
73264
- _context4.next = 17;
73262
+ _context3.next = 17;
73265
73263
  return res.arrayBuffer();
73266
73264
  case 17:
73267
- data = _context4.sent;
73265
+ data = _context3.sent;
73268
73266
  blob = new Blob([data], {
73269
73267
  type: audioFileType(getFileExt(src))
73270
73268
  });
73271
73269
  case 19:
73272
- return _context4.abrupt("return", URL.createObjectURL(blob));
73270
+ return _context3.abrupt("return", URL.createObjectURL(blob));
73273
73271
  case 20:
73274
73272
  case "end":
73275
- return _context4.stop();
73273
+ return _context3.stop();
73276
73274
  }
73277
- }, _callee4);
73275
+ }, _callee3);
73278
73276
  }));
73279
73277
  return _preloadMediaBlob.apply(this, arguments);
73280
73278
  }
@@ -73282,11 +73280,11 @@ function fetchJSON(_x6, _x7) {
73282
73280
  return _fetchJSON.apply(this, arguments);
73283
73281
  }
73284
73282
  function _fetchJSON() {
73285
- _fetchJSON = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(url, jwtToken) {
73286
- return _regeneratorRuntime().wrap(function _callee5$(_context5) {
73287
- while (1) switch (_context5.prev = _context5.next) {
73283
+ _fetchJSON = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(url, jwtToken) {
73284
+ return _regeneratorRuntime().wrap(function _callee4$(_context4) {
73285
+ while (1) switch (_context4.prev = _context4.next) {
73288
73286
  case 0:
73289
- return _context5.abrupt("return", fetch(url, {
73287
+ return _context4.abrupt("return", fetch(url, {
73290
73288
  method: 'GET',
73291
73289
  headers: {
73292
73290
  'X-PREVIEW-JWT': jwtToken || ''
@@ -73298,9 +73296,9 @@ function _fetchJSON() {
73298
73296
  }));
73299
73297
  case 1:
73300
73298
  case "end":
73301
- return _context5.stop();
73299
+ return _context4.stop();
73302
73300
  }
73303
- }, _callee5);
73301
+ }, _callee4);
73304
73302
  }));
73305
73303
  return _fetchJSON.apply(this, arguments);
73306
73304
  }
@@ -73308,11 +73306,11 @@ function fetchText(_x8, _x9) {
73308
73306
  return _fetchText.apply(this, arguments);
73309
73307
  }
73310
73308
  function _fetchText() {
73311
- _fetchText = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6(url, jwtToken) {
73312
- return _regeneratorRuntime().wrap(function _callee6$(_context6) {
73313
- while (1) switch (_context6.prev = _context6.next) {
73309
+ _fetchText = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(url, jwtToken) {
73310
+ return _regeneratorRuntime().wrap(function _callee5$(_context5) {
73311
+ while (1) switch (_context5.prev = _context5.next) {
73314
73312
  case 0:
73315
- return _context6.abrupt("return", fetch(url, {
73313
+ return _context5.abrupt("return", fetch(url, {
73316
73314
  method: 'GET',
73317
73315
  headers: {
73318
73316
  'X-PREVIEW-JWT': jwtToken || ''
@@ -73331,9 +73329,9 @@ function _fetchText() {
73331
73329
  }));
73332
73330
  case 1:
73333
73331
  case "end":
73334
- return _context6.stop();
73332
+ return _context5.stop();
73335
73333
  }
73336
- }, _callee6);
73334
+ }, _callee5);
73337
73335
  }));
73338
73336
  return _fetchText.apply(this, arguments);
73339
73337
  }
@@ -73450,6 +73448,32 @@ function setExpiry(numDays) {
73450
73448
  var today = new Date();
73451
73449
  return new Date(today.setHours(24 * numDays || 1)).toJSON();
73452
73450
  }
73451
+ /**
73452
+ * Check whether a media item is currently within its valid date window.
73453
+ * Returns true when the media should be shown, false when it should be skipped.
73454
+ *
73455
+ * Rules:
73456
+ * - Empty / invalid fromDt → treat as "no start restriction"
73457
+ * - Empty / invalid toDt → treat as "no expiry"
73458
+ * - now < fromDt → not yet active → skip
73459
+ * - now > toDt → expired → skip
73460
+ */
73461
+ function isMediaActive(fromDt, toDt) {
73462
+ var now = Date.now();
73463
+ if (fromDt) {
73464
+ var from = new Date(fromDt).getTime();
73465
+ if (!isNaN(from) && now < from) {
73466
+ return false;
73467
+ }
73468
+ }
73469
+ if (toDt) {
73470
+ var to = new Date(toDt).getTime();
73471
+ if (!isNaN(to) && now > to) {
73472
+ return false;
73473
+ }
73474
+ }
73475
+ return true;
73476
+ }
73453
73477
  /**
73454
73478
  * Check if given layout exists in the loop using layoutId
73455
73479
  * @param layouts Schedule loop unique layouts (uniqueLayouts)
@@ -73658,7 +73682,7 @@ function prepareVideoMedia(media, region) {
73658
73682
  var $layout = region.layout.html;
73659
73683
  var layoutSelector = '#' + region.layout.containerName + '[data-sequence="' + region.layout.index + '"]';
73660
73684
  var $layoutWithIndex = document.querySelector(layoutSelector);
73661
- var $region = document.querySelector('#' + region.containerName);
73685
+ var $region = region.html;
73662
73686
  var mediaInRegion = $region === null || $region === void 0 ? void 0 : $region.querySelector('.' + mediaId);
73663
73687
  console.debug('??? XLR.debug >> [Generators::prepareVideoMedia]', {
73664
73688
  layoutSelector: layoutSelector,
@@ -73677,7 +73701,7 @@ function prepareVideoMedia(media, region) {
73677
73701
  media.html = createMediaElement(media);
73678
73702
  }
73679
73703
  // Append fresh copy of the media into the region
73680
- $region !== null && $region.appendChild(media.html);
73704
+ region.html.appendChild(media.html);
73681
73705
  var isMediaInDOM = document.body.contains(media.html);
73682
73706
  console.debug('??? XLR.debug >> [Generators::prepareVideoMedia]', {
73683
73707
  isMediaInDOM: isMediaInDOM,
@@ -73686,29 +73710,9 @@ function prepareVideoMedia(media, region) {
73686
73710
  });
73687
73711
  // Initialize video.js
73688
73712
  media.player = videojs(mediaId, _objectSpread2(_objectSpread2({}, defaultVjsOpts), {}, {
73689
- errorDisplay: region.xlr.config.platform !== ConsumerPlatform.CHROMEOS,
73713
+ errorDisplay: !reportToPlayerPlatform.includes(region.xlr.config.platform),
73690
73714
  loop: media.loop
73691
73715
  }));
73692
- media.player.on('error', /*#__PURE__*/function () {
73693
- var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(err) {
73694
- return _regeneratorRuntime().wrap(function _callee2$(_context2) {
73695
- while (1) switch (_context2.prev = _context2.next) {
73696
- case 0:
73697
- if (media.region.xlr.config.platform === ConsumerPlatform.CHROMEOS) {
73698
- playerReportFault('Video file not supported', media).then(function () {
73699
- media.emitter.emit('end', media);
73700
- });
73701
- }
73702
- case 1:
73703
- case "end":
73704
- return _context2.stop();
73705
- }
73706
- }, _callee2);
73707
- }));
73708
- return function (_x10) {
73709
- return _ref3.apply(this, arguments);
73710
- };
73711
- }());
73712
73716
  media.player.el().style.setProperty('visibility', 'hidden');
73713
73717
  media.player.el().style.setProperty('opacity', '0');
73714
73718
  media.player.el().style.setProperty('z-index', '-99');
@@ -73723,9 +73727,9 @@ function prepareImageMedia(media, region) {
73723
73727
  if (mediaInRegion) {
73724
73728
  mediaInRegion.remove();
73725
73729
  }
73726
- // Append media to its region
73727
- var $region = document.querySelector('#' + region.containerName);
73728
- $region !== null && $region.appendChild(media.html);
73730
+ // Append media to its region using the direct reference to avoid
73731
+ // global querySelector finding a same-named region in another layout
73732
+ region.html.appendChild(media.html);
73729
73733
  }
73730
73734
  function prepareAudioMedia(media, region) {
73731
73735
  var mediaId = getMediaId(media);
@@ -73738,9 +73742,8 @@ function prepareAudioMedia(media, region) {
73738
73742
  if (mediaInRegion) {
73739
73743
  mediaInRegion.remove();
73740
73744
  }
73741
- // Append media to its region
73742
- var $region = document.querySelector('#' + region.containerName);
73743
- $region !== null && $region.appendChild(media.html);
73745
+ // Append media to its region using the direct reference
73746
+ region.html.appendChild(media.html);
73744
73747
  }
73745
73748
  function prepareHtmlMedia(media, region) {
73746
73749
  // Set state as false ( for now )
@@ -73760,57 +73763,92 @@ function prepareHtmlMedia(media, region) {
73760
73763
  media.html.innerHTML = '';
73761
73764
  media.html.appendChild(media.iframe);
73762
73765
  if (!mediaInRegion) {
73763
- // Add fresh copy of the media into the region
73764
- var _$region = document.querySelector('#' + region.containerName);
73765
- _$region !== null && _$region.appendChild(media.html);
73766
+ // Add fresh copy of the media into the region using the direct reference
73767
+ region.html.appendChild(media.html);
73766
73768
  media.ready = true;
73767
73769
  }
73768
73770
  }
73769
73771
  }
73770
- function playerReportFault(_x11, _x12) {
73772
+ var FaultCodes;
73773
+ (function (FaultCodes) {
73774
+ FaultCodes[FaultCodes["FaultVideoSource"] = 2001] = "FaultVideoSource";
73775
+ FaultCodes[FaultCodes["FaultVideoUnexpected"] = 2099] = "FaultVideoUnexpected";
73776
+ })(FaultCodes || (FaultCodes = {}));
73777
+ function playerReportFault(_x10, _x11) {
73771
73778
  return _playerReportFault.apply(this, arguments);
73772
73779
  }
73773
73780
  function _playerReportFault() {
73774
- _playerReportFault = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee7(msg, media) {
73775
- var playerSW, hasSW;
73776
- return _regeneratorRuntime().wrap(function _callee7$(_context7) {
73777
- while (1) switch (_context7.prev = _context7.next) {
73781
+ _playerReportFault = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6(msg, media) {
73782
+ var code,
73783
+ platform,
73784
+ playerSW,
73785
+ hasSW,
73786
+ mediaFault,
73787
+ channel,
73788
+ _args6 = arguments;
73789
+ return _regeneratorRuntime().wrap(function _callee6$(_context6) {
73790
+ while (1) switch (_context6.prev = _context6.next) {
73778
73791
  case 0:
73792
+ code = _args6.length > 2 && _args6[2] !== undefined ? _args6[2] : FaultCodes.FaultVideoUnexpected;
73779
73793
  // Immediately expire media and report a fault
73794
+ platform = media.region.xlr.config.platform;
73780
73795
  playerSW = PwaSW();
73781
- _context7.next = 3;
73796
+ _context6.next = 5;
73782
73797
  return playerSW.getSW();
73783
- case 3:
73784
- hasSW = _context7.sent;
73785
- if (hasSW) {
73786
- playerSW.postMsg({
73787
- type: 'MEDIA_FAULT',
73788
- code: 5002,
73789
- reason: msg,
73798
+ case 5:
73799
+ hasSW = _context6.sent;
73800
+ mediaFault = {
73801
+ type: 'MEDIA_FAULT',
73802
+ code: code,
73803
+ reason: msg,
73804
+ mediaId: media.id,
73805
+ regionId: media.region.id,
73806
+ layoutId: media.region.layout.id,
73807
+ date: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
73808
+ // Temporary setting
73809
+ expires: format(new Date(setExpiry(1)), 'yyyy-MM-dd HH:mm:ss')
73810
+ };
73811
+ console.debug('playerReportFault >> Reporting media fault', {
73812
+ mediaFault: mediaFault,
73813
+ platform: platform,
73814
+ hasSW: hasSW
73815
+ });
73816
+ if (!(platform === ConsumerPlatform.CHROMEOS && hasSW)) {
73817
+ _context6.next = 12;
73818
+ break;
73819
+ }
73820
+ return _context6.abrupt("return", playerSW.postMsg(mediaFault).then(function () {
73821
+ // We try to prepare next media if we have more than 1 media
73822
+ if (media.region.totalMediaObjects > 1) {
73823
+ media.region.prepareNextMedia();
73824
+ }
73825
+ })["finally"](function () {
73826
+ // Stopping media as we have reported the error as fault
73827
+ console.debug('??? XLR.debug >> VideoMedia - Done reporting media fault', {
73790
73828
  mediaId: media.id,
73791
- regionId: media.region.id,
73792
- layoutId: media.region.layout.id,
73793
- date: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
73794
- // Temporary setting
73795
- expires: format(new Date(setExpiry(1)), 'yyyy-MM-dd HH:mm:ss')
73796
- }).then(function () {
73797
- // We try to prepare next media if we have more than 1 media
73798
- if (media.region.totalMediaObjects > 1) {
73799
- media.region.prepareNextMedia();
73800
- }
73801
- })["finally"](function () {
73802
- // Stopping media as we have reported the error as fault
73803
- console.debug('??? XLR.debug >> VideoMedia - Done reporting media fault', {
73804
- mediaId: media.id,
73805
- regionItems: media.region.totalMediaObjects
73806
- });
73829
+ regionItems: media.region.totalMediaObjects
73807
73830
  });
73831
+ }));
73832
+ case 12:
73833
+ if (!(platform === ConsumerPlatform.ELECTRON)) {
73834
+ _context6.next = 17;
73835
+ break;
73808
73836
  }
73809
- case 5:
73837
+ // Create a broadcast channel to report media fault to the main process
73838
+ channel = new BroadcastChannel('player-faults-bc');
73839
+ channel.postMessage(mediaFault);
73840
+ console.debug('playerReportFault >> Electron platform - posted media fault to channel', {
73841
+ mediaFault: mediaFault
73842
+ });
73843
+ // channel.close();
73844
+ return _context6.abrupt("return", Promise.resolve());
73845
+ case 17:
73846
+ return _context6.abrupt("return", Promise.resolve());
73847
+ case 18:
73810
73848
  case "end":
73811
- return _context7.stop();
73849
+ return _context6.stop();
73812
73850
  }
73813
- }, _callee7);
73851
+ }, _callee6);
73814
73852
  }));
73815
73853
  return _playerReportFault.apply(this, arguments);
73816
73854
  }
@@ -73838,7 +73876,7 @@ let nanoid = (size = 21) => {
73838
73876
  function AudioMedia(media) {
73839
73877
  var audioMediaObject = {
73840
73878
  init: function init() {
73841
- var $audioMedia = document.getElementById(getMediaId(media));
73879
+ var $audioMedia = media.html;
73842
73880
  var $playBtn = null;
73843
73881
  if ($audioMedia) {
73844
73882
  $audioMedia.onloadstart = function () {
@@ -73901,6 +73939,8 @@ var Media = /*#__PURE__*/function () {
73901
73939
  _this$xml3,
73902
73940
  _this$xml4,
73903
73941
  _this$xml5,
73942
+ _this$xml6,
73943
+ _this$xml7,
73904
73944
  _this = this;
73905
73945
  _classCallCheck(this, Media);
73906
73946
  _defineProperty(this, "attachedAudio", false);
@@ -73913,6 +73953,7 @@ var Media = /*#__PURE__*/function () {
73913
73953
  _defineProperty(this, "enableStat", false);
73914
73954
  _defineProperty(this, "fileId", '');
73915
73955
  _defineProperty(this, "finished", false);
73956
+ _defineProperty(this, "fromDt", '');
73916
73957
  _defineProperty(this, "html", null);
73917
73958
  _defineProperty(this, "id", '');
73918
73959
  _defineProperty(this, "idCounter", 0);
@@ -73934,6 +73975,7 @@ var Media = /*#__PURE__*/function () {
73934
73975
  _defineProperty(this, "state", MediaState.IDLE);
73935
73976
  _defineProperty(this, "tempSrc", '');
73936
73977
  _defineProperty(this, "timeoutId", setTimeout(function () {}, 0));
73978
+ _defineProperty(this, "toDt", '');
73937
73979
  _defineProperty(this, "type", '');
73938
73980
  _defineProperty(this, "uri", '');
73939
73981
  _defineProperty(this, "url", null);
@@ -73941,6 +73983,9 @@ var Media = /*#__PURE__*/function () {
73941
73983
  _defineProperty(this, "xml", null);
73942
73984
  _defineProperty(this, "videoHandler", void 0);
73943
73985
  _defineProperty(this, "mediaTimer", void 0);
73986
+ _defineProperty(this, "sspImpressionUrls", undefined);
73987
+ _defineProperty(this, "sspErrorUrls", undefined);
73988
+ _defineProperty(this, "isSspWidget", false);
73944
73989
  _defineProperty(this, "mediaTimeCount", 0);
73945
73990
  _defineProperty(this, "xlr", {});
73946
73991
  _defineProperty(this, "statsBC", new BroadcastChannel('statsBC'));
@@ -73960,12 +74005,14 @@ var Media = /*#__PURE__*/function () {
73960
74005
  this.duration = parseInt((_this$xml4 = this.xml) === null || _this$xml4 === void 0 ? void 0 : _this$xml4.getAttribute('duration')) || 0;
73961
74006
  this.enableStat = Boolean(((_this$xml5 = this.xml) === null || _this$xml5 === void 0 ? void 0 : _this$xml5.getAttribute('enableStat')) || false);
73962
74007
  this.hasCommandExecuted = false;
74008
+ this.fromDt = ((_this$xml6 = this.xml) === null || _this$xml6 === void 0 ? void 0 : _this$xml6.getAttribute('fromDt')) || '';
74009
+ this.toDt = ((_this$xml7 = this.xml) === null || _this$xml7 === void 0 ? void 0 : _this$xml7.getAttribute('toDt')) || '';
73963
74010
  this.on('start', function (media) {
73964
74011
  if (media.state === MediaState.PLAYING) return;
73965
74012
  media.state = MediaState.PLAYING;
73966
74013
  if (media.mediaType === 'video') {
73967
- var videoMedia = VideoMedia(media, _this.xlr);
73968
- videoMedia.init();
74014
+ media.videoHandler = VideoMedia(media, _this.xlr);
74015
+ media.videoHandler.init();
73969
74016
  if (media.duration > 0) {
73970
74017
  _this.startMediaTimer(media);
73971
74018
  }
@@ -74024,6 +74071,10 @@ var Media = /*#__PURE__*/function () {
74024
74071
  layoutId: media.region.layout.id
74025
74072
  });
74026
74073
  _this.xlr.emitter.emit('widgetEnd', parseInt(media.id));
74074
+ if (_this.isSspWidget) {
74075
+ var _this$sspImpressionUr, _this$sspErrorUrls;
74076
+ _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);
74077
+ }
74027
74078
  media.region.playNextMedia();
74028
74079
  });
74029
74080
  this.on('cancelled', function (media) {
@@ -74050,6 +74101,10 @@ var Media = /*#__PURE__*/function () {
74050
74101
  layoutId: media.region.layout.id
74051
74102
  });
74052
74103
  _this.xlr.emitter.emit('widgetEnd', parseInt(media.id));
74104
+ if (_this.isSspWidget) {
74105
+ var _this$sspImpressionUr2, _this$sspErrorUrls2;
74106
+ _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);
74107
+ }
74053
74108
  media.region.playNextMedia();
74054
74109
  });
74055
74110
  // Initialize Media object
@@ -74092,8 +74147,8 @@ var Media = /*#__PURE__*/function () {
74092
74147
  if (media.mediaType === 'video') {
74093
74148
  // Dispose the video media
74094
74149
  console.debug("??? XLR.debug >> VideoMedia::stop - ".concat(capitalizeStr(media.mediaType), " for media > ").concat(media.id, " has ended playing . . ."));
74095
- if (media.player !== undefined) {
74096
- VideoMedia(media, _this2.xlr).stop(true);
74150
+ if (media.videoHandler !== undefined) {
74151
+ media.videoHandler.stop(true);
74097
74152
  }
74098
74153
  }
74099
74154
  }
@@ -74108,8 +74163,8 @@ var Media = /*#__PURE__*/function () {
74108
74163
  }, {
74109
74164
  key: "init",
74110
74165
  value: function init() {
74111
- var _this$xml6;
74112
- var mediaOptions = (_this$xml6 = this.xml) === null || _this$xml6 === void 0 ? void 0 : _this$xml6.getElementsByTagName('options');
74166
+ var _this$xml8;
74167
+ var mediaOptions = (_this$xml8 = this.xml) === null || _this$xml8 === void 0 ? void 0 : _this$xml8.getElementsByTagName('options');
74113
74168
  if (mediaOptions) {
74114
74169
  for (var _i = 0, _Array$from = Array.from(mediaOptions); _i < _Array$from.length; _i++) {
74115
74170
  var _options = _Array$from[_i];
@@ -74150,22 +74205,33 @@ var Media = /*#__PURE__*/function () {
74150
74205
  if (this.mediaType === 'image' || this.mediaType === 'video') {
74151
74206
  resourceUrlParams.mediaType = this.mediaType;
74152
74207
  }
74153
- var tmpUrl = '';
74154
- if (this.xlr.config.platform === ConsumerPlatform.CMS) {
74155
- tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74156
- } else if (this.xlr.config.platform === ConsumerPlatform.CHROMEOS) {
74157
- tmpUrl = composeResourceUrl(this.xlr.config, resourceUrlParams);
74158
- if (this.mediaType === 'image' || this.mediaType === 'video' || this.mediaType === 'audio') {
74159
- tmpUrl = composeMediaUrl(resourceUrlParams);
74208
+ // SSP widget: URL is not known until the consumer resolves an ad at play-time.
74209
+ // Skip all URL composition and leave url as null.
74210
+ if (this.mediaType === 'ssp') {
74211
+ this.url = null;
74212
+ this.isSspWidget = true;
74213
+ } else {
74214
+ var tmpUrl = '';
74215
+ if (this.xlr.config.platform === ConsumerPlatform.CMS) {
74216
+ tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74217
+ } else if (this.xlr.config.platform === ConsumerPlatform.CHROMEOS) {
74218
+ tmpUrl = composeResourceUrl(this.xlr.config, resourceUrlParams);
74219
+ if (this.mediaType === 'image' || this.mediaType === 'video' || this.mediaType === 'audio') {
74220
+ tmpUrl = composeMediaUrl(resourceUrlParams);
74221
+ // this is an SSP Layout
74222
+ if (this.region.layout.layoutId === -1) {
74223
+ tmpUrl = this.uri;
74224
+ }
74225
+ }
74226
+ } else if (this.xlr.config.platform === ConsumerPlatform.ELECTRON) {
74227
+ tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74160
74228
  // this is an SSP Layout
74161
74229
  if (this.region.layout.layoutId === -1) {
74162
74230
  tmpUrl = this.uri;
74163
74231
  }
74164
74232
  }
74165
- } else if (this.xlr.config.platform === ConsumerPlatform.ELECTRON) {
74166
- tmpUrl = composeResourceUrlByPlatform(this.xlr.config, resourceUrlParams);
74233
+ this.url = tmpUrl;
74167
74234
  }
74168
- this.url = tmpUrl;
74169
74235
  // Loop if media has loop, or if region has loop and a single media
74170
74236
  this.loop = this.options['loop'] == '1' || this.region.options['loop'] == '1' && this.region.totalMediaObjects == 1;
74171
74237
  this.html = createMediaElement(this);
@@ -74206,8 +74272,8 @@ var Media = /*#__PURE__*/function () {
74206
74272
  mediaType: _this3.mediaType,
74207
74273
  containerName: _this3.containerName
74208
74274
  });
74209
- var $region = document.querySelector('#' + _this3.region.containerName);
74210
- var $media = $region !== null && $region.querySelector('.' + mediaId);
74275
+ var $region = _this3.region.html;
74276
+ var $media = $region.querySelector('.' + mediaId);
74211
74277
  if (!$media) {
74212
74278
  $media = getNewMedia();
74213
74279
  }
@@ -74271,40 +74337,66 @@ var Media = /*#__PURE__*/function () {
74271
74337
  }
74272
74338
  };
74273
74339
  var getNewMedia = function getNewMedia() {
74274
- var $region = document.getElementById("".concat(_this3.region.containerName));
74340
+ var $region = _this3.region.html;
74275
74341
  // This function is for checking whether
74276
74342
  // the region still has to show a media item
74277
74343
  // when another region is not finished yet
74278
74344
  if (_this3.region.complete && !_this3.region.layout.allEnded) {
74279
74345
  // Add currentMedia to the region
74280
- $region && $region.insertBefore(_this3.html, $region.lastElementChild);
74346
+ $region.insertBefore(_this3.html, $region.lastElementChild);
74281
74347
  return _this3.html;
74282
74348
  }
74283
74349
  return null;
74284
74350
  };
74351
+ // SSP widget: if the consumer did not resolve an ad during the preload window
74352
+ // (i.e. setSspAdUrl was never called), skip this widget and advance normally.
74353
+ if (this.mediaType === 'ssp') {
74354
+ console.debug('??? XLR.debug >> Media.run() > SSP widget: no ad resolved during preload, skipping');
74355
+ this.emitter.emit('end', this);
74356
+ return;
74357
+ }
74285
74358
  showCurrentMedia();
74286
74359
  }
74360
+ }, {
74361
+ key: "setSspAdUrl",
74362
+ value: function setSspAdUrl(url, adMediaType, impressionUrls, errorUrls) {
74363
+ // Ignore if the media has already been skipped or cancelled before the ad arrived.
74364
+ if (this.state !== MediaState.IDLE) {
74365
+ console.debug('??? XLR.debug >> Media::setSspAdUrl - ignoring, media is no longer idle', {
74366
+ state: this.state
74367
+ });
74368
+ return;
74369
+ }
74370
+ // Remove the placeholder <div> so the correct element type can take its place.
74371
+ if (this.html) {
74372
+ this.html.remove();
74373
+ this.html = null;
74374
+ }
74375
+ this.url = url;
74376
+ this.mediaType = adMediaType;
74377
+ this.sspImpressionUrls = impressionUrls;
74378
+ this.sspErrorUrls = errorUrls;
74379
+ // Re-create the element now that mediaType is known, then prepare and append to region DOM.
74380
+ // Visibility and playback are handled by run() when this media's turn comes.
74381
+ this.html = createMediaElement(this);
74382
+ this.region.prepareMedia(this);
74383
+ }
74287
74384
  }, {
74288
74385
  key: "stop",
74289
74386
  value: function () {
74290
74387
  var _stop = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
74291
- var $media;
74292
74388
  return _regeneratorRuntime().wrap(function _callee$(_context) {
74293
74389
  while (1) switch (_context.prev = _context.next) {
74294
74390
  case 0:
74295
- $media = document.getElementById(getMediaId({
74296
- mediaType: this.mediaType,
74297
- containerName: this.containerName
74298
- }));
74299
- if ($media) {
74300
- $media.style.display = 'none';
74301
- $media.remove();
74391
+ if (this.html) {
74392
+ this.html.style.display = 'none';
74393
+ this.html.remove();
74302
74394
  }
74303
74395
  // Release blob URLs for image media to prevent memory leaks on long-running signage
74304
74396
  if (this.mediaType === 'image' && this.url) {
74305
74397
  BlobLoader.release(this.url);
74306
74398
  }
74307
- case 3:
74399
+ case 2:
74308
74400
  case "end":
74309
74401
  return _context.stop();
74310
74402
  }
@@ -74477,18 +74569,30 @@ var Region = /*#__PURE__*/function () {
74477
74569
  this.html = $region;
74478
74570
  /* Parse region media objects */
74479
74571
  var regionMediaItems = Array.from(this.xml.getElementsByTagName('media'));
74480
- this.totalMediaObjects = regionMediaItems.length;
74481
74572
  $layout && $layout.appendChild(this.html);
74482
74573
  Array.from(regionMediaItems).forEach( /*#__PURE__*/function () {
74483
74574
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(mediaXml, indx) {
74484
- var mediaObj;
74575
+ var fromDt, toDt, mediaObj;
74485
74576
  return _regeneratorRuntime().wrap(function _callee$(_context) {
74486
74577
  while (1) switch (_context.prev = _context.next) {
74487
74578
  case 0:
74579
+ fromDt = mediaXml.getAttribute('fromDt') || '';
74580
+ toDt = mediaXml.getAttribute('toDt') || '';
74581
+ if (isMediaActive(fromDt, toDt)) {
74582
+ _context.next = 5;
74583
+ break;
74584
+ }
74585
+ console.debug('??? XLR.debug >> Region::prepareRegion - skipping expired/inactive media', {
74586
+ mediaId: mediaXml.getAttribute('id'),
74587
+ fromDt: fromDt,
74588
+ toDt: toDt
74589
+ });
74590
+ return _context.abrupt("return");
74591
+ case 5:
74488
74592
  mediaObj = new Media(_this, (mediaXml === null || mediaXml === void 0 ? void 0 : mediaXml.getAttribute('id')) || '', mediaXml, _this.options, _this.xlr);
74489
74593
  mediaObj.index = indx;
74490
74594
  _this.mediaObjects.push(mediaObj);
74491
- case 3:
74595
+ case 8:
74492
74596
  case "end":
74493
74597
  return _context.stop();
74494
74598
  }
@@ -74498,6 +74602,7 @@ var Region = /*#__PURE__*/function () {
74498
74602
  return _ref.apply(this, arguments);
74499
74603
  };
74500
74604
  }());
74605
+ this.totalMediaObjects = this.mediaObjects.length;
74501
74606
  console.debug('??? XLR.debug >> Region - done looping through media', {
74502
74607
  mediaObjects: this.mediaObjects
74503
74608
  });
@@ -74528,6 +74633,13 @@ var Region = /*#__PURE__*/function () {
74528
74633
  }, {
74529
74634
  key: "prepareMedia",
74530
74635
  value: function prepareMedia(media) {
74636
+ // SSP widget: signal the consumer to fetch an ad before this media's turn to play.
74637
+ // The consumer calls media.setSspAdUrl() to resolve it; if it never arrives,
74638
+ // Media.run() skips the widget and advances normally.
74639
+ if (media.mediaType === 'ssp') {
74640
+ this.xlr.emitter.emit('sspWidgetRequest', media);
74641
+ return;
74642
+ }
74531
74643
  if (media.mediaType === 'video') {
74532
74644
  prepareVideoMedia(media, this);
74533
74645
  } else if (media.mediaType === 'image' && media.url !== null) {
@@ -74553,6 +74665,23 @@ var Region = /*#__PURE__*/function () {
74553
74665
  key: "prepareNextMedia",
74554
74666
  value: function prepareNextMedia() {
74555
74667
  var nextMediaIndex = (this.currentMediaIndex + 1) % this.totalMediaObjects;
74668
+ // Skip over any media items that are no longer within their active date window
74669
+ var skippedCount = 0;
74670
+ while (skippedCount < this.totalMediaObjects) {
74671
+ var candidate = this.mediaObjects[nextMediaIndex];
74672
+ if (isMediaActive(candidate.fromDt, candidate.toDt)) {
74673
+ break;
74674
+ }
74675
+ skippedCount++;
74676
+ nextMediaIndex = (nextMediaIndex + 1) % this.totalMediaObjects;
74677
+ }
74678
+ // Nothing to pre-load when:
74679
+ // - every item is expired, OR
74680
+ // - the only active item is the one currently playing (skip loop wrapped back to it)
74681
+ if (skippedCount >= this.totalMediaObjects || nextMediaIndex === this.currentMediaIndex) {
74682
+ console.debug('<><> XLR.debug >> [Region::prepareNextMedia()] - no active next media to preload');
74683
+ return;
74684
+ }
74556
74685
  var nextMedia = this.mediaObjects[nextMediaIndex];
74557
74686
  console.debug('<><> XLR.debug >> [Media] - [Region::prepareNextMedia()] - Preparing next media', {
74558
74687
  currentMediaIndex: this.currentMediaIndex,
@@ -74593,9 +74722,21 @@ var Region = /*#__PURE__*/function () {
74593
74722
  }, {
74594
74723
  key: "run",
74595
74724
  value: function run() {
74725
+ var _this$currMedia, _this$oldMedia2;
74596
74726
  console.debug('??? XLR.debug >> Region Called Region::run > ', this.id);
74597
74727
  // Reset region states
74598
74728
  this.reset();
74729
+ // All media were filtered out (all expired/inactive before this run started)
74730
+ if (this.mediaObjects.length === 0) {
74731
+ console.debug('??? XLR.debug >> Region::run - no active media, finishing region', this.id);
74732
+ this.finished();
74733
+ return;
74734
+ }
74735
+ console.debug('??? XLR.debug >> Region Called Region::run - after reset > ', {
74736
+ regionId: this.id,
74737
+ currMedia: (_this$currMedia = this.currMedia) === null || _this$currMedia === void 0 ? void 0 : _this$currMedia.containerName,
74738
+ oldMedia: (_this$oldMedia2 = this.oldMedia) === null || _this$oldMedia2 === void 0 ? void 0 : _this$oldMedia2.containerName
74739
+ });
74599
74740
  if (this.currMedia) {
74600
74741
  this.transitionNodes(this.oldMedia, this.currMedia);
74601
74742
  }
@@ -74644,6 +74785,7 @@ var Region = /*#__PURE__*/function () {
74644
74785
  // Hide oldMedia
74645
74786
  if (oldMedia) {
74646
74787
  var $layout = document.querySelector("#".concat(_this2.layout.containerName, "[data-sequence=\"").concat(_this2.layout.index, "\"]"));
74788
+ if (!$layout) return;
74647
74789
  var $region = $layout.querySelector('#' + _this2.containerName);
74648
74790
  var $oldMedia = $region ? $region.querySelector('.' + getMediaId(oldMedia)) : null;
74649
74791
  if ($oldMedia) {
@@ -74664,7 +74806,7 @@ var Region = /*#__PURE__*/function () {
74664
74806
  $videoWrapper.style.setProperty('visibility', 'hidden');
74665
74807
  $videoWrapper.style.setProperty('z-index', '-999');
74666
74808
  $videoWrapper.style.setProperty('opacity', '0');
74667
- if (oldMedia.player && oldMedia.videoHandler) {
74809
+ if (oldMedia.videoHandler) {
74668
74810
  oldMedia.videoHandler.stop(true);
74669
74811
  }
74670
74812
  }
@@ -74714,7 +74856,12 @@ var Region = /*#__PURE__*/function () {
74714
74856
  }
74715
74857
  };
74716
74858
  if (oldMedia) {
74717
- hideOldMedia();
74859
+ // Skip hiding old media when it is the same object as new media
74860
+ // (single-media loop): removing it would also remove the element
74861
+ // that is about to be shown, leaving the region blank.
74862
+ if (oldMedia !== newMedia) {
74863
+ hideOldMedia();
74864
+ }
74718
74865
  newMedia.run();
74719
74866
  } else {
74720
74867
  newMedia.run();
@@ -74724,13 +74871,13 @@ var Region = /*#__PURE__*/function () {
74724
74871
  }, {
74725
74872
  key: "playNextMedia",
74726
74873
  value: function playNextMedia() {
74727
- var _this$oldMedia2, _this$currMedia, _this$nxtMedia, _this$currMedia2, _this$currMedia3, _this$currMedia4, _this$currMedia5, _this$currMedia6, _this$oldMedia3, _this$currMedia7, _this$nxtMedia2;
74874
+ var _this$oldMedia3, _this$currMedia2, _this$nxtMedia, _this$currMedia3, _this$currMedia4, _this$currMedia5, _this$currMedia6, _this$oldMedia4, _this$currMedia7, _this$nxtMedia2;
74728
74875
  console.debug('??? XLR.debug Region playing next media', {
74729
74876
  regionId: this.id,
74730
74877
  currentMediaIndex: this.currentMediaIndex,
74731
74878
  mediaItemsLn: this.mediaObjects.length,
74732
- oldMedia: (_this$oldMedia2 = this.oldMedia) === null || _this$oldMedia2 === void 0 ? void 0 : _this$oldMedia2.containerName,
74733
- currMedia: (_this$currMedia = this.currMedia) === null || _this$currMedia === void 0 ? void 0 : _this$currMedia.containerName,
74879
+ oldMedia: (_this$oldMedia3 = this.oldMedia) === null || _this$oldMedia3 === void 0 ? void 0 : _this$oldMedia3.containerName,
74880
+ currMedia: (_this$currMedia2 = this.currMedia) === null || _this$currMedia2 === void 0 ? void 0 : _this$currMedia2.containerName,
74734
74881
  nxtMedia: (_this$nxtMedia = this.nxtMedia) === null || _this$nxtMedia === void 0 ? void 0 : _this$nxtMedia.containerName
74735
74882
  });
74736
74883
  /* The current media has finished running */
@@ -74740,33 +74887,20 @@ var Region = /*#__PURE__*/function () {
74740
74887
  });
74741
74888
  return;
74742
74889
  }
74743
- // Are we in a playlist, and has the playlist completed a full cycle?
74744
- var isLastMediaInPlaylist = this.currentMediaIndex === this.mediaObjects.length - 1 && this.mediaObjects.length > 1;
74745
- // If yes, enable shell command widgets again (if any), so they execute on the next playlist cycle
74746
- if (isLastMediaInPlaylist) {
74747
- this.mediaObjects.forEach(function (media) {
74748
- if (media.mediaType === 'shellcommand') {
74749
- // reset per-playlist-cycle execution state
74750
- media.hasCommandExecuted = false;
74751
- }
74752
- });
74753
- }
74754
- if (!this.layout.isOverlay && this.currentMediaIndex === this.mediaObjects.length - 1) {
74755
- this.finished();
74756
- if (this.layout.allEnded) {
74757
- console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74758
- return;
74759
- }
74760
- }
74890
+ // Snapshot the index of the media that just ended so we can detect
74891
+ // cycle completion after the skip loop runs.
74892
+ var origIndex = this.currentMediaIndex;
74761
74893
  // When the region has completed and when currentMedia is html
74762
74894
  // Then, preserve the currentMedia state
74763
- if (this.complete && ((_this$currMedia2 = this.currMedia) === null || _this$currMedia2 === void 0 ? void 0 : _this$currMedia2.render) === 'html') {
74895
+ if (this.complete && ((_this$currMedia3 = this.currMedia) === null || _this$currMedia3 === void 0 ? void 0 : _this$currMedia3.render) === 'html') {
74764
74896
  return;
74765
74897
  }
74766
74898
  // When the region has completed and mediaObjects.length = 1
74767
- // and curMedia.loop = false, then put the media on
74768
- // its current state
74769
- 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)) {
74899
+ // and render is not html, preserve the current media state without
74900
+ // calling transitionNodes (which would remove the media from DOM for 1s).
74901
+ // Do NOT restart the media timer here the layout will end naturally when
74902
+ // regionExpired() detects all regions complete on the next cycle.
74903
+ 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')) {
74770
74904
  return;
74771
74905
  }
74772
74906
  if (this.currMedia) {
@@ -74774,17 +74908,51 @@ var Region = /*#__PURE__*/function () {
74774
74908
  } else {
74775
74909
  this.oldMedia = undefined;
74776
74910
  }
74777
- this.currentMediaIndex = (this.currentMediaIndex + 1) % this.totalMediaObjects;
74911
+ this.currentMediaIndex = (origIndex + 1) % this.totalMediaObjects;
74778
74912
  this.currMedia = this.mediaObjects[this.currentMediaIndex];
74913
+ // Skip media items that are no longer within their active date window
74914
+ var skippedCount = 0;
74915
+ while (this.currMedia && !isMediaActive(this.currMedia.fromDt, this.currMedia.toDt)) {
74916
+ skippedCount++;
74917
+ if (skippedCount >= this.totalMediaObjects) {
74918
+ // Every item in the playlist has expired; finish this region
74919
+ console.debug('??? XLR.debug >> Region::playNextMedia - all media expired, finishing region', this.id);
74920
+ this.finished();
74921
+ return;
74922
+ }
74923
+ this.currentMediaIndex = (this.currentMediaIndex + 1) % this.totalMediaObjects;
74924
+ this.currMedia = this.mediaObjects[this.currentMediaIndex];
74925
+ }
74779
74926
  this.nxtMedia = this.mediaObjects[(this.currentMediaIndex + 1) % this.totalMediaObjects];
74927
+ // A full playlist cycle has been traversed when the total advancement
74928
+ // (the initial +1 step plus any skips over expired items) reaches or
74929
+ // crosses the end of the array.
74930
+ var crossedEnd = origIndex + 1 + skippedCount >= this.totalMediaObjects;
74931
+ // Re-enable shell command widgets at the end of each full cycle so they
74932
+ // execute again on the next pass.
74933
+ if (crossedEnd && this.mediaObjects.length > 1) {
74934
+ this.mediaObjects.forEach(function (media) {
74935
+ if (media.mediaType === 'shellcommand') {
74936
+ // reset per-playlist-cycle execution state
74937
+ media.hasCommandExecuted = false;
74938
+ }
74939
+ });
74940
+ }
74780
74941
  console.debug('??? XLR.debug >> End Region::playNextMedia > execute transitionNodes', {
74781
74942
  regionId: this.id,
74782
74943
  currentMediaIndex: this.currentMediaIndex,
74783
74944
  mediaItemsLn: this.mediaObjects.length,
74784
- oldMedia: (_this$oldMedia3 = this.oldMedia) === null || _this$oldMedia3 === void 0 ? void 0 : _this$oldMedia3.containerName,
74945
+ oldMedia: (_this$oldMedia4 = this.oldMedia) === null || _this$oldMedia4 === void 0 ? void 0 : _this$oldMedia4.containerName,
74785
74946
  currMedia: (_this$currMedia7 = this.currMedia) === null || _this$currMedia7 === void 0 ? void 0 : _this$currMedia7.containerName,
74786
74947
  nxtMedia: (_this$nxtMedia2 = this.nxtMedia) === null || _this$nxtMedia2 === void 0 ? void 0 : _this$nxtMedia2.containerName
74787
74948
  });
74949
+ if (!this.layout.isOverlay && crossedEnd) {
74950
+ this.finished();
74951
+ if (this.layout.allEnded) {
74952
+ console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74953
+ return;
74954
+ }
74955
+ }
74788
74956
  this.transitionNodes(this.oldMedia, this.currMedia);
74789
74957
  }
74790
74958
  }, {
@@ -74813,8 +74981,7 @@ var Region = /*#__PURE__*/function () {
74813
74981
  }, {
74814
74982
  key: "exitTransition",
74815
74983
  value: function exitTransition() {
74816
- /* TODO: Actually implement region exit transitions */
74817
- document.getElementById("".concat(this.containerName));
74984
+ /* TODO: Actually implement region exit transitions using this.html */
74818
74985
  console.debug('Called Region::exitTransition ', this.id);
74819
74986
  this.exitTransitionComplete();
74820
74987
  }
@@ -75114,7 +75281,7 @@ var ActionController = /*#__PURE__*/function () {
75114
75281
  if (dataset.source === 'region') {
75115
75282
  // Try to find the region
75116
75283
  if (regionObj.id === dataset.sourceid) {
75117
- $sourceObj = document.getElementById(regionObj.containerName);
75284
+ $sourceObj = regionObj.html;
75118
75285
  break;
75119
75286
  }
75120
75287
  } else if (dataset.source === 'widget') {
@@ -75123,7 +75290,7 @@ var ActionController = /*#__PURE__*/function () {
75123
75290
  for (var _i2 = 0, _mediaObjects = mediaObjects; _i2 < _mediaObjects.length; _i2++) {
75124
75291
  var mediaObject = _mediaObjects[_i2];
75125
75292
  if (mediaObject.id === dataset.sourceid) {
75126
- $sourceObj = document.getElementById(mediaObject.containerName);
75293
+ $sourceObj = mediaObject.html;
75127
75294
  break;
75128
75295
  }
75129
75296
  }
@@ -75388,7 +75555,11 @@ var Layout = /*#__PURE__*/function () {
75388
75555
  this.on('start', function (layout) {
75389
75556
  layout.done = false;
75390
75557
  layout.state = ELayoutState.RUNNING;
75391
- console.debug('>>>> XLR.debug Layout start emitted > Layout ID > ', layout.id);
75558
+ console.debug('>>>> XLR.debug Layout start emitted > Layout > ', {
75559
+ layoutId: layout.id,
75560
+ layoutIndex: layout.index,
75561
+ layoutState: layout.state
75562
+ });
75392
75563
  // Check if stats are enabled for the layout
75393
75564
  if (layout.enableStat) {
75394
75565
  _this.statsBC.postMessage({
@@ -75409,12 +75580,21 @@ var Layout = /*#__PURE__*/function () {
75409
75580
  while (1) switch (_context2.prev = _context2.next) {
75410
75581
  case 0:
75411
75582
  if (!(layout.state === ELayoutState.CANCELLED)) {
75412
- _context2.next = 2;
75583
+ _context2.next = 3;
75413
75584
  break;
75414
75585
  }
75586
+ console.debug('>>>> XLR.debug Layout end emitted but layout is already cancelled > Layout ID > ', {
75587
+ layoutId: layout.id,
75588
+ layoutIndex: layout.index,
75589
+ layoutState: layout.state
75590
+ });
75415
75591
  return _context2.abrupt("return");
75416
- case 2:
75417
- console.debug('>>>> XLR.debug Ending layout with ID of > ', layout.layoutId);
75592
+ case 3:
75593
+ console.debug('>>>> XLR.debug Ending layout', {
75594
+ layoutId: layout.id,
75595
+ layoutIndex: layout.index,
75596
+ layoutState: layout.state
75597
+ });
75418
75598
  /* Remove layout that has ended */
75419
75599
  $layout = document.querySelector("#".concat(layout.containerName, "[data-sequence=\"").concat(layout.index, "\"]")); // Only update layout.state when last state === RUNNING
75420
75600
  if (layout.state === ELayoutState.RUNNING) {
@@ -75425,6 +75605,8 @@ var Layout = /*#__PURE__*/function () {
75425
75605
  console.debug('>>> XLR.debug Layout end emitted > Layout ID > ', {
75426
75606
  $layout: $layout,
75427
75607
  layoutId: layout.id,
75608
+ layoutIndex: layout.index,
75609
+ layoutState: layout.state,
75428
75610
  isOverlay: layout.isOverlay,
75429
75611
  isInterrupt: layout.isInterrupt()
75430
75612
  });
@@ -75451,9 +75633,9 @@ var Layout = /*#__PURE__*/function () {
75451
75633
  }
75452
75634
  // Emit layout end event
75453
75635
  console.debug('>>>>> XLR.debug Awaited XLR::emitSync > End - Calling layoutEnd event');
75454
- _context2.next = 12;
75636
+ _context2.next = 13;
75455
75637
  return layout.xlr.emitSync('layoutEnd', layout);
75456
- case 12:
75638
+ case 13:
75457
75639
  if (_this.xlr.config.platform !== ConsumerPlatform.CMS && layout.inLoop) {
75458
75640
  // Transition next layout to current layout and prepare next layout if exist
75459
75641
  _this.xlr.prepareLayouts().then( /*#__PURE__*/function () {
@@ -75483,7 +75665,7 @@ var Layout = /*#__PURE__*/function () {
75483
75665
  };
75484
75666
  }());
75485
75667
  }
75486
- case 13:
75668
+ case 14:
75487
75669
  case "end":
75488
75670
  return _context2.stop();
75489
75671
  }
@@ -75496,6 +75678,34 @@ var Layout = /*#__PURE__*/function () {
75496
75678
  this.on('cancelled', function (layout) {
75497
75679
  console.debug('>>>>> XLR.debug / Layout cancelled > Layout ID > ', layout.id);
75498
75680
  layout.state = ELayoutState.CANCELLED;
75681
+ layout.inLoop = false;
75682
+ // Dispose video handlers immediately so their stall watchdogs and error
75683
+ // callbacks can't fire against a layout whose DOM is about to be removed.
75684
+ var _iterator = _createForOfIteratorHelper(layout.regions),
75685
+ _step;
75686
+ try {
75687
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
75688
+ var region = _step.value;
75689
+ var _iterator2 = _createForOfIteratorHelper(region.mediaObjects),
75690
+ _step2;
75691
+ try {
75692
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
75693
+ var media = _step2.value;
75694
+ if (media.videoHandler) {
75695
+ media.videoHandler.stop(true);
75696
+ }
75697
+ }
75698
+ } catch (err) {
75699
+ _iterator2.e(err);
75700
+ } finally {
75701
+ _iterator2.f();
75702
+ }
75703
+ }
75704
+ } catch (err) {
75705
+ _iterator.e(err);
75706
+ } finally {
75707
+ _iterator.f();
75708
+ }
75499
75709
  });
75500
75710
  }
75501
75711
  return _createClass(Layout, [{
@@ -75637,6 +75847,14 @@ var Layout = /*#__PURE__*/function () {
75637
75847
  if ($splashScreen) {
75638
75848
  $splashScreen.style.display = 'none';
75639
75849
  }
75850
+ // 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.
75851
+ console.debug('??? XLR.debug >> Layout::run() - Checking if layout container is still in the DOM before playing regions...', {
75852
+ layoutId: this.id,
75853
+ layoutContainerExists: !!$layoutContainer,
75854
+ $layoutContainer: $layoutContainer,
75855
+ layoutIndex: this.index,
75856
+ shouldParse: false
75857
+ });
75640
75858
  if ($layoutContainer) {
75641
75859
  $layoutContainer.style.setProperty('visibility', 'visible');
75642
75860
  $layoutContainer.style.setProperty('opacity', '1');
@@ -75667,19 +75885,19 @@ var Layout = /*#__PURE__*/function () {
75667
75885
  key: "regionExpired",
75668
75886
  value: function regionExpired() {
75669
75887
  this.allExpired = true;
75670
- var _iterator = _createForOfIteratorHelper(this.regions),
75671
- _step;
75888
+ var _iterator3 = _createForOfIteratorHelper(this.regions),
75889
+ _step3;
75672
75890
  try {
75673
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
75674
- var layoutRegion = _step.value;
75891
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
75892
+ var layoutRegion = _step3.value;
75675
75893
  if (!layoutRegion.complete) {
75676
75894
  this.allExpired = false;
75677
75895
  }
75678
75896
  }
75679
75897
  } catch (err) {
75680
- _iterator.e(err);
75898
+ _iterator3.e(err);
75681
75899
  } finally {
75682
- _iterator.f();
75900
+ _iterator3.f();
75683
75901
  }
75684
75902
  if (this.allExpired) {
75685
75903
  this.end();
@@ -75690,17 +75908,17 @@ var Layout = /*#__PURE__*/function () {
75690
75908
  value: function end() {
75691
75909
  console.debug('Executing Layout::end and Calling Region::end ', this);
75692
75910
  /* Ask the layout to gracefully stop running now */
75693
- var _iterator2 = _createForOfIteratorHelper(this.regions),
75694
- _step2;
75911
+ var _iterator4 = _createForOfIteratorHelper(this.regions),
75912
+ _step4;
75695
75913
  try {
75696
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
75697
- var layoutRegion = _step2.value;
75914
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
75915
+ var layoutRegion = _step4.value;
75698
75916
  layoutRegion.end();
75699
75917
  }
75700
75918
  } catch (err) {
75701
- _iterator2.e(err);
75919
+ _iterator4.e(err);
75702
75920
  } finally {
75703
- _iterator2.f();
75921
+ _iterator4.f();
75704
75922
  }
75705
75923
  }
75706
75924
  }, {
@@ -75713,7 +75931,11 @@ var Layout = /*#__PURE__*/function () {
75713
75931
  }
75714
75932
  }
75715
75933
  if (this.allEnded) {
75716
- console.debug('starting to end layout . . .');
75934
+ console.debug('starting to end layout . . .', {
75935
+ layoutId: this.layoutId,
75936
+ layoutIndex: this.index,
75937
+ layoutState: this.state
75938
+ });
75717
75939
  if (this.xlr.config.platform === ConsumerPlatform.CMS) {
75718
75940
  var $end = document.getElementById('play_ended');
75719
75941
  var $preview = document.getElementById('screen_container');
@@ -75812,6 +76034,42 @@ var Layout = /*#__PURE__*/function () {
75812
76034
  (_$layout$parentElemen2 = $layout.parentElement) === null || _$layout$parentElemen2 === void 0 || _$layout$parentElemen2.removeChild($layout);
75813
76035
  }
75814
76036
  }
76037
+ }, {
76038
+ key: "discardLayout",
76039
+ value: function discardLayout() {
76040
+ var caller = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : LayoutPlaybackType.NEXT;
76041
+ // Dispose any video.js players that were initialized during prepareVideoMedia
76042
+ // but never played. The isDisposed() guard makes this safe to call on
76043
+ // layouts that were fully played as well.
76044
+ var _iterator5 = _createForOfIteratorHelper(this.regions),
76045
+ _step5;
76046
+ try {
76047
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
76048
+ var region = _step5.value;
76049
+ var _iterator6 = _createForOfIteratorHelper(region.mediaObjects),
76050
+ _step6;
76051
+ try {
76052
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
76053
+ var media = _step6.value;
76054
+ if (media.player && !media.player.isDisposed()) {
76055
+ media.player.dispose();
76056
+ media.player = undefined;
76057
+ media.html = null;
76058
+ }
76059
+ }
76060
+ } catch (err) {
76061
+ _iterator6.e(err);
76062
+ } finally {
76063
+ _iterator6.f();
76064
+ }
76065
+ }
76066
+ } catch (err) {
76067
+ _iterator5.e(err);
76068
+ } finally {
76069
+ _iterator5.f();
76070
+ }
76071
+ this.removeLayout(caller);
76072
+ }
75815
76073
  }, {
75816
76074
  key: "getXlf",
75817
76075
  value: function getXlf() {
@@ -76054,6 +76312,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76054
76312
  }());
76055
76313
  xlrObject.on('updateLoop', /*#__PURE__*/function () {
76056
76314
  var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(inputLayouts) {
76315
+ var _xlrObject$currentLay;
76057
76316
  return _regeneratorRuntime().wrap(function _callee3$(_context3) {
76058
76317
  while (1) switch (_context3.prev = _context3.next) {
76059
76318
  case 0:
@@ -76062,7 +76321,17 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76062
76321
  return xlrObject.updateLoop(inputLayouts);
76063
76322
  case 3:
76064
76323
  xlrObject.isUpdatingLoop = false;
76065
- case 4:
76324
+ // If the running layout finished while isUpdatingLoop was true, the
76325
+ // layout end-handler bailed out of prepareLayouts() early and the
76326
+ // subsequent playLayouts() call saw currentLayout.done === true and
76327
+ // skipped run() — leaving a black screen. Catch up now that the flag
76328
+ // is clear.
76329
+ if ((_xlrObject$currentLay = xlrObject.currentLayout) !== null && _xlrObject$currentLay !== void 0 && _xlrObject$currentLay.done) {
76330
+ xlrObject.prepareLayouts().then(function (xlr) {
76331
+ return xlrObject.playLayouts(xlr);
76332
+ });
76333
+ }
76334
+ case 5:
76066
76335
  case "end":
76067
76336
  return _context3.stop();
76068
76337
  }
@@ -76092,40 +76361,35 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76092
76361
  return _ref4.apply(this, arguments);
76093
76362
  };
76094
76363
  }());
76095
- /**
76096
- * Asynchronous event emitter. Extended nanoevents event emitter.
76097
- *
76098
- * NOTE: Known limitation — nanoevents emit() is synchronous.
76099
- * Any async event handlers registered via .on() are fire-and-forget;
76100
- * emitSync does NOT await them. The returned Promise resolves
76101
- * immediately after handlers are invoked, not after they complete.
76102
- *
76103
- * @param eventName
76104
- * @param args
76105
- */
76106
- xlrObject.emitSync = function (eventName) {
76107
- for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
76108
- args[_key - 1] = arguments[_key];
76109
- }
76110
- return new Promise( /*#__PURE__*/function () {
76111
- var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(resolve) {
76112
- var _xlrObject$emitter;
76113
- return _regeneratorRuntime().wrap(function _callee5$(_context5) {
76114
- while (1) switch (_context5.prev = _context5.next) {
76115
- case 0:
76116
- (_xlrObject$emitter = xlrObject.emitter).emit.apply(_xlrObject$emitter, [eventName].concat(args));
76117
- resolve();
76118
- case 2:
76119
- case "end":
76120
- return _context5.stop();
76121
- }
76122
- }, _callee5);
76123
- }));
76124
- return function (_x4) {
76125
- return _ref5.apply(this, arguments);
76126
- };
76127
- }());
76128
- };
76364
+ xlrObject.emitSync = /*#__PURE__*/function () {
76365
+ var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(eventName) {
76366
+ var _xlrObject$emitter$ev;
76367
+ var _len,
76368
+ args,
76369
+ _key,
76370
+ handlers,
76371
+ _args5 = arguments;
76372
+ return _regeneratorRuntime().wrap(function _callee5$(_context5) {
76373
+ while (1) switch (_context5.prev = _context5.next) {
76374
+ case 0:
76375
+ for (_len = _args5.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
76376
+ args[_key - 1] = _args5[_key];
76377
+ }
76378
+ handlers = (_xlrObject$emitter$ev = xlrObject.emitter.events[eventName]) !== null && _xlrObject$emitter$ev !== void 0 ? _xlrObject$emitter$ev : [];
76379
+ _context5.next = 4;
76380
+ return Promise.all(handlers.map(function (handler) {
76381
+ return handler.apply(void 0, args);
76382
+ }));
76383
+ case 4:
76384
+ case "end":
76385
+ return _context5.stop();
76386
+ }
76387
+ }, _callee5);
76388
+ }));
76389
+ return function (_x4) {
76390
+ return _ref5.apply(this, arguments);
76391
+ };
76392
+ }();
76129
76393
  xlrObject.bootstrap = function () {
76130
76394
  // Place to set configurations and initialize required props
76131
76395
  var self = this;
@@ -76160,12 +76424,21 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76160
76424
  if ($splashScreen && $splashScreen.style.display === 'block') {
76161
76425
  $splashScreen === null || $splashScreen === void 0 || $splashScreen.hide();
76162
76426
  }
76427
+ console.debug('>>>> XLR.debug XLR::playLayouts > currentLayout', {
76428
+ layoutId: xlr.currentLayout.layoutId,
76429
+ layoutIndex: xlr.currentLayout.index,
76430
+ layoutState: xlr.currentLayout.state
76431
+ });
76163
76432
  if (!xlr.currentLayout.done) {
76164
76433
  // Hide overlays when current layout is interrupt
76165
76434
  if (xlr.currentLayout.isInterrupt()) {
76166
76435
  xlrObject.overlayLayoutManager.stopOverlays();
76167
76436
  }
76168
- console.log('>>>> XLR.debug XLR::playSchedules > Running currentLayout', xlr.currentLayout);
76437
+ console.debug('>>>> XLR.debug XLR::playLayouts > Running currentLayout', {
76438
+ layoutId: xlr.currentLayout.layoutId,
76439
+ layoutIndex: xlr.currentLayout.index,
76440
+ layoutState: xlr.currentLayout.state
76441
+ });
76169
76442
  xlr.currentLayout.run();
76170
76443
  }
76171
76444
  } else {
@@ -76243,7 +76516,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76243
76516
  _context7.next = 10;
76244
76517
  return _overlay.finishAllRegions();
76245
76518
  case 10:
76246
- _overlay.removeLayout();
76519
+ _overlay.removeLayout(LayoutPlaybackType.OVERLAY);
76247
76520
  case 11:
76248
76521
  _context7.next = 14;
76249
76522
  break;
@@ -76309,6 +76582,31 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76309
76582
  var $layout = document.querySelector("#".concat(containerName, "[data-sequence=\"").concat(layoutIndex, "\"]"));
76310
76583
  return $layout !== null;
76311
76584
  };
76585
+ // Scans screen_container for non-overlay layout divs and removes any that
76586
+ // are not the current or next active layout. Prevents DOM accumulation when
76587
+ // prepareLayouts() races with updateLoop and multiple same-layoutId elements
76588
+ // end up in screen_container (e.g. transitioning from a 1-layout loop where
76589
+ // two elements exist for the same layout to a multi-layout schedule).
76590
+ // keepCurrent / keepNext:
76591
+ // undefined → fall back to this.currentLayout / this.nextLayout
76592
+ // null → keep nothing for that slot (explicit "no layout to preserve")
76593
+ // ILayout → keep exactly that instance
76594
+ xlrObject.cleanupOrphanedLayouts = function (keepCurrent, keepNext) {
76595
+ var $screen = document.getElementById('screen_container');
76596
+ if (!$screen) return;
76597
+ var current = keepCurrent !== undefined ? keepCurrent : this.currentLayout;
76598
+ var next = keepNext !== undefined ? keepNext : this.nextLayout;
76599
+ Array.from($screen.querySelectorAll(':scope > div:not(.is-overlay)')).forEach(function (el) {
76600
+ var div = el;
76601
+ var isCurrentLayout = current && div.id === current.containerName && div.dataset.sequence === String(current.index);
76602
+ var isNextLayout = next && div.id === next.containerName && div.dataset.sequence === String(next.index);
76603
+ if (!isCurrentLayout && !isNextLayout) {
76604
+ var _div$parentElement;
76605
+ console.debug('XLR::cleanupOrphanedLayouts - removing orphaned layout element', div.id);
76606
+ (_div$parentElement = div.parentElement) === null || _div$parentElement === void 0 || _div$parentElement.removeChild(div);
76607
+ }
76608
+ });
76609
+ };
76312
76610
  xlrObject.updateLoop = /*#__PURE__*/function () {
76313
76611
  var _ref10 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee11(inputLayouts) {
76314
76612
  var _this$currentLayout,
@@ -76355,11 +76653,11 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76355
76653
  };
76356
76654
  }();
76357
76655
  if (isCurrentLayoutValid) {
76358
- _context11.next = 54;
76656
+ _context11.next = 55;
76359
76657
  break;
76360
76658
  }
76361
76659
  if (!playback.hasDefaultOnly) {
76362
- _context11.next = 33;
76660
+ _context11.next = 34;
76363
76661
  break;
76364
76662
  }
76365
76663
  if (!(this.currentLayout && playback.currentLayout && this.currentLayout.layoutId !== playback.currentLayout.layoutId)) {
@@ -76372,82 +76670,102 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76372
76670
  case 19:
76373
76671
  this.currentLayout.removeLayout();
76374
76672
  case 20:
76375
- _context11.next = 22;
76673
+ // Discard old nextLayout before replacing it — same as the
76674
+ // other two branches do, otherwise the prepared DOM element
76675
+ // and any video.js players are orphaned.
76676
+ if (this.nextLayout && this.isLayoutInDOM(this.nextLayout.containerName, this.nextLayout.index)) {
76677
+ this.nextLayout.discardLayout(LayoutPlaybackType.NEXT);
76678
+ }
76679
+ _context11.next = 23;
76376
76680
  return this.prepareLayoutXlf(playback.currentLayout);
76377
- case 22:
76681
+ case 23:
76378
76682
  this.currentLayout = _context11.sent;
76379
76683
  this.currentLayoutId = this.currentLayout.layoutId;
76380
76684
  _context11.t0 = this;
76381
- _context11.next = 27;
76685
+ _context11.next = 28;
76382
76686
  return this.prepareLayoutXlf(playback.nextLayout);
76383
- case 27:
76687
+ case 28:
76384
76688
  _context11.t1 = _context11.sent;
76385
- _context11.next = 30;
76689
+ _context11.next = 31;
76386
76690
  return _context11.t0.prepareForSsp.call(_context11.t0, _context11.t1);
76387
- case 30:
76691
+ case 31:
76388
76692
  this.nextLayout = _context11.sent;
76389
- _context11.next = 50;
76693
+ _context11.next = 51;
76390
76694
  break;
76391
- case 33:
76695
+ case 34:
76392
76696
  if (!(this.currentLayout && this.isLayoutInDOM(this.currentLayout.containerName, this.currentLayout.index))) {
76393
- _context11.next = 38;
76697
+ _context11.next = 39;
76394
76698
  break;
76395
76699
  }
76396
76700
  this.currentLayout.inLoop = false;
76397
- _context11.next = 37;
76701
+ _context11.next = 38;
76398
76702
  return this.currentLayout.finishAllRegions();
76399
- case 37:
76400
- this.currentLayout.removeLayout();
76401
76703
  case 38:
76704
+ this.currentLayout.removeLayout();
76705
+ case 39:
76402
76706
  if (this.nextLayout && this.isLayoutInDOM(this.nextLayout.containerName, this.nextLayout.index)) {
76403
- this.nextLayout.removeLayout();
76707
+ this.nextLayout.discardLayout(LayoutPlaybackType.NEXT);
76404
76708
  }
76405
76709
  if (!playback.currentLayout) {
76406
- _context11.next = 42;
76710
+ _context11.next = 43;
76407
76711
  break;
76408
76712
  }
76409
- _context11.next = 42;
76713
+ _context11.next = 43;
76410
76714
  return prepareNewCurrentLayout();
76411
- case 42:
76715
+ case 43:
76412
76716
  if (!playback.nextLayout) {
76413
- _context11.next = 50;
76717
+ _context11.next = 51;
76414
76718
  break;
76415
76719
  }
76416
76720
  _context11.t2 = this;
76417
- _context11.next = 46;
76721
+ _context11.next = 47;
76418
76722
  return this.prepareLayoutXlf(playback.nextLayout);
76419
- case 46:
76723
+ case 47:
76420
76724
  _context11.t3 = _context11.sent;
76421
- _context11.next = 49;
76725
+ _context11.next = 50;
76422
76726
  return _context11.t2.prepareForSsp.call(_context11.t2, _context11.t3);
76423
- case 49:
76424
- this.nextLayout = _context11.sent;
76425
76727
  case 50:
76426
- _context11.next = 52;
76728
+ this.nextLayout = _context11.sent;
76729
+ case 51:
76730
+ _context11.next = 53;
76427
76731
  return this.playSchedules(this);
76428
- case 52:
76732
+ case 53:
76429
76733
  _context11.next = 67;
76430
76734
  break;
76431
- case 54:
76735
+ case 55:
76432
76736
  // Remove next layout if it is in the DOM
76433
76737
  if (this.nextLayout && this.isLayoutInDOM(this.nextLayout.containerName, this.nextLayout.index)) {
76434
- this.nextLayout.removeLayout();
76738
+ this.nextLayout.discardLayout(LayoutPlaybackType.NEXT);
76435
76739
  }
76436
- // Prepare new current layout
76740
+ // Purge any other orphaned layouts from screen_container that belong
76741
+ // to the old single-layout loop. When there was only one layout in the
76742
+ // loop, prepareLayouts() kept two DOM elements alive (current + next,
76743
+ // both the same layoutId but different containerNames). On a schedule
76744
+ // change the this.nextLayout check above only discards the element
76745
+ // currently referenced by this.nextLayout, but concurrent
76746
+ // prepareLayouts() calls can leave earlier same-layoutId elements
76747
+ // behind.
76748
+ // Pass null (not undefined) for keepNext: undefined would fall back to
76749
+ // this.nextLayout which may still reference the just-discarded layout
76750
+ // or — if isLayoutInDOM returned false and discardLayout was skipped —
76751
+ // the orphan itself, causing cleanupOrphanedLayouts to preserve it.
76752
+ // null means "no next to keep"; we are about to prepare a fresh one.
76753
+ this.cleanupOrphanedLayouts(this.currentLayout, null);
76754
+ // The current layout is still valid and running — do NOT replace the
76755
+ // live currentLayout object. Only refresh the queued nextLayout so
76756
+ // that when the running layout finishes it transitions to the correct
76757
+ // position in the updated loop. Using playback.currentLayout (the
76758
+ // slot that follows the running layout in the new queue) as the new
76759
+ // nextLayout keeps the cycle in order; the slot after that will be
76760
+ // prepared by the normal prepareLayouts() call at transition time.
76437
76761
  if (!playback.currentLayout) {
76438
- _context11.next = 58;
76439
- break;
76440
- }
76441
- _context11.next = 58;
76442
- return prepareNewCurrentLayout();
76443
- case 58:
76444
- if (!playback.nextLayout) {
76445
76762
  _context11.next = 66;
76446
76763
  break;
76447
76764
  }
76765
+ this.currentLayoutIndex = playback.currentLayoutIndex;
76448
76766
  _context11.t4 = this;
76449
76767
  _context11.next = 62;
76450
- return this.prepareLayoutXlf(playback.nextLayout);
76768
+ return this.prepareLayoutXlf(playback.currentLayout);
76451
76769
  case 62:
76452
76770
  _context11.t5 = _context11.sent;
76453
76771
  _context11.next = 65;
@@ -76553,8 +76871,21 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76553
76871
  _nextLayout = setLayoutIndex(_nextLayout, _nextLayoutIndex);
76554
76872
  }
76555
76873
  } else {
76874
+ var _this$currentLayout4, _this$currentLayout5;
76556
76875
  _currentLayout = this.nextLayout;
76557
76876
  _currentLayoutIndex = _currentLayout.index;
76877
+ // updateLoop can re-queue the same index that is currently
76878
+ // playing (e.g. it fires while nextLayout.index === currentLayout.index).
76879
+ // When that layout then ends, the catch-up prepareLayouts() would
76880
+ // replay the same slot instead of advancing. Detect this by checking
76881
+ // whether the queued next-to-current is at the same index as the
76882
+ // layout that just finished, and advance past it so the following
76883
+ // slot (e.g. an SSP that now has an ad) becomes current instead.
76884
+ 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)) {
76885
+ _currentLayoutIndex = (_currentLayoutIndex + 1) % this.inputLayouts.length;
76886
+ _currentLayout = this.getLayout(this.inputLayouts[_currentLayoutIndex]);
76887
+ _currentLayout = setLayoutIndex(_currentLayout, _currentLayoutIndex);
76888
+ }
76558
76889
  _nextLayoutIndex = (_currentLayoutIndex + 1) % this.inputLayouts.length;
76559
76890
  _nextLayout = this.getLayout(this.inputLayouts[_nextLayoutIndex]);
76560
76891
  _nextLayout = setLayoutIndex(_nextLayout, _nextLayoutIndex);
@@ -76610,6 +76941,10 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76610
76941
  var activeLayout = inputLayout;
76611
76942
  if (isCMS) {
76612
76943
  activeLayout.index = 0;
76944
+ // id stays null without this — setLayoutIndex returns undefined for CMS layouts
76945
+ if (activeLayout.id == null) {
76946
+ activeLayout.id = activeLayout.layoutId;
76947
+ }
76613
76948
  } else {
76614
76949
  activeLayout = _objectSpread2({}, this.uniqueLayouts[inputLayout.layoutId]);
76615
76950
  }
@@ -76640,7 +76975,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76640
76975
  };
76641
76976
  xlrObject.prepareLayouts = /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee14() {
76642
76977
  var _layoutPlayback$curre, _layoutPlayback$curre2;
76643
- var self, layoutPlayback, currentLayoutXlf, nextLayoutXlf, layouts;
76978
+ var self, layoutPlayback, currentLayoutXlf, wasCurrentReused, nextLayoutXlf, layouts;
76644
76979
  return _regeneratorRuntime().wrap(function _callee14$(_context14) {
76645
76980
  while (1) switch (_context14.prev = _context14.next) {
76646
76981
  case 0:
@@ -76663,7 +76998,12 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76663
76998
  shouldParse: false
76664
76999
  });
76665
77000
  self.currentLayoutId = (_layoutPlayback$curre = layoutPlayback.currentLayout) === null || _layoutPlayback$curre === void 0 ? void 0 : _layoutPlayback$curre.layoutId;
76666
- if (!((_layoutPlayback$curre2 = layoutPlayback.currentLayout) !== null && _layoutPlayback$curre2 !== void 0 && _layoutPlayback$curre2.layoutNode)) {
77001
+ // Only reuse the existing Layout instance if it is fully healthy
77002
+ // a done=true instance was removed from the DOM (e.g. an SSP slot that
77003
+ // had no ad), and an empty-XLF instance has no regions so it can never
77004
+ // advance the cycle. In either case re-prepare from scratch so we get
77005
+ // a fresh request (which may now have a valid ad / XLF).
77006
+ if (!((_layoutPlayback$curre2 = layoutPlayback.currentLayout) !== null && _layoutPlayback$curre2 !== void 0 && _layoutPlayback$curre2.layoutNode && !layoutPlayback.currentLayout.done && layoutPlayback.currentLayout.xlfString !== '')) {
76667
77007
  _context14.next = 12;
76668
77008
  break;
76669
77009
  }
@@ -76677,27 +77017,40 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76677
77017
  _context14.t0 = _context14.sent;
76678
77018
  case 15:
76679
77019
  currentLayoutXlf = _context14.t0;
76680
- _context14.next = 18;
77020
+ // True when the same object was returned (reused); false when a fresh
77021
+ // Layout was constructed by prepareLayoutXlf above.
77022
+ wasCurrentReused = currentLayoutXlf === layoutPlayback.currentLayout;
77023
+ _context14.next = 19;
76681
77024
  return self.prepareLayoutXlf(layoutPlayback.nextLayout);
76682
- case 18:
77025
+ case 19:
76683
77026
  nextLayoutXlf = _context14.sent;
76684
77027
  _context14.t1 = Promise;
76685
77028
  _context14.t2 = currentLayoutXlf;
76686
- _context14.next = 23;
77029
+ _context14.next = 24;
76687
77030
  return self.prepareForSsp(nextLayoutXlf);
76688
- case 23:
77031
+ case 24:
76689
77032
  _context14.t3 = _context14.sent;
76690
77033
  _context14.t4 = [_context14.t2, _context14.t3];
76691
- _context14.next = 27;
77034
+ _context14.next = 28;
76692
77035
  return _context14.t1.all.call(_context14.t1, _context14.t4);
76693
- case 27:
77036
+ case 28:
76694
77037
  layouts = _context14.sent;
76695
- // Return early when layout loop is updating
76696
- if (self.isUpdatingLoop) {
76697
- if (layoutPlayback.nextLayout && nextLayoutXlf && this.isLayoutInDOM(nextLayoutXlf.containerName, nextLayoutXlf.index)) {
76698
- nextLayoutXlf.removeLayout();
76699
- }
77038
+ if (!(self.isUpdatingLoop || layouts[0].done)) {
77039
+ _context14.next = 33;
77040
+ break;
77041
+ }
77042
+ // If currentLayout was freshly prepared (not reused from nextLayout),
77043
+ // its DOM element was just appended — discard it now so it does not
77044
+ // accumulate in screen_container. Also disposes any video.js players
77045
+ // that were initialized during prepareVideoMedia but never played.
77046
+ if (!wasCurrentReused && this.isLayoutInDOM(currentLayoutXlf.containerName, currentLayoutXlf.index)) {
77047
+ currentLayoutXlf.discardLayout(LayoutPlaybackType.NEXT);
76700
77048
  }
77049
+ if (layoutPlayback.nextLayout && nextLayoutXlf && this.isLayoutInDOM(nextLayoutXlf.containerName, nextLayoutXlf.index)) {
77050
+ nextLayoutXlf.discardLayout(LayoutPlaybackType.NEXT);
77051
+ }
77052
+ return _context14.abrupt("return", Promise.resolve(self));
77053
+ case 33:
76701
77054
  console.debug('>>>>> XLR.debug prepared layout XLF', layouts);
76702
77055
  return _context14.abrupt("return", new Promise( /*#__PURE__*/function () {
76703
77056
  var _ref14 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee13(resolve) {
@@ -76714,8 +77067,15 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76714
77067
  self.currentLayout = self.layouts.current;
76715
77068
  self.currentLayoutId = self.currentLayout.layoutId;
76716
77069
  self.nextLayout = self.layouts.next;
77070
+ // Evict any orphaned layout DOM elements that aren't the current
77071
+ // or next layout. Concurrent prepareLayouts() calls can each append
77072
+ // a freshly-prepared nextLayout to screen_container and then
77073
+ // overwrite this.nextLayout, leaving earlier elements behind.
77074
+ // Calling this here — with explicit references — ensures every
77075
+ // completed prepare cycle leaves the DOM in a clean state.
77076
+ self.cleanupOrphanedLayouts(self.currentLayout, self.nextLayout);
76717
77077
  resolve(xlrObject);
76718
- case 8:
77078
+ case 9:
76719
77079
  case "end":
76720
77080
  return _context13.stop();
76721
77081
  }
@@ -76725,7 +77085,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76725
77085
  return _ref14.apply(this, arguments);
76726
77086
  };
76727
77087
  }()));
76728
- case 31:
77088
+ case 35:
76729
77089
  case "end":
76730
77090
  return _context14.stop();
76731
77091
  }
@@ -76753,34 +77113,38 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76753
77113
  newOptions.xlfUrl = inputLayout.path;
76754
77114
  }
76755
77115
  if (!(inputLayout && inputLayout.layoutNode === undefined)) {
76756
- _context15.next = 21;
77116
+ _context15.next = 22;
76757
77117
  break;
76758
77118
  }
76759
77119
  if (!(inputLayout.layoutId === -1)) {
76760
- _context15.next = 14;
77120
+ _context15.next = 15;
76761
77121
  break;
76762
77122
  }
76763
77123
  _context15.next = 10;
76764
77124
  return self.emitSync('adRequest', inputLayout.index);
76765
77125
  case 10:
76766
77126
  sspInputLayout = self.inputLayouts[inputLayout.index];
77127
+ console.debug('XLR::prepareLayoutXlf > SSP input layout', {
77128
+ sspInputLayout: sspInputLayout,
77129
+ inputLayout: inputLayout
77130
+ });
76767
77131
  // @ts-ignore
76768
- layoutXlf = ((_sspInputLayout = sspInputLayout) === null || _sspInputLayout === void 0 ? void 0 : _sspInputLayout.getXlf()) || '';
76769
- _context15.next = 17;
77132
+ layoutXlf = typeof ((_sspInputLayout = sspInputLayout) === null || _sspInputLayout === void 0 ? void 0 : _sspInputLayout.getXlf) === 'function' ? sspInputLayout.getXlf() : '';
77133
+ _context15.next = 18;
76770
77134
  break;
76771
- case 14:
76772
- _context15.next = 16;
77135
+ case 15:
77136
+ _context15.next = 17;
76773
77137
  return getXlf(newOptions);
76774
- case 16:
76775
- layoutXlf = _context15.sent;
76776
77138
  case 17:
77139
+ layoutXlf = _context15.sent;
77140
+ case 18:
76777
77141
  parser = new window.DOMParser();
76778
77142
  layoutXlfNode = parser.parseFromString(layoutXlf, 'text/xml');
76779
- _context15.next = 22;
77143
+ _context15.next = 23;
76780
77144
  break;
76781
- case 21:
76782
- layoutXlfNode = inputLayout && inputLayout.layoutNode;
76783
77145
  case 22:
77146
+ layoutXlfNode = inputLayout && inputLayout.layoutNode;
77147
+ case 23:
76784
77148
  isOverlayLayout = !!(inputLayout !== null && inputLayout !== void 0 && inputLayout.isOverlay);
76785
77149
  return _context15.abrupt("return", new Promise(function (resolve) {
76786
77150
  var _inputLayout$ad;
@@ -76805,13 +77169,22 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76805
77169
  xlrLayoutObj.duration = sspInputLayout.duration || 0;
76806
77170
  xlrLayoutObj.ad = sspInputLayout.ad;
76807
77171
  }
77172
+ var xlrLayout;
76808
77173
  if (isOverlayLayout) {
76809
- resolve(new OverlayLayout(xlrLayoutObj, newOptions, self, layoutXlfNode));
77174
+ xlrLayout = new OverlayLayout(xlrLayoutObj, newOptions, self, layoutXlfNode);
76810
77175
  } else {
76811
- resolve(new Layout(xlrLayoutObj, newOptions, self, layoutXlfNode));
77176
+ xlrLayout = new Layout(xlrLayoutObj, newOptions, self, layoutXlfNode);
77177
+ }
77178
+ // Advance the shared counter so the next prepareLayoutXlf() call
77179
+ // starts from where this layout left off — prevents every layout
77180
+ // instance from reusing idCounter=1 and colliding on the same
77181
+ // containerName / DOM element.
77182
+ if (props.options) {
77183
+ props.options.idCounter = newOptions.idCounter;
76812
77184
  }
77185
+ resolve(xlrLayout);
76813
77186
  }));
76814
- case 24:
77187
+ case 25:
76815
77188
  case "end":
76816
77189
  return _context15.stop();
76817
77190
  }
@@ -76823,46 +77196,49 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76823
77196
  }();
76824
77197
  xlrObject.prepareForSsp = /*#__PURE__*/function () {
76825
77198
  var _ref16 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee16(nextLayout) {
76826
- var self, _nextLayout, nextIndex, inputLayout, nextLayoutObj;
77199
+ var self, _nextLayout, iterations, maxIterations, nextIndex, inputLayout, nextLayoutObj;
76827
77200
  return _regeneratorRuntime().wrap(function _callee16$(_context16) {
76828
77201
  while (1) switch (_context16.prev = _context16.next) {
76829
77202
  case 0:
76830
77203
  self = this;
76831
77204
  _nextLayout = nextLayout;
76832
- case 2:
76833
- if (!(_nextLayout && _nextLayout.xlfString === '')) {
76834
- _context16.next = 17;
76835
- break;
76836
- }
76837
- // Remove skipped layout
76838
- _nextLayout.removeLayout();
76839
- // Get next valid layout
76840
- // We will skip next layout that has no valid xlf
76841
- nextIndex = _nextLayout.index + 1;
76842
- if (!(nextIndex >= self.inputLayouts.length)) {
76843
- _context16.next = 7;
77205
+ iterations = 0;
77206
+ maxIterations = self.inputLayouts.length;
77207
+ case 4:
77208
+ if (!(_nextLayout && _nextLayout.xlfString === '' && iterations < maxIterations)) {
77209
+ _context16.next = 20;
76844
77210
  break;
76845
77211
  }
76846
- return _context16.abrupt("break", 17);
76847
- case 7:
77212
+ // Remove the empty slot's DOM element before skipping past it
77213
+ _nextLayout.removeLayout(LayoutPlaybackType.NEXT);
77214
+ iterations++;
77215
+ // Advance to the next slot, wrapping around so a trailing SSP slot
77216
+ // with no ad does not strand the queue at the end of the array.
77217
+ nextIndex = (_nextLayout.index + 1) % self.inputLayouts.length;
76848
77218
  inputLayout = self.inputLayouts[nextIndex];
76849
77219
  if (inputLayout) {
76850
- _context16.next = 10;
77220
+ _context16.next = 11;
76851
77221
  break;
76852
77222
  }
76853
- return _context16.abrupt("break", 17);
76854
- case 10:
77223
+ return _context16.abrupt("break", 20);
77224
+ case 11:
76855
77225
  nextLayoutObj = self.getLayout(inputLayout);
76856
77226
  nextLayoutObj = setLayoutIndex(nextLayoutObj, nextIndex);
76857
- _context16.next = 14;
77227
+ if (nextLayoutObj) {
77228
+ _context16.next = 15;
77229
+ break;
77230
+ }
77231
+ return _context16.abrupt("break", 20);
77232
+ case 15:
77233
+ _context16.next = 17;
76858
77234
  return self.prepareLayoutXlf(nextLayoutObj);
76859
- case 14:
77235
+ case 17:
76860
77236
  _nextLayout = _context16.sent;
76861
- _context16.next = 2;
77237
+ _context16.next = 4;
76862
77238
  break;
76863
- case 17:
77239
+ case 20:
76864
77240
  return _context16.abrupt("return", _nextLayout);
76865
- case 18:
77241
+ case 21:
76866
77242
  case "end":
76867
77243
  return _context16.stop();
76868
77244
  }
@@ -76874,7 +77250,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76874
77250
  }();
76875
77251
  xlrObject.gotoPrevLayout = /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee18() {
76876
77252
  var _this4 = this;
76877
- var _currentLayoutIndex, _assumedPrevIndex, _this$currentLayout4;
77253
+ var _currentLayoutIndex, _assumedPrevIndex, _this$currentLayout6;
76878
77254
  return _regeneratorRuntime().wrap(function _callee18$(_context18) {
76879
77255
  while (1) switch (_context18.prev = _context18.next) {
76880
77256
  case 0:
@@ -76897,7 +77273,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76897
77273
  break;
76898
77274
  }
76899
77275
  _context18.next = 8;
76900
- return (_this$currentLayout4 = this.currentLayout) === null || _this$currentLayout4 === void 0 ? void 0 : _this$currentLayout4.finishAllRegions();
77276
+ return (_this$currentLayout6 = this.currentLayout) === null || _this$currentLayout6 === void 0 ? void 0 : _this$currentLayout6.finishAllRegions();
76901
77277
  case 8:
76902
77278
  // and set the previous layout as current layout
76903
77279
  this.currentLayoutIndex = _assumedPrevIndex;
@@ -76925,7 +77301,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76925
77301
  }, _callee18, this);
76926
77302
  }));
76927
77303
  xlrObject.gotoNextLayout = /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee19() {
76928
- var _xlrObject$currentLay;
77304
+ var _xlrObject$currentLay2;
76929
77305
  return _regeneratorRuntime().wrap(function _callee19$(_context19) {
76930
77306
  while (1) switch (_context19.prev = _context19.next) {
76931
77307
  case 0:
@@ -76935,7 +77311,7 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76935
77311
  shouldParse: false
76936
77312
  });
76937
77313
  _context19.next = 3;
76938
- return (_xlrObject$currentLay = xlrObject.currentLayout) === null || _xlrObject$currentLay === void 0 ? void 0 : _xlrObject$currentLay.finishAllRegions();
77314
+ return (_xlrObject$currentLay2 = xlrObject.currentLayout) === null || _xlrObject$currentLay2 === void 0 ? void 0 : _xlrObject$currentLay2.finishAllRegions();
76939
77315
  case 3:
76940
77316
  case "end":
76941
77317
  return _context19.stop();
@@ -76947,11 +77323,16 @@ function XiboLayoutRenderer(inputLayouts, overlays, options) {
76947
77323
  if (layout !== null) {
76948
77324
  layout.index = xlrInputLayout.index;
76949
77325
  }
77326
+ console.debug('XLR::updateInputLayout', {
77327
+ layoutIndex: layoutIndex,
77328
+ layout: layout,
77329
+ xlrInputLayout: xlrInputLayout
77330
+ });
76950
77331
  this.inputLayouts[layoutIndex] = layout || xlrInputLayout;
76951
77332
  };
76952
77333
  xlrObject.bootstrap();
76953
77334
  return xlrObject;
76954
77335
  }
76955
77336
 
76956
- export { Action, ActionsWrapper, AudioMedia, ConsumerPlatform, ELayoutState, ELayoutType, LayoutPlaybackType, Media, MediaState, Region, VideoMedia, audioFileType, capitalizeStr, composeBgUrlByPlatform, composeMediaUrl, composeResourceUrl, composeResourceUrlByPlatform, composeVideoSource, createMediaElement, XiboLayoutRenderer as default, defaultTrans, defaultVjsOpts, fadeInElem, fadeOutElem, fetchJSON, fetchText, flyInElem, flyOutElem, flyTransitionKeyframes, getAllAttributes, getDataBlob, getFileExt, getIndexByLayoutId, getLayout, getLayoutIndexByLayoutId, getMediaId, getXlf, hasDefaultOnly, hasSspLayout, initRenderingDOM, initialLayout, initialMedia, initialRegion, initialXlr, isEmpty, isLayoutValid, nextId, playerReportFault, preloadMediaBlob, prepareAudioMedia, prepareHtmlMedia, prepareImageMedia, prepareVideoMedia, setExpiry, transitionElement, videoFileType, vjsDefaultOptions };
77337
+ export { Action, ActionsWrapper, AudioMedia, ConsumerPlatform, ELayoutState, ELayoutType, FaultCodes, LayoutPlaybackType, Media, MediaState, Region, VideoMedia, audioFileType, capitalizeStr, composeBgUrlByPlatform, composeMediaUrl, composeResourceUrl, composeResourceUrlByPlatform, composeVideoSource, createMediaElement, XiboLayoutRenderer as default, defaultTrans, defaultVjsOpts, fadeInElem, fadeOutElem, fetchJSON, fetchText, flyInElem, flyOutElem, flyTransitionKeyframes, getAllAttributes, getDataBlob, getFileExt, getIndexByLayoutId, getLayout, getLayoutIndexByLayoutId, getMediaId, getXlf, hasDefaultOnly, hasSspLayout, initRenderingDOM, initialLayout, initialRegion, initialXlr, isEmpty, isLayoutValid, isMediaActive, nextId, playerReportFault, preloadMediaBlob, prepareAudioMedia, prepareHtmlMedia, prepareImageMedia, prepareVideoMedia, reportToPlayerPlatform, setExpiry, transitionElement, videoFileType, vjsDefaultOptions };
76957
77338
  //# sourceMappingURL=xibo-layout-renderer.esm.js.map