@xiboplayer/renderer 0.7.5 → 0.7.7
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/renderer-lite.js +101 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"pdfjs-dist": "^4.10.38",
|
|
15
|
-
"@xiboplayer/
|
|
16
|
-
"@xiboplayer/
|
|
17
|
-
"@xiboplayer/
|
|
15
|
+
"@xiboplayer/schedule": "0.7.7",
|
|
16
|
+
"@xiboplayer/utils": "0.7.7",
|
|
17
|
+
"@xiboplayer/cache": "0.7.7"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"jsdom": "^25.0.1",
|
package/src/renderer-lite.js
CHANGED
|
@@ -714,6 +714,7 @@ export class RendererLite {
|
|
|
714
714
|
if (maxRegionDuration > 0 && maxRegionDuration !== this.currentLayout.duration) {
|
|
715
715
|
const oldDuration = this.currentLayout.duration;
|
|
716
716
|
this.currentLayout.duration = maxRegionDuration;
|
|
717
|
+
this.currentLayout._durationFromMetadata = true;
|
|
717
718
|
|
|
718
719
|
this.log.info(`Layout duration updated: ${oldDuration}s → ${maxRegionDuration}s (based on video metadata)`);
|
|
719
720
|
const final_ = !this._hasUnprobedVideos();
|
|
@@ -1639,7 +1640,9 @@ export class RendererLite {
|
|
|
1639
1640
|
// Dynamic layouts (useDuration=0 videos): defer timer until video metadata
|
|
1640
1641
|
// provides real durations. Safety timeout ensures corrupt/missing videos
|
|
1641
1642
|
// don't freeze the display forever.
|
|
1642
|
-
|
|
1643
|
+
// Skip deferral if updateLayoutDuration() already set the duration from
|
|
1644
|
+
// video metadata (e.g. during preload or a previous play of this layout).
|
|
1645
|
+
if (layout.isDynamic && !layout._durationFromMetadata && this._hasUnprobedVideos()) {
|
|
1643
1646
|
this._deferredTimerLayoutId = layoutId;
|
|
1644
1647
|
this._layoutTimerStartedAt = Date.now();
|
|
1645
1648
|
this.log.info(`Layout ${layoutId} has unprobed videos — deferring timer until metadata loads`);
|
|
@@ -1662,14 +1665,30 @@ export class RendererLite {
|
|
|
1662
1665
|
}
|
|
1663
1666
|
|
|
1664
1667
|
/**
|
|
1665
|
-
* Check if any video widget
|
|
1666
|
-
* been
|
|
1667
|
-
*
|
|
1668
|
+
* Check if any region's longest-running video widget (useDuration=0) hasn't
|
|
1669
|
+
* been probed yet. Used to decide whether to defer the layout timer.
|
|
1670
|
+
*
|
|
1671
|
+
* Only checks widgets that have had <video> elements created (during preload
|
|
1672
|
+
* or show). Widgets that haven't been displayed yet can never be probed —
|
|
1673
|
+
* checking them would always force a 30s timeout on layouts with multiple
|
|
1674
|
+
* video widgets per region.
|
|
1675
|
+
*
|
|
1676
|
+
* Returns false if the layout duration has already been updated from video
|
|
1677
|
+
* metadata (meaning at least one probe succeeded and updateLayoutDuration
|
|
1678
|
+
* computed a real duration), since the timer can start with that value.
|
|
1668
1679
|
*/
|
|
1669
1680
|
_hasUnprobedVideos() {
|
|
1681
|
+
// If any video was probed and updateLayoutDuration ran, the layout duration
|
|
1682
|
+
// is already based on real metadata — no need to defer further.
|
|
1683
|
+
for (const [, region] of this.regions) {
|
|
1684
|
+
for (const widget of region.widgets) {
|
|
1685
|
+
if (widget.type === 'video' && widget.useDuration === 0 && widget._probed) return false;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
// No videos probed at all — check if there are any that need probing
|
|
1670
1689
|
for (const [, region] of this.regions) {
|
|
1671
1690
|
for (const widget of region.widgets) {
|
|
1672
|
-
if (widget.
|
|
1691
|
+
if (widget.type === 'video' && widget.useDuration === 0) return true;
|
|
1673
1692
|
}
|
|
1674
1693
|
}
|
|
1675
1694
|
return false;
|
|
@@ -1841,27 +1860,36 @@ export class RendererLite {
|
|
|
1841
1860
|
}
|
|
1842
1861
|
|
|
1843
1862
|
const videoEl = widgetElement.querySelector('video');
|
|
1844
|
-
if (videoEl
|
|
1863
|
+
if (videoEl) {
|
|
1864
|
+
videoEl.pause();
|
|
1845
1865
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1866
|
+
// Stop MediaStream tracks (webcam/mic) to release the device
|
|
1867
|
+
if (videoEl._mediaStream) {
|
|
1868
|
+
videoEl._mediaStream.getTracks().forEach(t => t.stop());
|
|
1869
|
+
videoEl._mediaStream = null;
|
|
1870
|
+
videoEl.srcObject = null;
|
|
1871
|
+
}
|
|
1852
1872
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1873
|
+
// Destroy HLS.js instance to free worker + buffers
|
|
1874
|
+
if (videoEl._hlsInstance) {
|
|
1875
|
+
videoEl._hlsInstance.destroy();
|
|
1876
|
+
videoEl._hlsInstance = null;
|
|
1877
|
+
}
|
|
1858
1878
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1879
|
+
// Release decoded video buffers (GPU dmabufs) — without this, paused
|
|
1880
|
+
// videos hold texture memory until the layout is evicted from the pool.
|
|
1881
|
+
// removeAttribute('src') + load() forces the browser to drop the decoded
|
|
1882
|
+
// frame, releasing GPU dmabufs immediately instead of at pool eviction.
|
|
1883
|
+
videoEl.removeAttribute('src');
|
|
1884
|
+
videoEl.load();
|
|
1885
|
+
|
|
1886
|
+
// Remove event listeners to prevent accumulation across widget cycles
|
|
1887
|
+
if (videoEl._eventCleanup) {
|
|
1888
|
+
for (const [event, handler] of videoEl._eventCleanup) {
|
|
1889
|
+
videoEl.removeEventListener(event, handler);
|
|
1890
|
+
}
|
|
1891
|
+
videoEl._eventCleanup = null;
|
|
1863
1892
|
}
|
|
1864
|
-
videoEl._eventCleanup = null;
|
|
1865
1893
|
}
|
|
1866
1894
|
|
|
1867
1895
|
const audioEl = widgetElement.querySelector('audio');
|
|
@@ -2844,6 +2872,13 @@ export class RendererLite {
|
|
|
2844
2872
|
return true;
|
|
2845
2873
|
}
|
|
2846
2874
|
|
|
2875
|
+
// Don't preload if already in-flight (prevents triple preload when
|
|
2876
|
+
// 75% and 90% timers both fire before the async preload completes)
|
|
2877
|
+
if (this._preloadingLayoutId === layoutId) {
|
|
2878
|
+
this.log.info(`Layout ${layoutId} preload already in-flight, skipping`);
|
|
2879
|
+
return true;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2847
2882
|
try {
|
|
2848
2883
|
this.log.info(`Preloading layout ${layoutId} into pool...`);
|
|
2849
2884
|
|
|
@@ -3076,6 +3111,50 @@ export class RendererLite {
|
|
|
3076
3111
|
}
|
|
3077
3112
|
|
|
3078
3113
|
this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);
|
|
3114
|
+
this._logResourceStats(layoutId);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
/**
|
|
3118
|
+
* Log resource allocation stats for debugging memory/GPU leaks.
|
|
3119
|
+
* Called after every layout swap to track DOM node accumulation,
|
|
3120
|
+
* video element lifecycle, and pool state.
|
|
3121
|
+
*/
|
|
3122
|
+
_logResourceStats(layoutId) {
|
|
3123
|
+
const domNodes = document.querySelectorAll('*').length;
|
|
3124
|
+
const videos = document.querySelectorAll('video').length;
|
|
3125
|
+
const videosSrc = document.querySelectorAll('video[src]').length;
|
|
3126
|
+
const canvases = document.querySelectorAll('canvas').length;
|
|
3127
|
+
const iframes = document.querySelectorAll('iframe').length;
|
|
3128
|
+
const images = document.querySelectorAll('img').length;
|
|
3129
|
+
const poolSize = this.layoutPool ? this.layoutPool.size : 0;
|
|
3130
|
+
const regionCount = this.regions ? this.regions.size : 0;
|
|
3131
|
+
const widgetElements = [...(this.regions?.values() || [])].reduce(
|
|
3132
|
+
(sum, r) => sum + (r.widgetElements?.size || 0), 0
|
|
3133
|
+
);
|
|
3134
|
+
const jsHeap = performance?.memory ? {
|
|
3135
|
+
used: Math.round(performance.memory.usedJSHeapSize / 1048576),
|
|
3136
|
+
total: Math.round(performance.memory.totalJSHeapSize / 1048576),
|
|
3137
|
+
limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576),
|
|
3138
|
+
} : null;
|
|
3139
|
+
|
|
3140
|
+
// Count blob URLs still tracked (potential leak indicator)
|
|
3141
|
+
const blobUrls = this._blobUrls ? [...this._blobUrls.values()].reduce((s, set) => s + set.size, 0) : 0;
|
|
3142
|
+
const blobLayouts = this._blobUrls ? this._blobUrls.size : 0;
|
|
3143
|
+
|
|
3144
|
+
// Preload wrapper divs in DOM (should be 0-1 in normal operation)
|
|
3145
|
+
const preloadWrappers = document.querySelectorAll('.renderer-lite-preload-wrapper').length;
|
|
3146
|
+
|
|
3147
|
+
// Audio overlay elements
|
|
3148
|
+
const audioEls = document.querySelectorAll('audio').length;
|
|
3149
|
+
|
|
3150
|
+
const heapStr = jsHeap ? `heap=${jsHeap.used}/${jsHeap.total}MB (limit ${jsHeap.limit}MB)` : 'heap=N/A';
|
|
3151
|
+
this.log.info(
|
|
3152
|
+
`[Resources] layout=${layoutId} dom=${domNodes} videos=${videos}(src=${videosSrc}) ` +
|
|
3153
|
+
`canvas=${canvases} iframe=${iframes} img=${images} audio=${audioEls} ` +
|
|
3154
|
+
`pool=${poolSize} preloadWrappers=${preloadWrappers} ` +
|
|
3155
|
+
`regions=${regionCount} widgets=${widgetElements} ` +
|
|
3156
|
+
`blobs=${blobUrls}(${blobLayouts} layouts) ${heapStr}`
|
|
3157
|
+
);
|
|
3079
3158
|
}
|
|
3080
3159
|
|
|
3081
3160
|
/**
|