@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.
- package/README.md +240 -25
- package/docs/RENDERER_COMPARISON.md +1 -1
- package/package.json +4 -4
- package/src/layout.js +6 -0
- package/src/renderer-lite.ic.test.js +203 -0
- package/src/renderer-lite.js +213 -12
- package/src/renderer-lite.test.js +493 -33
- package/vitest.config.js +3 -1
package/src/renderer-lite.js
CHANGED
|
@@ -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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
|
1703
|
-
selectedWidget = groupWidgets[
|
|
1704
|
-
|
|
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 =
|
|
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
|
}
|