@xibosignage/xibo-layout-renderer 1.0.28 → 1.0.29

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.
@@ -73575,7 +73575,7 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
73575
73575
  cssText += "\n visibility: hidden;\n opacity: 0;\n z-index: 0;\n ";
73576
73576
  }
73577
73577
  $media.style.cssText = cssText;
73578
- if (self.render === 'html' || self.mediaType === 'ticker' || self.mediaType === 'webpage') {
73578
+ if (self.mediaType !== 'spacer' && (self.render === 'html' || self.mediaType === 'ticker' || self.mediaType === 'webpage')) {
73579
73579
  self.checkIframeStatus = true;
73580
73580
  self.iframe = prepareIframe(self);
73581
73581
  } else if (self.mediaType === "image") {
@@ -73723,8 +73723,6 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
73723
73723
  region.html.appendChild(media.html);
73724
73724
  }
73725
73725
  function prepareHtmlMedia(media, region) {
73726
- // Set state as false ( for now )
73727
- media.ready = false;
73728
73726
  if (media.html) {
73729
73727
  var mediaId = getMediaId(media);
73730
73728
  // Clean up old copy of the media
@@ -73736,14 +73734,16 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
73736
73734
  mediaId: mediaId,
73737
73735
  mediaInRegion: mediaInRegion
73738
73736
  });
73739
- // Append iframe
73740
- media.html.innerHTML = '';
73741
- media.html.appendChild(media.iframe);
73742
73737
  if (!mediaInRegion) {
73743
- // Add fresh copy of the media into the region using the direct reference
73738
+ // Append iframe and insert into region only when not already in the DOM.
73739
+ // Calling innerHTML = '' when the element is already present detaches the
73740
+ // iframe, causing the browser to reload its src unnecessarily (e.g. when
73741
+ // a media was preloaded then skipped by a navigation action).
73742
+ media.html.innerHTML = '';
73743
+ media.html.appendChild(media.iframe);
73744
73744
  region.html.appendChild(media.html);
73745
- media.ready = true;
73746
73745
  }
73746
+ media.ready = true;
73747
73747
  }
73748
73748
  }
73749
73749
  exports.FaultCodes = void 0;
@@ -74051,7 +74051,12 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
74051
74051
  var _this$sspImpressionUr, _this$sspErrorUrls;
74052
74052
  _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);
74053
74053
  }
74054
- media.region.playNextMedia();
74054
+ // Only advance the region if this media is still the active one.
74055
+ // A user-triggered next/prev action may have already moved currMedia
74056
+ // on, in which case the timer firing here would cause a double-advance.
74057
+ if (media === media.region.currMedia) {
74058
+ media.region.playNextMedia();
74059
+ }
74055
74060
  });
74056
74061
  this.on('cancelled', function (media) {
74057
74062
  if (media.state === MediaState.CANCELLED) return;
@@ -74095,6 +74100,10 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
74095
74100
  key: "startMediaTimer",
74096
74101
  value: function startMediaTimer(media) {
74097
74102
  var _this2 = this;
74103
+ // Always reset the counter so a media replayed after cancellation runs
74104
+ // for its full duration rather than the residual time left from the
74105
+ // previous play.
74106
+ this.mediaTimeCount = 0;
74098
74107
  var preloadTimeMs = 2000;
74099
74108
  var preloadTimeBufferMs = media.duration * 1000 / 2 - preloadTimeMs;
74100
74109
  var isPreparingNextMedia = false;
@@ -74970,11 +74979,19 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
74970
74979
  nxtMedia: (_this$nxtMedia2 = this.nxtMedia) === null || _this$nxtMedia2 === void 0 ? void 0 : _this$nxtMedia2.containerName
74971
74980
  });
74972
74981
  if (!this.layout.isOverlay && crossedEnd) {
74982
+ var _this$currMedia8;
74973
74983
  this.finished();
74974
74984
  if (this.layout.allEnded) {
74975
74985
  console.debug('??? XLR.debug >> Region - playNextMedia - layout all ended');
74976
74986
  return;
74977
74987
  }
74988
+ // Freeze single-media HTML at its last state while waiting for other
74989
+ // regions to complete. The guard at the top only catches the second
74990
+ // call; this one catches the first completion when complete was just
74991
+ // set by finished() above.
74992
+ if (((_this$currMedia8 = this.currMedia) === null || _this$currMedia8 === void 0 ? void 0 : _this$currMedia8.render) === 'html' && this.totalMediaObjects === 1 && this.oldMedia === this.currMedia) {
74993
+ return;
74994
+ }
74978
74995
  }
74979
74996
  this.transitionNodes(this.oldMedia, this.currMedia);
74980
74997
  }
@@ -74984,11 +75001,18 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
74984
75001
  if (this.currentMediaIndex <= 0 || this.ended) {
74985
75002
  return;
74986
75003
  }
75004
+ var interruptedMedia = this.currMedia;
74987
75005
  this.oldMedia = this.currMedia;
74988
75006
  this.currentMediaIndex -= 1;
74989
75007
  this.currMedia = this.mediaObjects[this.currentMediaIndex];
74990
75008
  this.nxtMedia = this.mediaObjects[(this.currentMediaIndex + 1) % this.totalMediaObjects];
74991
75009
  this.complete = false;
75010
+ // Cancel the interrupted media after advancing currMedia, using the same
75011
+ // pattern as gotoMediaInRegion — emitting after the update ensures the
75012
+ // handler's (media === currMedia) guard correctly skips playNextMedia.
75013
+ if ((interruptedMedia === null || interruptedMedia === void 0 ? void 0 : interruptedMedia.state) === MediaState.PLAYING) {
75014
+ interruptedMedia.emitter.emit('cancelled', interruptedMedia);
75015
+ }
74992
75016
  console.debug('region::playPreviousMedia', this);
74993
75017
  this.transitionNodes(this.oldMedia, this.currMedia);
74994
75018
  }
@@ -75066,6 +75090,7 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75066
75090
  _defineProperty(this, "$actionControllerTitle", void 0);
75067
75091
  _defineProperty(this, "$actionsContainer", void 0);
75068
75092
  _defineProperty(this, "translations", {});
75093
+ _defineProperty(this, "keyboardHandler", null);
75069
75094
  this.parent = parent;
75070
75095
  this.actions = actions;
75071
75096
  this.options = options;
@@ -75230,7 +75255,7 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75230
75255
  this.parent.xlr.gotoPrevLayout();
75231
75256
  }
75232
75257
  }
75233
- /** Change media in region (next/previous) */
75258
+ /** Change media in region (next/previous) with wrap-around at boundaries. */
75234
75259
  }, {
75235
75260
  key: "gotoMediaInRegion",
75236
75261
  value: function gotoMediaInRegion(regionId, actionType) {
@@ -75238,15 +75263,34 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75238
75263
  regionId: regionId,
75239
75264
  actionType: actionType
75240
75265
  });
75241
- // Find target region
75242
75266
  this.parent.regions.forEach(function (regionObj) {
75243
- if (regionObj.id === regionId) {
75244
- if (actionType === 'next') {
75245
- regionObj.playNextMedia();
75246
- } else {
75247
- regionObj.playPreviousMedia();
75248
- }
75267
+ if (regionObj.id !== regionId || regionObj.ended) return;
75268
+ var total = regionObj.totalMediaObjects;
75269
+ if (total === 0) return;
75270
+ // Snapshot the currently-playing media before updating currMedia so
75271
+ // we can cancel it cleanly after the region state is advanced.
75272
+ var interruptedMedia = regionObj.currMedia;
75273
+ // Compute new index with wrap-around. We do NOT delegate to
75274
+ // playNextMedia() / playPreviousMedia() here because those carry
75275
+ // normal playlist-cycle semantics (finished(), regionExpired()) that
75276
+ // must not fire during user-driven navigation.
75277
+ var newIndex = actionType === 'next' ? (regionObj.currentMediaIndex + 1) % total : (regionObj.currentMediaIndex - 1 + total) % total;
75278
+ regionObj.oldMedia = regionObj.currMedia;
75279
+ regionObj.currentMediaIndex = newIndex;
75280
+ regionObj.currMedia = regionObj.mediaObjects[newIndex];
75281
+ regionObj.nxtMedia = regionObj.mediaObjects[(newIndex + 1) % total];
75282
+ regionObj.complete = false;
75283
+ // Properly cancel the interrupted media AFTER updating currMedia.
75284
+ // Using 'cancelled' rather than bare clearInterval ensures state is
75285
+ // reset from PLAYING and mediaTimeCount is zeroed — without this,
75286
+ // returning to a cancelled media causes run() → 'start' to bail on
75287
+ // the state === PLAYING guard, leaving the region stuck indefinitely.
75288
+ // currMedia is already updated so the handler's guard
75289
+ // (media === media.region.currMedia) correctly skips playNextMedia.
75290
+ if ((interruptedMedia === null || interruptedMedia === void 0 ? void 0 : interruptedMedia.state) === MediaState.PLAYING) {
75291
+ interruptedMedia.emitter.emit('cancelled', interruptedMedia);
75249
75292
  }
75293
+ regionObj.transitionNodes(regionObj.oldMedia, regionObj.currMedia);
75250
75294
  });
75251
75295
  }
75252
75296
  }, {
@@ -75297,11 +75341,19 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75297
75341
  console.debug('[ActionController::loadMediaInRegion] Target media already queued, skipping duplicate insertion');
75298
75342
  return;
75299
75343
  }
75300
- // Cancel the current media's duration timer so it doesn't fire and interrupt
75301
- // the target widget mid-playback (e.g. an Interactive Zone timer still ticking).
75302
- if ((_targetRegion2 = targetRegion) !== null && _targetRegion2 !== void 0 && (_targetRegion2 = _targetRegion2.currMedia) !== null && _targetRegion2 !== void 0 && _targetRegion2.mediaTimer) {
75303
- clearInterval(targetRegion.currMedia.mediaTimer);
75304
- targetRegion.currMedia.mediaTimer = undefined;
75344
+ // Cancel the interrupted media so it doesn't double-advance when the playlist
75345
+ // returns to it. currMedia has not been advanced yet at this point (playNextMedia
75346
+ // does that below), so we cannot use emitter.emit('cancelled') the handler's
75347
+ // currMedia guard would fire and call playNextMedia a second time. Instead we
75348
+ // cancel directly: clear the timer and reset state so the 'start' handler does
75349
+ // not bail on the state === PLAYING guard when this media is replayed.
75350
+ if (((_targetRegion2 = targetRegion) === null || _targetRegion2 === void 0 || (_targetRegion2 = _targetRegion2.currMedia) === null || _targetRegion2 === void 0 ? void 0 : _targetRegion2.state) === MediaState.PLAYING) {
75351
+ var interruptedMedia = targetRegion.currMedia;
75352
+ if (interruptedMedia.mediaTimer) {
75353
+ clearInterval(interruptedMedia.mediaTimer);
75354
+ interruptedMedia.mediaTimer = undefined;
75355
+ }
75356
+ interruptedMedia.state = MediaState.CANCELLED;
75305
75357
  }
75306
75358
  // Reset complete so the HTML-media guard in playNextMedia() doesn't block
75307
75359
  // the transition (that guard is for single-media loops, not navWidget injections).
@@ -75330,6 +75382,12 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75330
75382
  }, {
75331
75383
  key: "runAction",
75332
75384
  value: function runAction(actionData, options) {
75385
+ // If this layout is no longer active (being cancelled or navigated away from),
75386
+ // discard the action so it doesn't interfere with the outgoing transition.
75387
+ // inLoop is set to false synchronously before finishAllRegions() in all nav paths.
75388
+ if (!this.parent.inLoop) {
75389
+ return;
75390
+ }
75333
75391
  console.debug('[ActionController::runAction] Triggering action', {
75334
75392
  actionData: actionData
75335
75393
  });
@@ -75417,31 +75475,37 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75417
75475
  key: "initKeyboardActions",
75418
75476
  value: function initKeyboardActions() {
75419
75477
  var self = this;
75420
- // Store actions in a map
75421
75478
  var keyActions = new Map();
75422
- this.$actionController.querySelectorAll('.action[triggerType="keyPress"]').forEach(function ($el) {
75479
+ this.$actionController.querySelectorAll('.action[triggertype="keyPress"]').forEach(function ($el) {
75423
75480
  var dataset = $el.dataset;
75424
75481
  var code = dataset.triggercode;
75425
75482
  if (code) {
75426
- // Create an empty array, if not yet set
75427
75483
  if (!keyActions.get(code)) {
75428
75484
  keyActions.set(code, []);
75429
75485
  }
75430
- // Add new action to array
75431
75486
  keyActions.get(code).push(dataset);
75432
75487
  }
75433
75488
  });
75434
- // Keyboard listener
75435
- document.addEventListener('keydown', function (ev) {
75489
+ // Nothing to do if this layout has no keyboard-triggered actions.
75490
+ if (keyActions.size === 0) return;
75491
+ this.keyboardHandler = function (ev) {
75436
75492
  var actions = keyActions.get(ev.code);
75437
- // Are there action for this key code?
75438
75493
  if (actions) {
75439
- // Run all actions associated with it
75440
75494
  actions.forEach(function (dataset) {
75441
75495
  self.runAction(dataset, self.options);
75442
75496
  });
75443
75497
  }
75444
- });
75498
+ };
75499
+ document.addEventListener('keydown', this.keyboardHandler);
75500
+ }
75501
+ /** Remove the keydown listener registered by initKeyboardActions. Call when the layout ends or is cancelled. */
75502
+ }, {
75503
+ key: "removeKeyboardActions",
75504
+ value: function removeKeyboardActions() {
75505
+ if (this.keyboardHandler) {
75506
+ document.removeEventListener('keydown', this.keyboardHandler);
75507
+ this.keyboardHandler = null;
75508
+ }
75445
75509
  }
75446
75510
  }]);
75447
75511
  }();
@@ -75676,6 +75740,7 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75676
75740
  });
75677
75741
  this.on('end', /*#__PURE__*/function () {
75678
75742
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(layout) {
75743
+ var _layout$actionControl;
75679
75744
  var $layout, _$layout$parentElemen;
75680
75745
  return _regeneratorRuntime().wrap(function _callee2$(_context2) {
75681
75746
  while (1) switch (_context2.prev = _context2.next) {
@@ -75734,9 +75799,10 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75734
75799
  }
75735
75800
  // Emit layout end event
75736
75801
  console.debug('>>>>> XLR.debug Awaited XLR::emitSync > End - Calling layoutEnd event');
75737
- _context2.next = 13;
75802
+ (_layout$actionControl = layout.actionController) === null || _layout$actionControl === void 0 || _layout$actionControl.removeKeyboardActions();
75803
+ _context2.next = 14;
75738
75804
  return layout.xlr.emitSync('layoutEnd', layout);
75739
- case 13:
75805
+ case 14:
75740
75806
  if (_this.xlr.config.platform !== exports.ConsumerPlatform.CMS && layout.inLoop) {
75741
75807
  // Transition next layout to current layout and prepare next layout if exist
75742
75808
  _this.xlr.prepareLayouts().then( /*#__PURE__*/function () {
@@ -75766,7 +75832,7 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75766
75832
  };
75767
75833
  }());
75768
75834
  }
75769
- case 14:
75835
+ case 15:
75770
75836
  case "end":
75771
75837
  return _context2.stop();
75772
75838
  }
@@ -75777,9 +75843,11 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75777
75843
  };
75778
75844
  }());
75779
75845
  this.on('cancelled', function (layout) {
75846
+ var _layout$actionControl2;
75780
75847
  console.debug('>>>>> XLR.debug / Layout cancelled > Layout ID > ', layout.id);
75781
75848
  layout.state = exports.ELayoutState.CANCELLED;
75782
75849
  layout.inLoop = false;
75850
+ (_layout$actionControl2 = layout.actionController) === null || _layout$actionControl2 === void 0 || _layout$actionControl2.removeKeyboardActions();
75783
75851
  // Dispose video handlers immediately so their stall watchdogs and error
75784
75852
  // callbacks can't fire against a layout whose DOM is about to be removed.
75785
75853
  var _iterator = _createForOfIteratorHelper(layout.regions),
@@ -75938,7 +76006,8 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75938
76006
  _this2.regions.push(regionObj);
75939
76007
  });
75940
76008
  this.actionController.initTouchActions();
75941
- this.actionController.initKeyboardActions();
76009
+ // Keyboard actions are registered in run() so the global document listener
76010
+ // is only active while the layout is actually playing, not during background preparation.
75942
76011
  }
75943
76012
  }, {
75944
76013
  key: "run",
@@ -75957,6 +76026,7 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75957
76026
  shouldParse: false
75958
76027
  });
75959
76028
  if ($layoutContainer) {
76029
+ var _this$actionControlle;
75960
76030
  $layoutContainer.style.setProperty('visibility', 'visible');
75961
76031
  $layoutContainer.style.setProperty('opacity', '1');
75962
76032
  $layoutContainer.style.setProperty('z-index', this.zIndex !== null ? "".concat(this.zIndex) : '1');
@@ -75965,6 +76035,9 @@ ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated wi
75965
76035
  // Also set the background color of the player window > body
75966
76036
  document.body.style.setProperty('background-color', "".concat(this.bgColor));
75967
76037
  }
76038
+ // Register keyboard actions now that the layout is active.
76039
+ // Done here (not in parseXlf) so the global listener is scoped to playback time.
76040
+ (_this$actionControlle = this.actionController) === null || _this$actionControlle === void 0 || _this$actionControlle.initKeyboardActions();
75968
76041
  // Emit start event
75969
76042
  this.emitter.emit('start', this);
75970
76043
  // Play regions