@xiboplayer/renderer 0.6.2 → 0.6.4

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.
@@ -47,7 +47,7 @@ import { LayoutPool } from './layout-pool.js';
47
47
  /**
48
48
  * Transition utilities for widget animations
49
49
  */
50
- const Transitions = {
50
+ export const Transitions = {
51
51
  /**
52
52
  * Apply fade in transition
53
53
  */
@@ -162,11 +162,15 @@ const Transitions = {
162
162
 
163
163
  switch (type) {
164
164
  case 'fade':
165
+ return isIn ? this.fadeIn(element, duration) : this.fadeOut(element, duration);
165
166
  case 'fadein':
166
167
  return isIn ? this.fadeIn(element, duration) : null;
167
168
  case 'fadeout':
168
169
  return isIn ? null : this.fadeOut(element, duration);
169
170
  case 'fly':
171
+ return isIn
172
+ ? this.flyIn(element, duration, direction, regionWidth, regionHeight)
173
+ : this.flyOut(element, duration, direction, regionWidth, regionHeight);
170
174
  case 'flyin':
171
175
  return isIn ? this.flyIn(element, duration, direction, regionWidth, regionHeight) : null;
172
176
  case 'flyout':
@@ -239,6 +243,12 @@ export class RendererLite {
239
243
  // Setup container styles
240
244
  this.setupContainer();
241
245
 
246
+ // Interactive Control (XIC) event handlers
247
+ this.emitter.on('interactiveTrigger', (data) => this._handleInteractiveTrigger(data));
248
+ this.emitter.on('widgetExpire', (data) => this._handleWidgetExpire(data));
249
+ this.emitter.on('widgetExtendDuration', (data) => this._handleWidgetExtendDuration(data));
250
+ this.emitter.on('widgetSetDuration', (data) => this._handleWidgetSetDuration(data));
251
+
242
252
  this.log.info('Initialized');
243
253
  }
244
254
 
@@ -411,6 +421,7 @@ export class RendererLite {
411
421
  const regionAndDrawerEls = layoutEl.querySelectorAll(':scope > region, :scope > drawer');
412
422
  for (const regionEl of regionAndDrawerEls) {
413
423
  const isDrawer = regionEl.tagName === 'drawer';
424
+ const regionType = regionEl.getAttribute('type') || null;
414
425
  const region = {
415
426
  id: regionEl.getAttribute('id'),
416
427
  width: parseInt(regionEl.getAttribute('width') || '0'),
@@ -426,6 +437,7 @@ export class RendererLite {
426
437
  transitionDirection: null,
427
438
  loop: true, // Default: cycle widgets. Spec: loop=0 means single media stays visible
428
439
  isDrawer,
440
+ isCanvas: regionType === 'canvas', // Canvas regions render all widgets simultaneously
429
441
  widgets: []
430
442
  };
431
443
 
@@ -468,11 +480,21 @@ export class RendererLite {
468
480
  region.widgets.push(widget);
469
481
  }
470
482
 
483
+ // Auto-detect canvas from CMS "global" widget (CMS bundles canvas sub-widgets
484
+ // into a single type="global" media element in the XLF)
485
+ if (!region.isCanvas && region.widgets.some(w => w.type === 'global')) {
486
+ region.isCanvas = true;
487
+ }
488
+
471
489
  layout.regions.push(region);
472
490
 
473
491
  if (isDrawer) {
474
492
  this.log.info(`Parsed drawer: id=${region.id} with ${region.widgets.length} widgets`);
475
493
  }
494
+
495
+ if (region.isCanvas) {
496
+ this.log.info(`Parsed canvas region: id=${region.id} with ${region.widgets.length} widgets (all render simultaneously)`);
497
+ }
476
498
  }
477
499
 
478
500
  // Calculate layout duration if not specified (duration=0)
@@ -858,6 +880,133 @@ export class RendererLite {
858
880
  }
859
881
  }
860
882
 
883
+ // ── Interactive Control (XIC) ─────────────────────────────────────
884
+
885
+ /**
886
+ * Find a region containing a widget by widget ID.
887
+ * Searches main regions first, then overlay regions.
888
+ * @param {string} widgetId
889
+ * @returns {{ regionId: string, region: Object, widget: Object, widgetIndex: number, regionMap: Map }|null}
890
+ */
891
+ _findRegionByWidgetId(widgetId) {
892
+ // Search main regions
893
+ for (const [regionId, region] of this.regions) {
894
+ const widgetIndex = region.widgets.findIndex(w => w.id === widgetId);
895
+ if (widgetIndex !== -1) {
896
+ return { regionId, region, widget: region.widgets[widgetIndex], widgetIndex, regionMap: this.regions };
897
+ }
898
+ }
899
+ // Search overlay regions
900
+ for (const overlay of this.activeOverlays.values()) {
901
+ if (!overlay.regions) continue;
902
+ for (const [regionId, region] of overlay.regions) {
903
+ const widgetIndex = region.widgets.findIndex(w => w.id === widgetId);
904
+ if (widgetIndex !== -1) {
905
+ return { regionId, region, widget: region.widgets[widgetIndex], widgetIndex, regionMap: overlay.regions };
906
+ }
907
+ }
908
+ }
909
+ return null;
910
+ }
911
+
912
+ /**
913
+ * Advance a region to its next widget using the standard cycle.
914
+ * @param {string} regionId
915
+ * @param {Map} regionMap - The Map containing this region (main or overlay)
916
+ */
917
+ _advanceRegion(regionId, regionMap) {
918
+ const region = regionMap.get(regionId);
919
+ if (!region) return;
920
+ region.currentIndex = (region.currentIndex + 1) % region.widgets.length;
921
+ const isMain = regionMap === this.regions;
922
+ this._startRegionCycle(
923
+ region, regionId,
924
+ isMain ? (rid, idx) => this.renderWidget(rid, idx) : (rid, idx) => this.renderWidget(rid, idx),
925
+ isMain ? (rid, idx) => this.stopWidget(rid, idx) : (rid, idx) => this.stopWidget(rid, idx),
926
+ isMain ? () => this.checkLayoutComplete() : undefined
927
+ );
928
+ }
929
+
930
+ /**
931
+ * Handle interactiveTrigger XIC event — navigate to a target widget.
932
+ * @param {{ targetId: string, triggerCode: string }} data
933
+ */
934
+ _handleInteractiveTrigger({ targetId, triggerCode }) {
935
+ this.log.info(`XIC interactiveTrigger: target=${targetId} code=${triggerCode}`);
936
+ const found = this._findRegionByWidgetId(targetId);
937
+ if (found) {
938
+ this.navigateToWidget(targetId);
939
+ } else {
940
+ this.log.warn(`XIC interactiveTrigger: widget ${targetId} not found`);
941
+ }
942
+ }
943
+
944
+ /**
945
+ * Handle widgetExpire XIC event — immediately expire a widget and advance.
946
+ * @param {{ widgetId: string }} data
947
+ */
948
+ _handleWidgetExpire({ widgetId }) {
949
+ const found = this._findRegionByWidgetId(widgetId);
950
+ if (!found) {
951
+ this.log.warn(`XIC widgetExpire: widget ${widgetId} not found`);
952
+ return;
953
+ }
954
+ const { regionId, region, widgetIndex, regionMap } = found;
955
+ this.log.info(`XIC widgetExpire: widget=${widgetId} region=${regionId}`);
956
+ if (region.timer) {
957
+ clearTimeout(region.timer);
958
+ region.timer = null;
959
+ }
960
+ this.stopWidget(regionId, widgetIndex);
961
+ this._advanceRegion(regionId, regionMap);
962
+ }
963
+
964
+ /**
965
+ * Handle widgetExtendDuration XIC event — extend the current widget timer.
966
+ * @param {{ widgetId: string, duration: number }} data - duration in seconds (added to remaining)
967
+ */
968
+ _handleWidgetExtendDuration({ widgetId, duration }) {
969
+ const found = this._findRegionByWidgetId(widgetId);
970
+ if (!found) {
971
+ this.log.warn(`XIC widgetExtendDuration: widget ${widgetId} not found`);
972
+ return;
973
+ }
974
+ const { regionId, region } = found;
975
+ this.log.info(`XIC widgetExtendDuration: widget=${widgetId} +${duration}s`);
976
+ if (region.timer) {
977
+ clearTimeout(region.timer);
978
+ region.timer = null;
979
+ }
980
+ // Re-arm timer with the extended duration
981
+ region.timer = setTimeout(() => {
982
+ this.stopWidget(regionId, region.currentIndex);
983
+ this._advanceRegion(regionId, found.regionMap);
984
+ }, duration * 1000);
985
+ }
986
+
987
+ /**
988
+ * Handle widgetSetDuration XIC event — replace the widget timer with an absolute duration.
989
+ * @param {{ widgetId: string, duration: number }} data - duration in seconds (absolute)
990
+ */
991
+ _handleWidgetSetDuration({ widgetId, duration }) {
992
+ const found = this._findRegionByWidgetId(widgetId);
993
+ if (!found) {
994
+ this.log.warn(`XIC widgetSetDuration: widget ${widgetId} not found`);
995
+ return;
996
+ }
997
+ const { regionId, region } = found;
998
+ this.log.info(`XIC widgetSetDuration: widget=${widgetId} ${duration}s`);
999
+ if (region.timer) {
1000
+ clearTimeout(region.timer);
1001
+ region.timer = null;
1002
+ }
1003
+ // Set timer with the absolute duration
1004
+ region.timer = setTimeout(() => {
1005
+ this.stopWidget(regionId, region.currentIndex);
1006
+ this._advanceRegion(regionId, found.regionMap);
1007
+ }, duration * 1000);
1008
+ }
1009
+
861
1010
  /**
862
1011
  * Navigate to a specific widget within a region (for navWidget actions)
863
1012
  */
@@ -894,6 +1043,9 @@ export class RendererLite {
894
1043
  if (region.isDrawer && nextIndex === 0) {
895
1044
  region.element.style.display = 'none';
896
1045
  this.log.info(`Drawer region ${regionId} hidden (cycle complete)`);
1046
+ } else if (region.isDrawer) {
1047
+ // Continue cycling through remaining drawer widgets (will hide on wrap to 0)
1048
+ this.navigateToWidget(region.widgets[nextIndex].id);
897
1049
  } else {
898
1050
  this.startRegion(regionId);
899
1051
  }
@@ -1176,6 +1328,7 @@ export class RendererLite {
1176
1328
  height: regionConfig.height * sf,
1177
1329
  complete: false, // Track if region has played all widgets once
1178
1330
  isDrawer: regionConfig.isDrawer || false,
1331
+ isCanvas: regionConfig.isCanvas || false, // Canvas regions render all widgets simultaneously
1179
1332
  widgetElements: new Map() // widgetId -> DOM element (for element reuse)
1180
1333
  });
1181
1334
  }
@@ -1449,13 +1602,15 @@ export class RendererLite {
1449
1602
  region.element.appendChild(element);
1450
1603
  }
1451
1604
 
1452
- // Hide all other widgets in region
1605
+ // Hide all other widgets in region (skip for canvas — all widgets stay visible)
1453
1606
  // Cancel fill:forwards animations first — they override inline styles
1454
- for (const [widgetId, widgetEl] of region.widgetElements) {
1455
- if (widgetId !== widget.id) {
1456
- widgetEl.getAnimations?.().forEach(a => a.cancel());
1457
- widgetEl.style.visibility = 'hidden';
1458
- widgetEl.style.opacity = '0';
1607
+ if (!region.isCanvas) {
1608
+ for (const [widgetId, widgetEl] of region.widgetElements) {
1609
+ if (widgetId !== widget.id) {
1610
+ widgetEl.getAnimations?.().forEach(a => a.cancel());
1611
+ widgetEl.style.visibility = 'hidden';
1612
+ widgetEl.style.opacity = '0';
1613
+ }
1459
1614
  }
1460
1615
  }
1461
1616
 
@@ -1698,10 +1853,17 @@ export class RendererLite {
1698
1853
  const idx = Math.floor(Math.random() * groupWidgets.length);
1699
1854
  selectedWidget = groupWidgets[idx];
1700
1855
  } else {
1701
- // Round-robin based on cycle index
1702
- const cycleIdx = this._subPlaylistCycleIndex.get(groupId) || 0;
1703
- selectedWidget = groupWidgets[cycleIdx % groupWidgets.length];
1704
- this._subPlaylistCycleIndex.set(groupId, cycleIdx + 1);
1856
+ // Round-robin based on cycle index, respecting playCount
1857
+ const state = this._subPlaylistCycleIndex.get(groupId) || { widgetIdx: 0, playsDone: 0 };
1858
+ selectedWidget = groupWidgets[state.widgetIdx % groupWidgets.length];
1859
+ const effectivePlayCount = selectedWidget.playCount || 1;
1860
+
1861
+ state.playsDone++;
1862
+ if (state.playsDone >= effectivePlayCount) {
1863
+ state.widgetIdx++;
1864
+ state.playsDone = 0;
1865
+ }
1866
+ this._subPlaylistCycleIndex.set(groupId, state);
1705
1867
  }
1706
1868
 
1707
1869
  this.log.info(`Sub-playlist cycle: group ${groupId} selected widget ${selectedWidget.id} (${groupWidgets.length} in group)`);
@@ -1722,6 +1884,13 @@ export class RendererLite {
1722
1884
  _startRegionCycle(region, regionId, showFn, hideFn, onCycleComplete) {
1723
1885
  if (!region || region.widgets.length === 0) return;
1724
1886
 
1887
+ // Canvas regions: render ALL widgets simultaneously (stacked), no cycling.
1888
+ // Duration = max widget duration; region completes when the longest widget expires.
1889
+ if (region.isCanvas) {
1890
+ this._startCanvasRegion(region, regionId, showFn, onCycleComplete);
1891
+ return;
1892
+ }
1893
+
1725
1894
  // Non-looping region with a single widget: show it and stay (spec: loop=0)
1726
1895
  if (region.widgets.length === 1) {
1727
1896
  showFn(regionId, 0);
@@ -1744,6 +1913,37 @@ export class RendererLite {
1744
1913
  playNext();
1745
1914
  }
1746
1915
 
1916
+ /**
1917
+ * Start a canvas region — render all widgets simultaneously (stacked).
1918
+ * Canvas regions show every widget at once rather than cycling through them.
1919
+ * The region duration is the maximum widget duration.
1920
+ * @param {Object} region - Region state
1921
+ * @param {string} regionId - Region ID
1922
+ * @param {Function} showFn - Show widget function (regionId, widgetIndex)
1923
+ * @param {Function} onCycleComplete - Callback when region completes
1924
+ */
1925
+ _startCanvasRegion(region, regionId, showFn, onCycleComplete) {
1926
+ // Show all widgets at once
1927
+ for (let i = 0; i < region.widgets.length; i++) {
1928
+ showFn(regionId, i);
1929
+ }
1930
+
1931
+ // Mark region as complete after max widget duration
1932
+ const maxDuration = Math.max(...region.widgets.map(w => w.duration)) * 1000;
1933
+ if (maxDuration > 0) {
1934
+ region.timer = setTimeout(() => {
1935
+ if (!region.complete) {
1936
+ region.complete = true;
1937
+ onCycleComplete?.();
1938
+ }
1939
+ }, maxDuration);
1940
+ } else {
1941
+ // No duration — immediately complete
1942
+ region.complete = true;
1943
+ onCycleComplete?.();
1944
+ }
1945
+ }
1946
+
1747
1947
  /**
1748
1948
  * Handle widget cycle end — shared logic for timer-based and event-based cycling
1749
1949
  */
@@ -1951,7 +2151,7 @@ export class RendererLite {
1951
2151
  // NOT update the current layout's duration with a different layout's video.
1952
2152
  const createdForLayoutId = this.currentLayoutId;
1953
2153
  const onLoadedMetadata = () => {
1954
- const videoDuration = Math.floor(video.duration);
2154
+ const videoDuration = video.duration;
1955
2155
  this.log.info(`Video ${storedAs} duration detected: ${videoDuration}s`);
1956
2156
 
1957
2157
  if (widget.duration === 0 || widget.useDuration === 0) {
@@ -2947,6 +3147,7 @@ export class RendererLite {
2947
3147
  width: regionConfig.width * sf,
2948
3148
  height: regionConfig.height * sf,
2949
3149
  complete: false,
3150
+ isCanvas: regionConfig.isCanvas || false,
2950
3151
  widgetElements: new Map()
2951
3152
  });
2952
3153
  }