@xiboplayer/renderer 0.7.2 → 0.7.3

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.2",
3
+ "version": "0.7.3",
4
4
  "description": "RendererLite - Fast, efficient XLF layout rendering engine",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -12,13 +12,13 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "pdfjs-dist": "^4.10.38",
15
- "@xiboplayer/cache": "0.7.2",
16
- "@xiboplayer/schedule": "0.7.2",
17
- "@xiboplayer/utils": "0.7.2"
15
+ "@xiboplayer/schedule": "0.7.3",
16
+ "@xiboplayer/utils": "0.7.3",
17
+ "@xiboplayer/cache": "0.7.3"
18
18
  },
19
19
  "devDependencies": {
20
- "vitest": "^2.0.0",
21
- "jsdom": "^25.0.0"
20
+ "jsdom": "^25.0.1",
21
+ "vitest": "^2.1.9"
22
22
  },
23
23
  "keywords": [
24
24
  "xibo",
package/src/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export class LayoutPool {
21
21
  get(layoutId: number): any | undefined;
22
22
  add(layoutId: number, entry: any): void;
23
23
  clearWarmNotIn(keepIds: Set<number>): number;
24
- makeHot(layoutId: number): void;
25
- remove(layoutId: number): void;
24
+ setHot(layoutId: number): void;
25
+ evict(layoutId: number): void;
26
26
  clear(): void;
27
27
  }
28
28
 
@@ -59,10 +59,10 @@ export class RendererLite {
59
59
  nextWidget(regionId?: string): void;
60
60
  previousWidget(regionId?: string): void;
61
61
 
62
+ resumeRegionMedia(regionId: string): void;
62
63
  pause(): void;
63
64
  resume(): void;
64
65
  isPaused(): boolean;
65
- resumeRegionMedia?(regionId: string): void;
66
66
  showLayout(layoutId?: number): void;
67
67
  getCurrentLayoutId(): number | null;
68
68
 
@@ -219,7 +219,6 @@ export class RendererLite {
219
219
  this._paused = false;
220
220
  this._layoutTimerStartedAt = null; // Date.now() when layout timer started
221
221
  this._layoutTimerDurationMs = null; // Total layout duration in ms
222
- this.widgetTimers = new Map(); // widgetId => timer
223
222
  this.layoutBlobUrls = new Map(); // layoutId => Set<blobUrl> (for lifecycle tracking)
224
223
  this.audioOverlays = new Map(); // widgetId => [HTMLAudioElement] (audio overlays for widgets)
225
224
 
@@ -1339,26 +1338,64 @@ export class RendererLite {
1339
1338
  }
1340
1339
 
1341
1340
  /**
1342
- * Create a region element
1343
- * @param {Object} regionConfig - Region configuration
1341
+ * Build a region DOM element and state entry.
1342
+ * Shared by createRegion, preloadLayout, and renderOverlay.
1343
+ *
1344
+ * @param {Object} regionConfig - Region configuration from parsed XLF
1345
+ * @param {string} elementId - DOM element ID for the region div
1346
+ * @param {HTMLElement} parentEl - Parent element to append the region to
1347
+ * @param {Object} [extraState] - Additional properties merged into region state
1348
+ * @returns {Object} Region state object { element, config, widgets, ... }
1344
1349
  */
1345
- async createRegion(regionConfig) {
1350
+ _createRegionEntry(regionConfig, elementId, parentEl, extraState = {}) {
1351
+ const { className = 'renderer-lite-region', ...stateProps } = extraState;
1352
+
1346
1353
  const regionEl = document.createElement('div');
1347
- regionEl.id = `region_${regionConfig.id}`;
1348
- regionEl.className = 'renderer-lite-region';
1354
+ regionEl.id = elementId;
1355
+ regionEl.className = className;
1349
1356
  regionEl.style.position = 'absolute';
1350
- regionEl.style.zIndex = regionConfig.zindex;
1357
+ regionEl.style.zIndex = String(regionConfig.zindex);
1351
1358
  regionEl.style.overflow = 'hidden';
1352
1359
 
1353
- // Drawer regions start fully hidden — shown only by navWidget actions
1354
- if (regionConfig.isDrawer) {
1355
- regionEl.style.display = 'none';
1356
- }
1357
-
1358
1360
  // Apply scaled positioning
1359
1361
  this.applyRegionScale(regionEl, regionConfig);
1360
1362
 
1361
- this.container.appendChild(regionEl);
1363
+ parentEl.appendChild(regionEl);
1364
+
1365
+ const sf = this.scaleFactor;
1366
+ return {
1367
+ element: regionEl,
1368
+ config: regionConfig,
1369
+ widgets: regionConfig.widgets,
1370
+ currentIndex: 0,
1371
+ timer: null,
1372
+ width: regionConfig.width * sf,
1373
+ height: regionConfig.height * sf,
1374
+ complete: false,
1375
+ widgetElements: new Map(),
1376
+ ...stateProps,
1377
+ };
1378
+ }
1379
+
1380
+ /**
1381
+ * Create a region element
1382
+ * @param {Object} regionConfig - Region configuration
1383
+ */
1384
+ async createRegion(regionConfig) {
1385
+ const region = this._createRegionEntry(
1386
+ regionConfig,
1387
+ `region_${regionConfig.id}`,
1388
+ this.container,
1389
+ {
1390
+ isDrawer: regionConfig.isDrawer || false,
1391
+ isCanvas: regionConfig.isCanvas || false,
1392
+ }
1393
+ );
1394
+
1395
+ // Drawer regions start fully hidden — shown only by navWidget actions
1396
+ if (regionConfig.isDrawer) {
1397
+ region.element.style.display = 'none';
1398
+ }
1362
1399
 
1363
1400
  // Filter expired widgets (fromDt/toDt time-gating within XLF)
1364
1401
  let widgets = regionConfig.widgets.filter(w => this._isWidgetActive(w));
@@ -1367,22 +1404,9 @@ export class RendererLite {
1367
1404
  if (widgets.some(w => w.cyclePlayback)) {
1368
1405
  widgets = this._applyCyclePlayback(widgets);
1369
1406
  }
1407
+ region.widgets = widgets;
1370
1408
 
1371
- // Store region state (dimensions use scaled values for transitions)
1372
- const sf = this.scaleFactor;
1373
- this.regions.set(regionConfig.id, {
1374
- element: regionEl,
1375
- config: regionConfig,
1376
- widgets,
1377
- currentIndex: 0,
1378
- timer: null,
1379
- width: regionConfig.width * sf,
1380
- height: regionConfig.height * sf,
1381
- complete: false, // Track if region has played all widgets once
1382
- isDrawer: regionConfig.isDrawer || false,
1383
- isCanvas: regionConfig.isCanvas || false, // Canvas regions render all widgets simultaneously
1384
- widgetElements: new Map() // widgetId -> DOM element (for element reuse)
1385
- });
1409
+ this.regions.set(regionConfig.id, region);
1386
1410
  }
1387
1411
 
1388
1412
  /**
@@ -2502,45 +2526,7 @@ export class RendererLite {
2502
2526
  * Render text/ticker widget
2503
2527
  */
2504
2528
  async renderTextWidget(widget, region) {
2505
- const iframe = document.createElement('iframe');
2506
- iframe.className = 'renderer-lite-widget';
2507
- iframe.style.width = '100%';
2508
- iframe.style.height = '100%';
2509
- iframe.style.border = 'none';
2510
- iframe.style.opacity = '0';
2511
-
2512
- // Get widget HTML (may return { url } for cache-path loading or string for blob)
2513
- let html = widget.raw;
2514
- if (this.options.getWidgetHtml) {
2515
- const result = await this.options.getWidgetHtml(widget);
2516
- if (result && typeof result === 'object' && result.url) {
2517
- // Use cache URL — SW serves HTML and intercepts sub-resources
2518
- iframe.src = result.url;
2519
-
2520
- // Parse NUMITEMS/DURATION from fallback HTML (cache path)
2521
- if (result.fallback) {
2522
- this._parseDurationComments(result.fallback, widget);
2523
- }
2524
-
2525
- return iframe;
2526
- }
2527
- html = result;
2528
- }
2529
-
2530
- if (html) {
2531
- // Parse NUMITEMS/DURATION HTML comments for dynamic widget duration
2532
- this._parseDurationComments(html, widget);
2533
- }
2534
-
2535
- // Fallback: Create blob URL for iframe
2536
- const blob = new Blob([html], { type: 'text/html' });
2537
- const blobUrl = URL.createObjectURL(blob);
2538
- iframe.src = blobUrl;
2539
-
2540
- // Track blob URL for lifecycle management
2541
- this.trackBlobUrl(blobUrl);
2542
-
2543
- return iframe;
2529
+ return await this._renderIframeWidget(widget, region);
2544
2530
  }
2545
2531
 
2546
2532
  /**
@@ -2723,6 +2709,15 @@ export class RendererLite {
2723
2709
  * Render generic widget (clock, calendar, weather, etc.)
2724
2710
  */
2725
2711
  async renderGenericWidget(widget, region) {
2712
+ return await this._renderIframeWidget(widget, region);
2713
+ }
2714
+
2715
+ /**
2716
+ * Shared iframe rendering for text/ticker and generic widgets.
2717
+ * Creates an iframe, resolves widget HTML via getWidgetHtml (cache URL or blob),
2718
+ * and parses NUMITEMS/DURATION comments for dynamic widget duration.
2719
+ */
2720
+ async _renderIframeWidget(widget, region) {
2726
2721
  const iframe = document.createElement('iframe');
2727
2722
  iframe.className = 'renderer-lite-widget';
2728
2723
  iframe.style.width = '100%';
@@ -2886,33 +2881,12 @@ export class RendererLite {
2886
2881
 
2887
2882
  // Create regions in the hidden wrapper
2888
2883
  const preloadRegions = new Map();
2889
- const sf = this.scaleFactor;
2890
-
2891
2884
  for (const regionConfig of layout.regions) {
2892
- const regionEl = document.createElement('div');
2893
- regionEl.id = `preload_region_${layoutId}_${regionConfig.id}`;
2894
- regionEl.className = 'renderer-lite-region';
2895
- regionEl.style.position = 'absolute';
2896
- regionEl.style.zIndex = regionConfig.zindex;
2897
- regionEl.style.overflow = 'hidden';
2898
-
2899
- // Apply scaled positioning
2900
- this.applyRegionScale(regionEl, regionConfig);
2901
-
2902
- wrapper.appendChild(regionEl);
2903
-
2904
- const region = {
2905
- element: regionEl,
2906
- config: regionConfig,
2907
- widgets: regionConfig.widgets,
2908
- currentIndex: 0,
2909
- timer: null,
2910
- width: regionConfig.width * sf,
2911
- height: regionConfig.height * sf,
2912
- complete: false,
2913
- widgetElements: new Map()
2914
- };
2915
-
2885
+ const region = this._createRegionEntry(
2886
+ regionConfig,
2887
+ `preload_region_${layoutId}_${regionConfig.id}`,
2888
+ wrapper
2889
+ );
2916
2890
  preloadRegions.set(regionConfig.id, region);
2917
2891
  }
2918
2892
 
@@ -2993,20 +2967,7 @@ export class RendererLite {
2993
2967
 
2994
2968
  // ── Tear down old layout ──
2995
2969
  this.removeActionListeners();
2996
-
2997
- if (this.layoutTimer) {
2998
- clearTimeout(this.layoutTimer);
2999
- this.layoutTimer = null;
3000
- }
3001
-
3002
- if (this.preloadTimer) {
3003
- clearTimeout(this.preloadTimer);
3004
- this.preloadTimer = null;
3005
- }
3006
- if (this._preloadRetryTimer) {
3007
- clearTimeout(this._preloadRetryTimer);
3008
- this._preloadRetryTimer = null;
3009
- }
2970
+ this._clearLayoutTimers();
3010
2971
 
3011
2972
  const oldLayoutId = this.currentLayoutId;
3012
2973
  const alreadyEmittedEnd = this.layoutEndEmitted;
@@ -3176,6 +3137,24 @@ export class RendererLite {
3176
3137
  }
3177
3138
  }
3178
3139
 
3140
+ /**
3141
+ * Clear all layout-level timers (layout duration, preload, preload retry).
3142
+ */
3143
+ _clearLayoutTimers() {
3144
+ if (this.layoutTimer) {
3145
+ clearTimeout(this.layoutTimer);
3146
+ this.layoutTimer = null;
3147
+ }
3148
+ if (this.preloadTimer) {
3149
+ clearTimeout(this.preloadTimer);
3150
+ this.preloadTimer = null;
3151
+ }
3152
+ if (this._preloadRetryTimer) {
3153
+ clearTimeout(this._preloadRetryTimer);
3154
+ this._preloadRetryTimer = null;
3155
+ }
3156
+ }
3157
+
3179
3158
  /**
3180
3159
  * Stop current layout
3181
3160
  */
@@ -3197,18 +3176,7 @@ export class RendererLite {
3197
3176
  this.currentLayoutId = null;
3198
3177
 
3199
3178
  // Clear timers
3200
- if (this.layoutTimer) {
3201
- clearTimeout(this.layoutTimer);
3202
- this.layoutTimer = null;
3203
- }
3204
- if (this.preloadTimer) {
3205
- clearTimeout(this.preloadTimer);
3206
- this.preloadTimer = null;
3207
- }
3208
- if (this._preloadRetryTimer) {
3209
- clearTimeout(this._preloadRetryTimer);
3210
- this._preloadRetryTimer = null;
3211
- }
3179
+ this._clearLayoutTimers();
3212
3180
 
3213
3181
  // Remove interactive action listeners before teardown
3214
3182
  this.removeActionListeners();
@@ -3298,33 +3266,17 @@ export class RendererLite {
3298
3266
 
3299
3267
  // Create regions for overlay
3300
3268
  const overlayRegions = new Map();
3301
- const sf = this.scaleFactor;
3302
3269
  for (const regionConfig of layout.regions) {
3303
- const regionEl = document.createElement('div');
3304
- regionEl.id = `overlay_${layoutId}_region_${regionConfig.id}`;
3305
- regionEl.className = 'renderer-lite-region overlay-region';
3306
- regionEl.style.position = 'absolute';
3307
- regionEl.style.zIndex = String(regionConfig.zindex);
3308
- regionEl.style.overflow = 'hidden';
3309
-
3310
- // Apply scaled positioning
3311
- this.applyRegionScale(regionEl, regionConfig);
3312
-
3313
- overlayDiv.appendChild(regionEl);
3314
-
3315
- // Store region state (dimensions use scaled values)
3316
- overlayRegions.set(regionConfig.id, {
3317
- element: regionEl,
3318
- config: regionConfig,
3319
- widgets: regionConfig.widgets,
3320
- currentIndex: 0,
3321
- timer: null,
3322
- width: regionConfig.width * sf,
3323
- height: regionConfig.height * sf,
3324
- complete: false,
3325
- isCanvas: regionConfig.isCanvas || false,
3326
- widgetElements: new Map()
3327
- });
3270
+ const region = this._createRegionEntry(
3271
+ regionConfig,
3272
+ `overlay_${layoutId}_region_${regionConfig.id}`,
3273
+ overlayDiv,
3274
+ {
3275
+ className: 'renderer-lite-region overlay-region',
3276
+ isCanvas: regionConfig.isCanvas || false,
3277
+ }
3278
+ );
3279
+ overlayRegions.set(regionConfig.id, region);
3328
3280
  }
3329
3281
 
3330
3282
  // Pre-create widget elements for overlay