@xiboplayer/renderer 0.6.5 → 0.6.6
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/package.json +4 -4
- package/src/index.d.ts +1 -0
- package/src/layout.js +2 -1
- package/src/renderer-lite.js +90 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"nanoevents": "^9.1.0",
|
|
15
15
|
"pdfjs-dist": "^4.10.38",
|
|
16
|
-
"@xiboplayer/cache": "0.6.
|
|
17
|
-
"@xiboplayer/schedule": "0.6.
|
|
18
|
-
"@xiboplayer/utils": "0.6.
|
|
16
|
+
"@xiboplayer/cache": "0.6.6",
|
|
17
|
+
"@xiboplayer/schedule": "0.6.6",
|
|
18
|
+
"@xiboplayer/utils": "0.6.6"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"vitest": "^2.0.0",
|
package/src/index.d.ts
CHANGED
package/src/layout.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { cacheWidgetHtml } from '@xiboplayer/cache';
|
|
7
|
-
import { createLogger, PLAYER_API } from '@xiboplayer/utils';
|
|
7
|
+
import { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';
|
|
8
8
|
|
|
9
9
|
const log = createLogger('Layout');
|
|
10
10
|
|
|
@@ -754,6 +754,7 @@ ${mediaJS}
|
|
|
754
754
|
border-radius: 4px;
|
|
755
755
|
font-size: 14px;
|
|
756
756
|
z-index: 10;
|
|
757
|
+
display: ${isDebug() ? 'block' : 'none'};
|
|
757
758
|
\`;
|
|
758
759
|
container.appendChild(pageIndicator);
|
|
759
760
|
|
package/src/renderer-lite.js
CHANGED
|
@@ -212,6 +212,7 @@ export class RendererLite {
|
|
|
212
212
|
this.regions = new Map(); // regionId => { element, widgets, currentIndex, timer }
|
|
213
213
|
this.layoutTimer = null;
|
|
214
214
|
this.layoutEndEmitted = false; // Prevents double layoutEnd on stop after timer
|
|
215
|
+
this._deferredTimerLayoutId = null; // Set when timer is deferred for dynamic layouts
|
|
215
216
|
this._paused = false;
|
|
216
217
|
this._layoutTimerStartedAt = null; // Date.now() when layout timer started
|
|
217
218
|
this._layoutTimerDurationMs = null; // Total layout duration in ms
|
|
@@ -262,9 +263,11 @@ export class RendererLite {
|
|
|
262
263
|
this.container.style.overflow = 'hidden';
|
|
263
264
|
|
|
264
265
|
// Watch for container resize to rescale layout (debounced to avoid spam)
|
|
266
|
+
this._resizeSuppressed = false;
|
|
265
267
|
if (typeof ResizeObserver !== 'undefined') {
|
|
266
268
|
let resizeTimer = null;
|
|
267
269
|
this.resizeObserver = new ResizeObserver(() => {
|
|
270
|
+
if (this._resizeSuppressed) return;
|
|
268
271
|
if (resizeTimer) clearTimeout(resizeTimer);
|
|
269
272
|
resizeTimer = setTimeout(() => this.rescaleRegions(), 150);
|
|
270
273
|
});
|
|
@@ -500,9 +503,10 @@ export class RendererLite {
|
|
|
500
503
|
// Calculate layout duration if not specified (duration=0)
|
|
501
504
|
// Uses shared parseLayoutDuration() — single source of truth for XLF-based duration calc
|
|
502
505
|
if (layout.duration === 0) {
|
|
503
|
-
const { duration } = parseLayoutDuration(xlfXml);
|
|
506
|
+
const { duration, isDynamic } = parseLayoutDuration(xlfXml);
|
|
504
507
|
layout.duration = duration;
|
|
505
|
-
|
|
508
|
+
layout.isDynamic = isDynamic;
|
|
509
|
+
this.log.info(`Calculated layout duration: ${layout.duration}s (not specified in XLF)${isDynamic ? ' [dynamic — has useDuration=0 video]' : ''}`);
|
|
506
510
|
}
|
|
507
511
|
|
|
508
512
|
return layout;
|
|
@@ -705,10 +709,26 @@ export class RendererLite {
|
|
|
705
709
|
this.log.info(`Layout duration updated: ${oldDuration}s → ${maxRegionDuration}s (based on video metadata)`);
|
|
706
710
|
this.emit('layoutDurationUpdated', this.currentLayoutId, maxRegionDuration);
|
|
707
711
|
|
|
708
|
-
//
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
+
// Deferred timer: video metadata arrived, start the timer now
|
|
713
|
+
if (this._deferredTimerLayoutId === this.currentLayoutId && !this.layoutTimer) {
|
|
714
|
+
if (this._hasUnprobedVideos()) {
|
|
715
|
+
this.log.info(`Layout duration updated to ${maxRegionDuration}s but still has unprobed videos — keeping timer deferred`);
|
|
716
|
+
} else {
|
|
717
|
+
const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());
|
|
718
|
+
const remainingMs = Math.max(1000, maxRegionDuration * 1000 - elapsed);
|
|
719
|
+
this._deferredTimerLayoutId = null;
|
|
720
|
+
this._layoutTimerDurationMs = remainingMs;
|
|
721
|
+
this.layoutTimer = setTimeout(() => {
|
|
722
|
+
this.log.info(`Layout ${this.currentLayoutId} duration expired (${this.currentLayout.duration}s)`);
|
|
723
|
+
if (this.currentLayoutId) {
|
|
724
|
+
this.layoutEndEmitted = true;
|
|
725
|
+
this.emit('layoutEnd', this.currentLayoutId);
|
|
726
|
+
}
|
|
727
|
+
}, remainingMs);
|
|
728
|
+
this.log.info(`All video durations resolved — deferred timer started: ${(remainingMs / 1000).toFixed(1)}s remaining (waited ${(elapsed / 1000).toFixed(1)}s for metadata)`);
|
|
729
|
+
}
|
|
730
|
+
} else if (this.layoutTimer) {
|
|
731
|
+
// Reset layout timer with REMAINING time — not full duration.
|
|
712
732
|
clearTimeout(this.layoutTimer);
|
|
713
733
|
|
|
714
734
|
const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());
|
|
@@ -1177,6 +1197,7 @@ export class RendererLite {
|
|
|
1177
1197
|
clearTimeout(this.layoutTimer);
|
|
1178
1198
|
this.layoutTimer = null;
|
|
1179
1199
|
}
|
|
1200
|
+
this._deferredTimerLayoutId = null;
|
|
1180
1201
|
this.layoutEndEmitted = false;
|
|
1181
1202
|
|
|
1182
1203
|
// DON'T call stopCurrentLayout() - keep elements alive!
|
|
@@ -1561,6 +1582,36 @@ export class RendererLite {
|
|
|
1561
1582
|
return;
|
|
1562
1583
|
}
|
|
1563
1584
|
|
|
1585
|
+
// Dynamic layouts (useDuration=0 videos): defer timer until video metadata
|
|
1586
|
+
// provides real durations. Without this, the 60s fallback fires prematurely
|
|
1587
|
+
// causing rapid layout cycling ("layout storm") on startup.
|
|
1588
|
+
if (layout.isDynamic && this._hasUnprobedVideos()) {
|
|
1589
|
+
this._deferredTimerLayoutId = layoutId;
|
|
1590
|
+
this._layoutTimerStartedAt = Date.now();
|
|
1591
|
+
this.log.info(`Layout ${layoutId} has unprobed videos — deferring timer until metadata loads`);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
this._startLayoutTimer(layoutId, layout);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Check if any video widget in current layout still has duration=0 (metadata not loaded).
|
|
1600
|
+
*/
|
|
1601
|
+
_hasUnprobedVideos() {
|
|
1602
|
+
for (const [, region] of this.regions) {
|
|
1603
|
+
for (const widget of region.widgets) {
|
|
1604
|
+
if (widget.useDuration === 0 && widget.duration === 0) return true;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Actually start the layout timer. Called directly or after deferred timer resolves.
|
|
1612
|
+
*/
|
|
1613
|
+
_startLayoutTimer(layoutId, layout) {
|
|
1614
|
+
this._deferredTimerLayoutId = null;
|
|
1564
1615
|
const layoutDurationMs = layout.duration * 1000;
|
|
1565
1616
|
this.log.info(`Layout ${layoutId} will end after ${layout.duration}s`);
|
|
1566
1617
|
|
|
@@ -2433,7 +2484,7 @@ export class RendererLite {
|
|
|
2433
2484
|
container.className = 'renderer-lite-widget pdf-widget';
|
|
2434
2485
|
container.style.width = '100%';
|
|
2435
2486
|
container.style.height = '100%';
|
|
2436
|
-
container.style.backgroundColor = '
|
|
2487
|
+
container.style.backgroundColor = 'transparent';
|
|
2437
2488
|
container.style.opacity = '0';
|
|
2438
2489
|
container.style.position = 'relative';
|
|
2439
2490
|
|
|
@@ -2481,9 +2532,10 @@ export class RendererLite {
|
|
|
2481
2532
|
const ctx = canvas.getContext('2d');
|
|
2482
2533
|
container.appendChild(canvas);
|
|
2483
2534
|
|
|
2484
|
-
// Page indicator (bottom-right, v1-style pill)
|
|
2535
|
+
// Page indicator (bottom-right, v1-style pill) — debug only
|
|
2485
2536
|
const indicator = document.createElement('div');
|
|
2486
2537
|
indicator.style.cssText = 'position:absolute;bottom:10px;right:10px;background:rgba(0,0,0,0.7);color:white;padding:8px 12px;border-radius:4px;font:14px system-ui;z-index:1;';
|
|
2538
|
+
if (!isDebug()) indicator.style.display = 'none';
|
|
2487
2539
|
container.appendChild(indicator);
|
|
2488
2540
|
|
|
2489
2541
|
let currentPage = 1;
|
|
@@ -2882,6 +2934,10 @@ export class RendererLite {
|
|
|
2882
2934
|
|
|
2883
2935
|
const oldLayoutId = this.currentLayoutId;
|
|
2884
2936
|
|
|
2937
|
+
this.layoutEndEmitted = false;
|
|
2938
|
+
this.currentLayout = null;
|
|
2939
|
+
this.currentLayoutId = null;
|
|
2940
|
+
|
|
2885
2941
|
if (oldLayoutId && this.layoutPool.has(oldLayoutId)) {
|
|
2886
2942
|
// Old layout was preloaded — evict from pool (safe: removes its wrapper div)
|
|
2887
2943
|
this.layoutPool.evict(oldLayoutId);
|
|
@@ -2915,11 +2971,6 @@ export class RendererLite {
|
|
|
2915
2971
|
}
|
|
2916
2972
|
}
|
|
2917
2973
|
|
|
2918
|
-
// Emit layoutEnd for old layout if timer hasn't already
|
|
2919
|
-
if (oldLayoutId && !this.layoutEndEmitted) {
|
|
2920
|
-
this.emit('layoutEnd', oldLayoutId);
|
|
2921
|
-
}
|
|
2922
|
-
|
|
2923
2974
|
this.regions.clear();
|
|
2924
2975
|
|
|
2925
2976
|
// ── Activate preloaded layout ──
|
|
@@ -2931,7 +2982,13 @@ export class RendererLite {
|
|
|
2931
2982
|
this.currentLayout = preloaded.layout;
|
|
2932
2983
|
this.currentLayoutId = layoutId;
|
|
2933
2984
|
this.regions = preloaded.regions;
|
|
2934
|
-
|
|
2985
|
+
|
|
2986
|
+
// Emit layoutEnd for old layout AFTER setting new currentLayoutId —
|
|
2987
|
+
// the listener guard in main.ts sees the new layout already playing
|
|
2988
|
+
// and skips advance, while stats/tracking still run.
|
|
2989
|
+
if (oldLayoutId) {
|
|
2990
|
+
this.emit('layoutEnd', oldLayoutId);
|
|
2991
|
+
}
|
|
2935
2992
|
|
|
2936
2993
|
// Update container background to match preloaded layout
|
|
2937
2994
|
this.container.style.backgroundColor = preloaded.layout.bgcolor;
|
|
@@ -3007,16 +3064,19 @@ export class RendererLite {
|
|
|
3007
3064
|
|
|
3008
3065
|
this.log.info(`Stopping layout ${this.currentLayoutId}`);
|
|
3009
3066
|
|
|
3010
|
-
|
|
3011
|
-
this.
|
|
3067
|
+
const endedLayoutId = this.currentLayoutId;
|
|
3068
|
+
const shouldEmit = endedLayoutId && !this.layoutEndEmitted;
|
|
3012
3069
|
|
|
3013
|
-
|
|
3070
|
+
this.layoutEndEmitted = false;
|
|
3071
|
+
this._deferredTimerLayoutId = null;
|
|
3072
|
+
this.currentLayout = null;
|
|
3073
|
+
this.currentLayoutId = null;
|
|
3074
|
+
|
|
3075
|
+
// Clear timers
|
|
3014
3076
|
if (this.layoutTimer) {
|
|
3015
3077
|
clearTimeout(this.layoutTimer);
|
|
3016
3078
|
this.layoutTimer = null;
|
|
3017
3079
|
}
|
|
3018
|
-
|
|
3019
|
-
// Clear preload timers
|
|
3020
3080
|
if (this.preloadTimer) {
|
|
3021
3081
|
clearTimeout(this.preloadTimer);
|
|
3022
3082
|
this.preloadTimer = null;
|
|
@@ -3026,16 +3086,19 @@ export class RendererLite {
|
|
|
3026
3086
|
this._preloadRetryTimer = null;
|
|
3027
3087
|
}
|
|
3028
3088
|
|
|
3089
|
+
// Remove interactive action listeners before teardown
|
|
3090
|
+
this.removeActionListeners();
|
|
3091
|
+
|
|
3029
3092
|
// If layout was preloaded (has its own wrapper div in pool), evict safely.
|
|
3030
3093
|
// Normally-rendered layouts are NOT in the pool, so we do manual cleanup.
|
|
3031
|
-
if (
|
|
3032
|
-
this.layoutPool.evict(
|
|
3094
|
+
if (endedLayoutId && this.layoutPool.has(endedLayoutId)) {
|
|
3095
|
+
this.layoutPool.evict(endedLayoutId);
|
|
3033
3096
|
} else {
|
|
3034
3097
|
// Normally-rendered layout - manual cleanup (regions are in this.container)
|
|
3035
3098
|
|
|
3036
3099
|
// Revoke all blob URLs for this layout (tracked lifecycle management)
|
|
3037
|
-
if (
|
|
3038
|
-
this.revokeBlobUrlsForLayout(
|
|
3100
|
+
if (endedLayoutId) {
|
|
3101
|
+
this.revokeBlobUrlsForLayout(endedLayoutId);
|
|
3039
3102
|
}
|
|
3040
3103
|
|
|
3041
3104
|
// Stop all regions
|
|
@@ -3068,21 +3131,13 @@ export class RendererLite {
|
|
|
3068
3131
|
|
|
3069
3132
|
}
|
|
3070
3133
|
|
|
3071
|
-
// Clear state
|
|
3072
3134
|
this.regions.clear();
|
|
3073
3135
|
|
|
3074
|
-
// Emit
|
|
3075
|
-
//
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
// schedule change), the timer hasn't fired yet, so we DO emit here.
|
|
3079
|
-
if (this.currentLayoutId && !this.layoutEndEmitted) {
|
|
3080
|
-
this.emit('layoutEnd', this.currentLayoutId);
|
|
3136
|
+
// Emit LAST — re-entrant renderLayout() sees currentLayout=null,
|
|
3137
|
+
// so stopCurrentLayout() returns early. No cascade.
|
|
3138
|
+
if (shouldEmit) {
|
|
3139
|
+
this.emit('layoutEnd', endedLayoutId);
|
|
3081
3140
|
}
|
|
3082
|
-
|
|
3083
|
-
this.layoutEndEmitted = false;
|
|
3084
|
-
this.currentLayout = null;
|
|
3085
|
-
this.currentLayoutId = null;
|
|
3086
3141
|
}
|
|
3087
3142
|
|
|
3088
3143
|
/**
|