@xiboplayer/renderer 0.5.20 → 0.6.0

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.5.20",
3
+ "version": "0.6.0",
4
4
  "description": "RendererLite - Fast, efficient XLF layout rendering engine",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -13,8 +13,8 @@
13
13
  "dependencies": {
14
14
  "nanoevents": "^9.1.0",
15
15
  "pdfjs-dist": "^4.10.38",
16
- "@xiboplayer/cache": "0.5.20",
17
- "@xiboplayer/utils": "0.5.20"
16
+ "@xiboplayer/cache": "0.6.0",
17
+ "@xiboplayer/utils": "0.6.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "vitest": "^2.0.0",
package/src/layout.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { cacheWidgetHtml } from '@xiboplayer/cache';
7
- import { createLogger } from '@xiboplayer/utils';
7
+ import { createLogger, PLAYER_API } from '@xiboplayer/utils';
8
8
 
9
9
  const log = createLogger('Layout');
10
10
 
@@ -157,10 +157,10 @@ export class LayoutTranslator {
157
157
 
158
158
  // Try to get cached widget HTML from ContentStore via proxy
159
159
  try {
160
- const resp = await fetch(`/store/widget/${layoutId}/${regionId}/${id}`);
160
+ const resp = await fetch(`/store${PLAYER_API}/widgets/${layoutId}/${regionId}/${id}`);
161
161
  if (resp.ok) {
162
162
  raw = await resp.text();
163
- options.widgetCacheKey = `/cache/widget/${layoutId}/${regionId}/${id}`;
163
+ options.widgetCacheKey = `${PLAYER_API}/widgets/${layoutId}/${regionId}/${id}`;
164
164
  log.info(`Using stored widget HTML (${raw.length} chars) - CMS update pending`);
165
165
  } else {
166
166
  log.error(`No stored version available for widget ${id}`);
@@ -466,7 +466,7 @@ ${mediaJS}
466
466
  switch (media.type) {
467
467
  case 'image':
468
468
  // Use absolute URL within service worker scope
469
- const imageSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;
469
+ const imageSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;
470
470
  startFn = `() => {
471
471
  const region = document.getElementById('region_${regionId}');
472
472
  const img = document.createElement('img');
@@ -490,7 +490,7 @@ ${mediaJS}
490
490
  case 'video':
491
491
  // All videos use cache URL pattern
492
492
  // Background-downloaded videos will auto-reload when cache completes
493
- const videoSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;
493
+ const videoSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;
494
494
  const videoFilename = media.options.uri;
495
495
 
496
496
  startFn = `() => {
@@ -564,7 +564,7 @@ ${mediaJS}
564
564
  // Text/ticker widgets use the same iframe pattern as default widgets.
565
565
  // If no widgetCacheKey, fall through to the default case which handles unsupported types.
566
566
  if (media.options.widgetCacheKey) {
567
- const textUrl = `${window.location.origin}/player${media.options.widgetCacheKey}`;
567
+ const textUrl = `${window.location.origin}${media.options.widgetCacheKey}`;
568
568
  const iframe = this._generateIframeWidgetJS(regionId, media.id, textUrl, transIn, transOut);
569
569
  startFn = iframe.startFn;
570
570
  stopFn = iframe.stopFn;
@@ -573,7 +573,7 @@ ${mediaJS}
573
573
  // Fall through to default (handles missing widgetCacheKey as unsupported)
574
574
 
575
575
  case 'audio':
576
- const audioSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;
576
+ const audioSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;
577
577
  const audioId = `audio_${regionId}_${media.id}`;
578
578
  const audioLoop = media.options.loop === '1';
579
579
  const audioVolume = (parseInt(media.options.volume || '100') / 100).toFixed(2);
@@ -689,7 +689,7 @@ ${mediaJS}
689
689
  break;
690
690
 
691
691
  case 'pdf':
692
- const pdfSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;
692
+ const pdfSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;
693
693
  const pdfContainerId = `pdf_${regionId}_${media.id}`;
694
694
  const pdfDuration = duration; // Total duration for entire PDF
695
695
 
@@ -902,7 +902,7 @@ ${mediaJS}
902
902
  // Widgets (clock, calendar, weather, etc.) - use cache URL pattern in /player/ scope for SW
903
903
  // Keep widget iframes alive across duration cycles (arexibo behavior)
904
904
  if (media.options.widgetCacheKey) {
905
- const widgetUrl = `${window.location.origin}/player${media.options.widgetCacheKey}`;
905
+ const widgetUrl = `${window.location.origin}${media.options.widgetCacheKey}`;
906
906
  const iframe = this._generateIframeWidgetJS(regionId, media.id, widgetUrl, transIn, transOut);
907
907
  startFn = iframe.startFn;
908
908
  stopFn = iframe.stopFn;
@@ -40,7 +40,7 @@
40
40
  */
41
41
 
42
42
  import { createNanoEvents } from 'nanoevents';
43
- import { createLogger, isDebug } from '@xiboplayer/utils';
43
+ import { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';
44
44
  import { LayoutPool } from './layout-pool.js';
45
45
 
46
46
  /**
@@ -1521,10 +1521,10 @@ export class RendererLite {
1521
1521
  this.options.getMediaUrl(mediaId).then(url => {
1522
1522
  audio.src = url;
1523
1523
  }).catch(() => {
1524
- audio.src = `${window.location.origin}/player/cache/media/${audioNode.uri}`;
1524
+ audio.src = `${window.location.origin}${PLAYER_API}/media/${audioNode.uri}`;
1525
1525
  });
1526
1526
  } else if (!audioSrc) {
1527
- audio.src = `${window.location.origin}/player/cache/media/${audioNode.uri}`;
1527
+ audio.src = `${window.location.origin}${PLAYER_API}/media/${audioNode.uri}`;
1528
1528
  } else {
1529
1529
  audio.src = audioSrc;
1530
1530
  }
@@ -1893,7 +1893,7 @@ export class RendererLite {
1893
1893
  if (!imageSrc && this.options.getMediaUrl) {
1894
1894
  imageSrc = await this.options.getMediaUrl(fileId);
1895
1895
  } else if (!imageSrc) {
1896
- imageSrc = `${window.location.origin}/player/cache/media/${widget.options.uri}`;
1896
+ imageSrc = `${window.location.origin}${PLAYER_API}/media/${widget.options.uri}`;
1897
1897
  }
1898
1898
 
1899
1899
  img.src = imageSrc;
@@ -1938,7 +1938,7 @@ export class RendererLite {
1938
1938
  if (!videoSrc && this.options.getMediaUrl) {
1939
1939
  videoSrc = await this.options.getMediaUrl(fileId);
1940
1940
  } else if (!videoSrc) {
1941
- videoSrc = `${window.location.origin}/player/cache/media/${fileId}`;
1941
+ videoSrc = `${window.location.origin}${PLAYER_API}/media/${fileId}`;
1942
1942
  }
1943
1943
 
1944
1944
  // HLS/DASH streaming support
@@ -2009,7 +2009,8 @@ export class RendererLite {
2009
2009
  const error = video.error;
2010
2010
  const errorCode = error?.code;
2011
2011
  const errorMessage = error?.message || 'Unknown error';
2012
- this.log.warn(`Video error (non-fatal, logged only): ${fileId}, code: ${errorCode}, time: ${video.currentTime.toFixed(1)}s, message: ${errorMessage}`);
2012
+ this.log.warn(`Video error: ${fileId}, code: ${errorCode}, time: ${video.currentTime.toFixed(1)}s, message: ${errorMessage}`);
2013
+ this.emit('videoError', { fileId, errorCode, errorMessage, currentTime: video.currentTime });
2013
2014
  };
2014
2015
  video.addEventListener('error', onError);
2015
2016
 
@@ -2119,7 +2120,7 @@ export class RendererLite {
2119
2120
  if (!audioSrc && this.options.getMediaUrl) {
2120
2121
  audioSrc = await this.options.getMediaUrl(fileId);
2121
2122
  } else if (!audioSrc) {
2122
- audioSrc = `${window.location.origin}/player/cache/media/${fileId}`;
2123
+ audioSrc = `${window.location.origin}${PLAYER_API}/media/${fileId}`;
2123
2124
  }
2124
2125
 
2125
2126
  audio.src = audioSrc;
@@ -2258,7 +2259,14 @@ export class RendererLite {
2258
2259
  }
2259
2260
 
2260
2261
  /**
2261
- * Render PDF widget
2262
+ * Render PDF widget — single reusable canvas, page-by-page cycling.
2263
+ *
2264
+ * Memory strategy:
2265
+ * - One canvas is created and reused for all pages (no DOM churn)
2266
+ * - Each page is rendered sequentially (avoids concurrent render errors)
2267
+ * - page.cleanup() releases PDF.js internal page buffers after each render
2268
+ * - pdf.destroy() releases the entire document on widget teardown
2269
+ * - Active renderTask is cancelled on cleanup to prevent stale renders
2262
2270
  */
2263
2271
  async renderPdf(widget, region) {
2264
2272
  const container = document.createElement('div');
@@ -2292,7 +2300,7 @@ export class RendererLite {
2292
2300
  if (!pdfUrl && this.options.getMediaUrl) {
2293
2301
  pdfUrl = await this.options.getMediaUrl(fileId);
2294
2302
  } else if (!pdfUrl) {
2295
- pdfUrl = `${window.location.origin}/player/cache/media/${widget.options.uri}`;
2303
+ pdfUrl = `${window.location.origin}${PLAYER_API}/media/${widget.options.uri}`;
2296
2304
  }
2297
2305
 
2298
2306
  // Render PDF with multi-page cycling
@@ -2304,10 +2312,7 @@ export class RendererLite {
2304
2312
  const timePerPage = (duration * 1000) / totalPages;
2305
2313
  this.log.info(`[pdf] PDF loaded: ${totalPages} pages, ${duration}s duration, ${(timePerPage / 1000).toFixed(1)}s/page`);
2306
2314
 
2307
- // Single reused canvas render each page on-demand, call page.cleanup()
2308
- // after each render to release PDF.js internal buffers. Sequential rendering
2309
- // (one page at a time via setTimeout) avoids the "Cannot use the same canvas
2310
- // during multiple render() operations" error.
2315
+ // Measure page size from first page to set up the single reusable canvas
2311
2316
  const page1 = await pdf.getPage(1);
2312
2317
  const viewport0 = page1.getViewport({ scale: 1 });
2313
2318
  const scale = Math.min(region.width / viewport0.width, region.height / viewport0.height);
@@ -2321,23 +2326,37 @@ export class RendererLite {
2321
2326
  const ctx = canvas.getContext('2d');
2322
2327
  container.appendChild(canvas);
2323
2328
 
2324
- // Page indicator (bottom-right)
2329
+ // Page indicator (bottom-right, v1-style pill)
2325
2330
  const indicator = document.createElement('div');
2326
- indicator.style.cssText = 'position:absolute;bottom:8px;right:12px;color:rgba(255,255,255,0.6);font:12px system-ui;z-index:1;';
2331
+ 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;';
2327
2332
  container.appendChild(indicator);
2328
2333
 
2329
2334
  let currentPage = 1;
2330
2335
  let cycleTimer = null;
2336
+ let activeRenderTask = null;
2331
2337
  let stopped = false;
2332
2338
 
2339
+ // Render one page at a time on the single canvas. Sequential scheduling
2340
+ // (setTimeout after render completes) avoids the "Cannot use the same
2341
+ // canvas during multiple render() operations" error from PDF.js.
2333
2342
  const cyclePage = async () => {
2334
2343
  if (stopped) return;
2335
- indicator.textContent = `${currentPage} / ${totalPages}`;
2344
+ indicator.textContent = `Page ${currentPage} / ${totalPages}`;
2336
2345
 
2337
2346
  const page = await pdf.getPage(currentPage);
2338
2347
  const scaledViewport = page.getViewport({ scale });
2348
+
2349
+ // Clear and render on the reusable canvas
2339
2350
  ctx.clearRect(0, 0, canvas.width, canvas.height);
2340
- await page.render({ canvasContext: ctx, viewport: scaledViewport }).promise;
2351
+ activeRenderTask = page.render({ canvasContext: ctx, viewport: scaledViewport });
2352
+ try {
2353
+ await activeRenderTask.promise;
2354
+ } catch (e) {
2355
+ // RenderingCancelledException is expected when stopped during render
2356
+ if (stopped) return;
2357
+ throw e;
2358
+ }
2359
+ activeRenderTask = null;
2341
2360
  page.cleanup(); // Release PDF.js internal page buffers
2342
2361
 
2343
2362
  // Schedule next page (only after current render completes)
@@ -2351,14 +2370,18 @@ export class RendererLite {
2351
2370
 
2352
2371
  await cyclePage();
2353
2372
 
2354
- // Store cleanup function on container for when widget is removed
2373
+ // Cleanup: cancel active render, clear timer, release PDF document
2355
2374
  container._pdfCleanup = () => {
2356
2375
  stopped = true;
2357
2376
  if (cycleTimer) clearTimeout(cycleTimer);
2358
2377
  cycleTimer = null;
2378
+ if (activeRenderTask) {
2379
+ activeRenderTask.cancel();
2380
+ activeRenderTask = null;
2381
+ }
2382
+ // Zero canvas dimensions to release GPU backing store
2359
2383
  canvas.width = 0;
2360
2384
  canvas.height = 0;
2361
- pdf.cleanup();
2362
2385
  pdf.destroy();
2363
2386
  };
2364
2387