@xiboplayer/renderer 0.5.8 → 0.5.9
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 +3 -3
- package/src/layout-pool.js +59 -11
- package/src/layout.js +28 -23
- package/src/renderer-lite.js +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"nanoevents": "^9.1.0",
|
|
14
14
|
"pdfjs-dist": "^4.10.38",
|
|
15
|
-
"@xiboplayer/utils": "0.5.
|
|
16
|
-
"@xiboplayer/cache": "0.5.
|
|
15
|
+
"@xiboplayer/utils": "0.5.9",
|
|
16
|
+
"@xiboplayer/cache": "0.5.9"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"vitest": "^2.0.0",
|
package/src/layout-pool.js
CHANGED
|
@@ -101,7 +101,7 @@ export class LayoutPool {
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Evict a specific layout from the pool.
|
|
104
|
-
*
|
|
104
|
+
* Releases video/audio resources, revokes blob URLs, and removes the container from the DOM.
|
|
105
105
|
* @param {number} layoutId
|
|
106
106
|
*/
|
|
107
107
|
evict(layoutId) {
|
|
@@ -110,6 +110,22 @@ export class LayoutPool {
|
|
|
110
110
|
|
|
111
111
|
log.info(`Evicting layout ${layoutId} from pool`);
|
|
112
112
|
|
|
113
|
+
// Stop any active region timers
|
|
114
|
+
if (entry.regions) {
|
|
115
|
+
for (const [regionId, region] of entry.regions) {
|
|
116
|
+
if (region.timer) {
|
|
117
|
+
clearTimeout(region.timer);
|
|
118
|
+
region.timer = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Release all video/audio resources BEFORE removing from DOM.
|
|
124
|
+
// Removing a <video> with an active src leaks decoded frame buffers.
|
|
125
|
+
if (entry.container) {
|
|
126
|
+
LayoutPool.releaseMediaElements(entry.container);
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
// Revoke blob URLs
|
|
114
130
|
if (entry.blobUrls && entry.blobUrls.size > 0) {
|
|
115
131
|
entry.blobUrls.forEach(url => {
|
|
@@ -127,16 +143,6 @@ export class LayoutPool {
|
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
// Stop any active region timers
|
|
131
|
-
if (entry.regions) {
|
|
132
|
-
for (const [regionId, region] of entry.regions) {
|
|
133
|
-
if (region.timer) {
|
|
134
|
-
clearTimeout(region.timer);
|
|
135
|
-
region.timer = null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
146
|
// Remove container from DOM
|
|
141
147
|
if (entry.container && entry.container.parentNode) {
|
|
142
148
|
entry.container.remove();
|
|
@@ -150,6 +156,48 @@ export class LayoutPool {
|
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Release all video and audio elements inside a container.
|
|
161
|
+
* Must be called BEFORE removing the container from the DOM —
|
|
162
|
+
* browsers keep decoded frame buffers alive for detached <video> elements
|
|
163
|
+
* that still have a src.
|
|
164
|
+
*
|
|
165
|
+
* @param {HTMLElement} container
|
|
166
|
+
*/
|
|
167
|
+
static releaseMediaElements(container) {
|
|
168
|
+
let videoCount = 0;
|
|
169
|
+
let hlsCount = 0;
|
|
170
|
+
|
|
171
|
+
container.querySelectorAll('video').forEach(v => {
|
|
172
|
+
// Destroy hls.js instance if attached (stored by renderVideo)
|
|
173
|
+
if (v._hlsInstance) {
|
|
174
|
+
v._hlsInstance.destroy();
|
|
175
|
+
v._hlsInstance = null;
|
|
176
|
+
hlsCount++;
|
|
177
|
+
}
|
|
178
|
+
// Stop MediaStream tracks (webcam/mic)
|
|
179
|
+
if (v._mediaStream) {
|
|
180
|
+
v._mediaStream.getTracks().forEach(t => t.stop());
|
|
181
|
+
v._mediaStream = null;
|
|
182
|
+
v.srcObject = null;
|
|
183
|
+
}
|
|
184
|
+
v.pause();
|
|
185
|
+
v.removeAttribute('src');
|
|
186
|
+
v.load(); // Forces browser to release decoded buffers
|
|
187
|
+
videoCount++;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
container.querySelectorAll('audio').forEach(a => {
|
|
191
|
+
a.pause();
|
|
192
|
+
a.removeAttribute('src');
|
|
193
|
+
a.load();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (videoCount > 0) {
|
|
197
|
+
log.info(`Released ${videoCount} video(s)${hlsCount ? ` (${hlsCount} HLS)` : ''}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
153
201
|
/**
|
|
154
202
|
* Evict the least-recently-used warm entry.
|
|
155
203
|
* Only warm entries are eligible for eviction (never the hot layout).
|
package/src/layout.js
CHANGED
|
@@ -155,23 +155,19 @@ export class LayoutTranslator {
|
|
|
155
155
|
if (!raw && lastError) {
|
|
156
156
|
log.warn('All retries failed, checking for cached widget HTML...');
|
|
157
157
|
|
|
158
|
-
// Try to get cached widget HTML
|
|
158
|
+
// Try to get cached widget HTML from ContentStore via proxy
|
|
159
159
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
raw = await cached.text();
|
|
166
|
-
options.widgetCacheKey = cachedKey;
|
|
167
|
-
log.info(`Using cached widget HTML (${raw.length} chars) - CMS update pending`);
|
|
160
|
+
const resp = await fetch(`/store/widget/${layoutId}/${regionId}/${id}`);
|
|
161
|
+
if (resp.ok) {
|
|
162
|
+
raw = await resp.text();
|
|
163
|
+
options.widgetCacheKey = `/cache/widget/${layoutId}/${regionId}/${id}`;
|
|
164
|
+
log.info(`Using stored widget HTML (${raw.length} chars) - CMS update pending`);
|
|
168
165
|
} else {
|
|
169
|
-
log.error(`No
|
|
170
|
-
// Show minimal placeholder that doesn't look like an error
|
|
166
|
+
log.error(`No stored version available for widget ${id}`);
|
|
171
167
|
raw = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;">Content updating...</div>`;
|
|
172
168
|
}
|
|
173
|
-
} catch (
|
|
174
|
-
log.error('
|
|
169
|
+
} catch (storeError) {
|
|
170
|
+
log.error('Store fallback failed:', storeError);
|
|
175
171
|
raw = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;">Content updating...</div>`;
|
|
176
172
|
}
|
|
177
173
|
}
|
|
@@ -330,10 +326,15 @@ Object.keys(regions).forEach(id => {
|
|
|
330
326
|
playRegion(id);
|
|
331
327
|
});
|
|
332
328
|
|
|
329
|
+
// Track active timers per region so layout teardown can cancel them
|
|
330
|
+
const regionTimers = {};
|
|
331
|
+
|
|
333
332
|
function playRegion(id) {
|
|
334
333
|
const region = regions[id];
|
|
335
334
|
if (!region || region.media.length === 0) return;
|
|
336
335
|
|
|
336
|
+
regionTimers[id] = [];
|
|
337
|
+
|
|
337
338
|
// If only one media item, just show it and don't cycle (arexibo behavior)
|
|
338
339
|
if (region.media.length === 1) {
|
|
339
340
|
const media = region.media[0];
|
|
@@ -349,15 +350,21 @@ function playRegion(id) {
|
|
|
349
350
|
if (media.start) media.start();
|
|
350
351
|
|
|
351
352
|
const duration = media.duration || 10;
|
|
352
|
-
setTimeout(() => {
|
|
353
|
+
const timerId = setTimeout(() => {
|
|
353
354
|
if (media.stop) media.stop();
|
|
354
355
|
currentIndex = (currentIndex + 1) % region.media.length;
|
|
355
356
|
playNext();
|
|
356
357
|
}, duration * 1000);
|
|
358
|
+
regionTimers[id].push(timerId);
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
playNext();
|
|
360
362
|
}
|
|
363
|
+
|
|
364
|
+
// Cleanup function — called before layout teardown
|
|
365
|
+
window._stopAllRegions = function() {
|
|
366
|
+
Object.values(regionTimers).forEach(timers => timers.forEach(t => clearTimeout(t)));
|
|
367
|
+
};
|
|
361
368
|
</script>
|
|
362
369
|
</body>
|
|
363
370
|
</html>`;
|
|
@@ -815,6 +822,9 @@ ${mediaJS}
|
|
|
815
822
|
}
|
|
816
823
|
}
|
|
817
824
|
|
|
825
|
+
// Store live timer array on element for cleanup (not JSON — stays current)
|
|
826
|
+
container._pageTimers = pageTimers;
|
|
827
|
+
|
|
818
828
|
// Start cycling
|
|
819
829
|
await cyclePage();
|
|
820
830
|
|
|
@@ -827,9 +837,6 @@ ${mediaJS}
|
|
|
827
837
|
container.style.opacity = '1';
|
|
828
838
|
}
|
|
829
839
|
|
|
830
|
-
// Store timers for cleanup
|
|
831
|
-
container.dataset.pageTimers = JSON.stringify(pageTimers.map(t => t));
|
|
832
|
-
|
|
833
840
|
} catch (error) {
|
|
834
841
|
console.error('[PDF] Render failed:', error);
|
|
835
842
|
container.innerHTML = '<div style="color:white;padding:20px;text-align:center;">Failed to load PDF</div>';
|
|
@@ -841,12 +848,10 @@ ${mediaJS}
|
|
|
841
848
|
const region = document.getElementById('region_${regionId}');
|
|
842
849
|
const container = document.getElementById('${pdfContainerId}');
|
|
843
850
|
if (container) {
|
|
844
|
-
// Clear page cycling timers
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
JSON.parse(timers).forEach(t => clearTimeout(t));
|
|
849
|
-
} catch (e) {}
|
|
851
|
+
// Clear page cycling timers (live array, always current)
|
|
852
|
+
if (container._pageTimers) {
|
|
853
|
+
container._pageTimers.forEach(t => clearTimeout(t));
|
|
854
|
+
container._pageTimers.length = 0;
|
|
850
855
|
}
|
|
851
856
|
|
|
852
857
|
const transOut = ${transOut};
|
package/src/renderer-lite.js
CHANGED
|
@@ -1923,10 +1923,12 @@ export class RendererLite {
|
|
|
1923
1923
|
const hls = new Hls({ enableWorker: true, lowLatencyMode: true });
|
|
1924
1924
|
hls.loadSource(videoSrc);
|
|
1925
1925
|
hls.attachMedia(video);
|
|
1926
|
+
video._hlsInstance = hls; // Store for cleanup on eviction
|
|
1926
1927
|
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
1927
1928
|
if (data.fatal) {
|
|
1928
1929
|
this.log.error(`HLS fatal error: ${data.type}`, data.details);
|
|
1929
1930
|
hls.destroy();
|
|
1931
|
+
video._hlsInstance = null;
|
|
1930
1932
|
}
|
|
1931
1933
|
});
|
|
1932
1934
|
this.log.info(`HLS stream (hls.js): ${fileId}`);
|
|
@@ -2670,12 +2672,8 @@ export class RendererLite {
|
|
|
2670
2672
|
clearTimeout(region.timer);
|
|
2671
2673
|
region.timer = null;
|
|
2672
2674
|
}
|
|
2673
|
-
// Release video resources
|
|
2674
|
-
region.element
|
|
2675
|
-
v.pause();
|
|
2676
|
-
v.removeAttribute('src');
|
|
2677
|
-
v.load();
|
|
2678
|
-
});
|
|
2675
|
+
// Release video/audio resources before removing from DOM
|
|
2676
|
+
LayoutPool.releaseMediaElements(region.element);
|
|
2679
2677
|
// Apply region exit transition if configured, then remove
|
|
2680
2678
|
if (region.config && region.config.exitTransition) {
|
|
2681
2679
|
const animation = Transitions.apply(
|
|
@@ -2838,6 +2836,9 @@ export class RendererLite {
|
|
|
2838
2836
|
this.stopWidget(regionId, region.currentIndex);
|
|
2839
2837
|
}
|
|
2840
2838
|
|
|
2839
|
+
// Release video/audio resources before removing from DOM
|
|
2840
|
+
LayoutPool.releaseMediaElements(region.element);
|
|
2841
|
+
|
|
2841
2842
|
// Apply region exit transition if configured, then remove
|
|
2842
2843
|
if (region.config && region.config.exitTransition) {
|
|
2843
2844
|
const animation = Transitions.apply(
|