@xiboplayer/renderer 0.7.7 → 0.7.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/renderer",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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/schedule": "0.7.7",
16
- "@xiboplayer/utils": "0.7.7",
17
- "@xiboplayer/cache": "0.7.7"
15
+ "@xiboplayer/cache": "0.7.8",
16
+ "@xiboplayer/schedule": "0.7.8",
17
+ "@xiboplayer/utils": "0.7.8"
18
18
  },
19
19
  "devDependencies": {
20
20
  "jsdom": "^25.0.1",
package/src/index.d.ts CHANGED
@@ -74,6 +74,7 @@ export class RendererLite {
74
74
 
75
75
  updateLayoutDuration(): void;
76
76
  checkLayoutComplete(): void;
77
+ hasActiveLayoutTimer(): boolean;
77
78
 
78
79
  cleanup(): void;
79
80
  }
@@ -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
 
@@ -1911,6 +1911,21 @@ export class RendererLite {
1911
1911
  widgetElement._pdfCleanup();
1912
1912
  }
1913
1913
 
1914
+ // Stop embedded widget iframes (HLS live streams, webcams, etc.)
1915
+ // Setting src=about:blank kills all network activity (HLS segment fetches,
1916
+ // WebSocket connections, SSE streams) and releases video decode buffers.
1917
+ const iframes = widgetElement.querySelectorAll('iframe');
1918
+ for (const iframe of iframes) {
1919
+ try {
1920
+ const doc = iframe.contentDocument || iframe.contentWindow?.document;
1921
+ if (doc) {
1922
+ doc.querySelectorAll('video').forEach(v => { v.pause(); v.removeAttribute('src'); v.load(); });
1923
+ doc.querySelectorAll('audio').forEach(a => { a.pause(); a.removeAttribute('src'); a.load(); });
1924
+ }
1925
+ } catch (_) {}
1926
+ iframe.src = 'about:blank';
1927
+ }
1928
+
1914
1929
  return { widget, animPromise };
1915
1930
  }
1916
1931
 
@@ -3192,6 +3207,15 @@ export class RendererLite {
3192
3207
  this._swapToPreloadedLayout(layoutId);
3193
3208
  }
3194
3209
 
3210
+ /**
3211
+ * Check if the layout timer is active (running or deferred waiting for metadata).
3212
+ * Used to detect stalled layouts that need timer restart.
3213
+ * @returns {boolean}
3214
+ */
3215
+ hasActiveLayoutTimer() {
3216
+ return this.layoutTimer !== null || this._deferredTimerLayoutId !== null;
3217
+ }
3218
+
3195
3219
  /**
3196
3220
  * Check if all regions have completed one full cycle
3197
3221
  * 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
- // One still unprobed
821
- expect(renderer._hasUnprobedVideos()).toBe(true);
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
  });