@xiboplayer/renderer 0.7.7 → 0.7.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 +4 -4
- package/src/index.d.ts +1 -0
- package/src/layout-pool.js +41 -2
- package/src/renderer-lite.js +42 -5
- package/src/renderer-lite.test.js +3 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
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/utils": "0.7.9",
|
|
16
|
+
"@xiboplayer/cache": "0.7.9",
|
|
17
|
+
"@xiboplayer/schedule": "0.7.9"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"jsdom": "^25.0.1",
|
package/src/index.d.ts
CHANGED
package/src/layout-pool.js
CHANGED
|
@@ -167,6 +167,15 @@ export class LayoutPool {
|
|
|
167
167
|
* @param {HTMLElement} container
|
|
168
168
|
*/
|
|
169
169
|
static releaseMediaElements(container) {
|
|
170
|
+
// Defer the actual release by one animation frame to give the GPU compositor
|
|
171
|
+
// time to stop referencing textures from the old layout. Without this delay,
|
|
172
|
+
// the compositor may still hold stale mailbox references when we destroy the
|
|
173
|
+
// video backing, causing SharedImageManager::ProduceSkia "non-existent mailbox"
|
|
174
|
+
// errors (Chrome bug: race in shared_image_manager.cc acknowledged in a TODO).
|
|
175
|
+
requestAnimationFrame(() => LayoutPool._releaseMediaElementsSync(container));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static _releaseMediaElementsSync(container) {
|
|
170
179
|
let videoCount = 0;
|
|
171
180
|
let hlsCount = 0;
|
|
172
181
|
|
|
@@ -195,13 +204,43 @@ export class LayoutPool {
|
|
|
195
204
|
a.load();
|
|
196
205
|
});
|
|
197
206
|
|
|
207
|
+
// Release media inside iframes (embedded widgets with HLS streams, webcams, etc.)
|
|
208
|
+
// We can't querySelectorAll('video') across iframe boundaries, but we can:
|
|
209
|
+
// 1. Try to access same-origin iframe contentDocument
|
|
210
|
+
// 2. Force-remove the iframe src to stop all network activity
|
|
211
|
+
let iframeCount = 0;
|
|
212
|
+
container.querySelectorAll('iframe').forEach(iframe => {
|
|
213
|
+
try {
|
|
214
|
+
// Same-origin iframes: reach inside and release videos
|
|
215
|
+
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
216
|
+
if (doc) {
|
|
217
|
+
doc.querySelectorAll('video').forEach(v => {
|
|
218
|
+
v.pause();
|
|
219
|
+
v.removeAttribute('src');
|
|
220
|
+
v.load();
|
|
221
|
+
videoCount++;
|
|
222
|
+
});
|
|
223
|
+
doc.querySelectorAll('audio').forEach(a => {
|
|
224
|
+
a.pause();
|
|
225
|
+
a.removeAttribute('src');
|
|
226
|
+
a.load();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
} catch (_) {
|
|
230
|
+
// Cross-origin: can't access contentDocument
|
|
231
|
+
}
|
|
232
|
+
// Force stop all iframe network activity (HLS segments, SSE, WebSocket, etc.)
|
|
233
|
+
iframe.src = 'about:blank';
|
|
234
|
+
iframeCount++;
|
|
235
|
+
});
|
|
236
|
+
|
|
198
237
|
// Destroy PDF documents and release GPU canvas backing stores
|
|
199
238
|
container.querySelectorAll('.pdf-widget').forEach(el => {
|
|
200
239
|
if (el._pdfDestroy) el._pdfDestroy();
|
|
201
240
|
});
|
|
202
241
|
|
|
203
|
-
if (videoCount > 0) {
|
|
204
|
-
log.info(`Released ${videoCount} video(s)${hlsCount ? ` (${hlsCount} HLS)` : ''}`);
|
|
242
|
+
if (videoCount > 0 || iframeCount > 0) {
|
|
243
|
+
log.info(`Released ${videoCount} video(s)${hlsCount ? ` (${hlsCount} HLS)` : ''}${iframeCount ? `, ${iframeCount} iframe(s)` : ''}`);
|
|
205
244
|
}
|
|
206
245
|
}
|
|
207
246
|
|
package/src/renderer-lite.js
CHANGED
|
@@ -211,6 +211,7 @@ export class RendererLite {
|
|
|
211
211
|
this.currentLayout = null;
|
|
212
212
|
this.currentLayoutId = null;
|
|
213
213
|
this._preloadingLayoutId = null; // Set during preload for blob URL tracking
|
|
214
|
+
this._preloadingPromise = null; // Promise for in-flight preload (await instead of skip)
|
|
214
215
|
this.regions = new Map(); // regionId => { element, widgets, currentIndex, timer }
|
|
215
216
|
this.layoutTimer = null;
|
|
216
217
|
this.layoutEndEmitted = false; // Prevents double layoutEnd on stop after timer
|
|
@@ -1911,6 +1912,21 @@ export class RendererLite {
|
|
|
1911
1912
|
widgetElement._pdfCleanup();
|
|
1912
1913
|
}
|
|
1913
1914
|
|
|
1915
|
+
// Stop embedded widget iframes (HLS live streams, webcams, etc.)
|
|
1916
|
+
// Setting src=about:blank kills all network activity (HLS segment fetches,
|
|
1917
|
+
// WebSocket connections, SSE streams) and releases video decode buffers.
|
|
1918
|
+
const iframes = widgetElement.querySelectorAll('iframe');
|
|
1919
|
+
for (const iframe of iframes) {
|
|
1920
|
+
try {
|
|
1921
|
+
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
1922
|
+
if (doc) {
|
|
1923
|
+
doc.querySelectorAll('video').forEach(v => { v.pause(); v.removeAttribute('src'); v.load(); });
|
|
1924
|
+
doc.querySelectorAll('audio').forEach(a => { a.pause(); a.removeAttribute('src'); a.load(); });
|
|
1925
|
+
}
|
|
1926
|
+
} catch (_) {}
|
|
1927
|
+
iframe.src = 'about:blank';
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1914
1930
|
return { widget, animPromise };
|
|
1915
1931
|
}
|
|
1916
1932
|
|
|
@@ -2872,13 +2888,20 @@ export class RendererLite {
|
|
|
2872
2888
|
return true;
|
|
2873
2889
|
}
|
|
2874
2890
|
|
|
2875
|
-
//
|
|
2876
|
-
//
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2891
|
+
// If already in-flight, wait for it instead of skipping (prevents the race
|
|
2892
|
+
// where showLayout is called before the background preload finishes adding
|
|
2893
|
+
// the layout to the pool).
|
|
2894
|
+
if (this._preloadingLayoutId === layoutId && this._preloadingPromise) {
|
|
2895
|
+
this.log.info(`Layout ${layoutId} preload in-flight, waiting for it...`);
|
|
2896
|
+
return this._preloadingPromise;
|
|
2880
2897
|
}
|
|
2881
2898
|
|
|
2899
|
+
// Store the preload promise so concurrent callers can await it
|
|
2900
|
+
this._preloadingPromise = this._doPreloadLayout(xlfXml, layoutId);
|
|
2901
|
+
return this._preloadingPromise;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
async _doPreloadLayout(xlfXml, layoutId) {
|
|
2882
2905
|
try {
|
|
2883
2906
|
this.log.info(`Preloading layout ${layoutId} into pool...`);
|
|
2884
2907
|
|
|
@@ -2981,6 +3004,11 @@ export class RendererLite {
|
|
|
2981
3004
|
} catch (error) {
|
|
2982
3005
|
this.log.error(`Preload failed for layout ${layoutId}:`, error);
|
|
2983
3006
|
return false;
|
|
3007
|
+
} finally {
|
|
3008
|
+
if (this._preloadingLayoutId === layoutId) {
|
|
3009
|
+
this._preloadingLayoutId = null;
|
|
3010
|
+
this._preloadingPromise = null;
|
|
3011
|
+
}
|
|
2984
3012
|
}
|
|
2985
3013
|
}
|
|
2986
3014
|
|
|
@@ -3192,6 +3220,15 @@ export class RendererLite {
|
|
|
3192
3220
|
this._swapToPreloadedLayout(layoutId);
|
|
3193
3221
|
}
|
|
3194
3222
|
|
|
3223
|
+
/**
|
|
3224
|
+
* Check if the layout timer is active (running or deferred waiting for metadata).
|
|
3225
|
+
* Used to detect stalled layouts that need timer restart.
|
|
3226
|
+
* @returns {boolean}
|
|
3227
|
+
*/
|
|
3228
|
+
hasActiveLayoutTimer() {
|
|
3229
|
+
return this.layoutTimer !== null || this._deferredTimerLayoutId !== null;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3195
3232
|
/**
|
|
3196
3233
|
* Check if all regions have completed one full cycle
|
|
3197
3234
|
* This is informational only - layout timer is authoritative
|
|
@@ -817,12 +817,9 @@ describe('RendererLite', () => {
|
|
|
817
817
|
const region = { id: 'r1', widgets: [widget1, widget2], isDrawer: false };
|
|
818
818
|
renderer.regions = new Map([['r1', region]]);
|
|
819
819
|
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
// Probe the second
|
|
824
|
-
widget2.duration = 30;
|
|
825
|
-
widget2._probed = true;
|
|
820
|
+
// Once any video is probed, updateLayoutDuration has already run with
|
|
821
|
+
// real metadata, so _hasUnprobedVideos() returns false — no further
|
|
822
|
+
// deferral needed.
|
|
826
823
|
expect(renderer._hasUnprobedVideos()).toBe(false);
|
|
827
824
|
});
|
|
828
825
|
});
|