@xiboplayer/renderer 0.5.20 → 0.6.1
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.js +9 -9
- package/src/renderer-lite.js +42 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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.
|
|
17
|
-
"@xiboplayer/utils": "0.
|
|
16
|
+
"@xiboplayer/cache": "0.6.1",
|
|
17
|
+
"@xiboplayer/utils": "0.6.1"
|
|
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/
|
|
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 =
|
|
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}/
|
|
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}/
|
|
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}
|
|
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}/
|
|
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}/
|
|
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}
|
|
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;
|
package/src/renderer-lite.js
CHANGED
|
@@ -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}/
|
|
1524
|
+
audio.src = `${window.location.origin}${PLAYER_API}/media/${audioNode.uri}`;
|
|
1525
1525
|
});
|
|
1526
1526
|
} else if (!audioSrc) {
|
|
1527
|
-
audio.src = `${window.location.origin}/
|
|
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}/
|
|
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}/
|
|
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
|
|
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}/
|
|
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}/
|
|
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
|
-
//
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|