@xiboplayer/renderer 0.6.3 → 0.6.4
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/README.md +240 -25
- package/docs/RENDERER_COMPARISON.md +1 -1
- package/package.json +4 -4
- package/src/renderer-lite.js +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,53 @@
|
|
|
1
1
|
# @xiboplayer/renderer
|
|
2
2
|
|
|
3
|
-
**XLF layout rendering engine for
|
|
3
|
+
**Fast, memory-efficient XLF layout rendering engine for digital signage.**
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Parses Xibo Layout Format (XLF) files and builds a live DOM with element reuse, instant transitions, and a 2-layout preload pool:
|
|
8
8
|
|
|
9
|
-
- **Rich media**
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
9
|
+
- **Rich media** -- video (MP4/HLS), images (scaleType, align/valign), audio (with visualization), PDF (PDF.js), text/ticker, web pages, clock, calendar, weather, and all CMS widget types
|
|
10
|
+
- **Instant layout transitions** -- 2-layout preload pool keeps the next layout DOM-ready for zero-gap swap
|
|
11
|
+
- **Element reuse** -- pre-creates all widget elements upfront; cycling reuses them instead of destroying/rebuilding
|
|
12
|
+
- **Transitions** -- fade and fly animations with 8 compass directions via Web Animations API
|
|
13
|
+
- **Canvas regions** -- simultaneous multi-widget rendering (stacked layers)
|
|
14
|
+
- **Drawer regions** -- hidden action-triggered areas for interactive controls
|
|
15
|
+
- **Overlays** -- multiple floating layouts with priority z-indexing
|
|
16
|
+
- **Interactive actions** -- touch/click and keyboard triggers for widget navigation, layout jumps, and command execution
|
|
17
|
+
- **Shell commands** -- native command execution via Electron IPC and Chromium HTTP endpoint
|
|
18
|
+
- **Sub-playlist cycling** -- round-robin or random selection from widget groups
|
|
19
|
+
- **Dynamic duration** -- video/audio metadata overrides for accurate timing
|
|
20
|
+
- **Scale-to-fit** -- responsive scaling for any screen size via ResizeObserver
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
XLF XML -> RendererLite
|
|
26
|
+
+- parseXlf() -> Layout object {width, height, regions: []}
|
|
27
|
+
+- renderLayout()
|
|
28
|
+
+- createRegion() for each region
|
|
29
|
+
| +- createWidgetElement() for each widget
|
|
30
|
+
| +- renderImage() [<img>]
|
|
31
|
+
| +- renderVideo() [<video>] (HLS/DASH)
|
|
32
|
+
| +- renderAudio() [<audio> + visual]
|
|
33
|
+
| +- renderTextWidget() [<iframe>] (GetResource)
|
|
34
|
+
| +- renderPdf() [<canvas>] (multi-page)
|
|
35
|
+
| +- renderWebpage() [<iframe>]
|
|
36
|
+
| +- renderVideoIn() [<video>] (webcam)
|
|
37
|
+
| +- renderGenericWidget() [<iframe>] (clock, etc.)
|
|
38
|
+
|
|
|
39
|
+
+- attachActionListeners() for touch/click/keyboard
|
|
40
|
+
+- startRegion() -> _startRegionCycle()
|
|
41
|
+
| +- renderWidget() / stopWidget() cycling
|
|
42
|
+
|
|
|
43
|
+
+- startLayoutTimerWhenReady()
|
|
44
|
+
+- Waits for all initial widgets to load
|
|
45
|
+
+- Starts layout duration timer
|
|
46
|
+
|
|
47
|
+
Layout Pool (2 entries max):
|
|
48
|
+
+- Hot entry (visible, currently playing)
|
|
49
|
+
+- Warm entry (preloaded, hidden, ready for instant swap)
|
|
50
|
+
```
|
|
17
51
|
|
|
18
52
|
## Installation
|
|
19
53
|
|
|
@@ -23,33 +57,214 @@ npm install @xiboplayer/renderer
|
|
|
23
57
|
|
|
24
58
|
## Usage
|
|
25
59
|
|
|
60
|
+
### Basic rendering
|
|
61
|
+
|
|
26
62
|
```javascript
|
|
27
63
|
import { RendererLite } from '@xiboplayer/renderer';
|
|
28
64
|
|
|
29
|
-
const renderer = new RendererLite(
|
|
30
|
-
|
|
65
|
+
const renderer = new RendererLite(
|
|
66
|
+
{ cmsUrl: 'https://cms.example.com', hardwareKey: 'DISPLAY-001' },
|
|
67
|
+
document.getElementById('player')
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
renderer.on('layoutStart', (layoutId, layout) => {
|
|
71
|
+
console.log(`Layout ${layoutId} started (${layout.duration}s)`);
|
|
31
72
|
});
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
|
|
74
|
+
renderer.on('layoutEnd', (layoutId) => {
|
|
75
|
+
console.log(`Layout ${layoutId} ended`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await renderer.renderLayout(xlfXmlContent, 42);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Preloading for instant transitions
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// At 75% of current layout duration, renderer emits:
|
|
85
|
+
renderer.on('request-next-layout-preload', async () => {
|
|
86
|
+
const nextLayout = await getNextLayoutFromSchedule();
|
|
87
|
+
if (nextLayout && !renderer.hasPreloadedLayout(nextLayout.id)) {
|
|
88
|
+
await renderer.preloadLayout(nextLayout.xlf, nextLayout.id);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// When it's time to show next layout, swap is INSTANT if preloaded
|
|
93
|
+
await renderer.renderLayout(nextXlfXml, nextLayoutId);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Overlays
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
// Render an overlay on top of the main layout
|
|
100
|
+
await renderer.renderOverlay(alertXlfXml, 101, 10);
|
|
101
|
+
|
|
102
|
+
renderer.on('overlayEnd', (overlayId) => console.log('Overlay done'));
|
|
103
|
+
|
|
104
|
+
// Stop overlay
|
|
105
|
+
renderer.stopOverlay(101);
|
|
35
106
|
```
|
|
36
107
|
|
|
37
108
|
## Widget Types
|
|
38
109
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
110
|
+
| Type | Element | Source | Notes |
|
|
111
|
+
|------|---------|--------|-------|
|
|
112
|
+
| **image** | `<img>` | Media file | Object-fit: center/stretch/fit. objectPosition: top/middle/bottom, left/center/right |
|
|
113
|
+
| **video** | `<video>` | Media file | HLS via native (Safari) + hls.js fallback. Pause-on-last-frame. Duration detection |
|
|
114
|
+
| **audio** | `<audio>` + visual | Media file | Gradient visualization + music note icon. Volume control. Loop option |
|
|
115
|
+
| **videoin** | `<video>` | getUserMedia() | Webcam/mic capture with mirror mode |
|
|
116
|
+
| **text** | `<iframe>` | GetResource | CMS-rendered HTML. Parses NUMITEMS/DURATION comments |
|
|
117
|
+
| **ticker** | `<iframe>` | GetResource | Data feeds with dynamic cycling |
|
|
118
|
+
| **pdf** | `<canvas>` | Media file | PDF.js multi-page cycling. Page indicator. Time-per-page = duration / pages |
|
|
119
|
+
| **webpage** | `<iframe>` | Direct URL | modeId=1 (URL), modeId=0 (GetResource) |
|
|
120
|
+
| **clock** | `<iframe>` | GetResource | Digital/analogue variants |
|
|
121
|
+
| **calendar** | `<iframe>` | GetResource | Calendar widget HTML |
|
|
122
|
+
| **weather** | `<iframe>` | GetResource | Weather service HTML |
|
|
123
|
+
| **global** | Stacked iframes | GetResource | Canvas region auto-detect for multi-layer content |
|
|
124
|
+
| **all others** | `<iframe>` | GetResource | Generic CMS widget HTML |
|
|
125
|
+
|
|
126
|
+
## Layout Lifecycle
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
XLF XML
|
|
130
|
+
|
|
|
131
|
+
+- parseXlf() -> {width, height, regions: [{widgets: [...]}]}
|
|
132
|
+
|
|
|
133
|
+
+- calculateScale() -> fit layout to screen
|
|
134
|
+
|
|
|
135
|
+
+- createRegion() for each region
|
|
136
|
+
| +- Filter widgets (fromDt/toDt time-gating)
|
|
137
|
+
| +- Apply sub-playlist cycling
|
|
138
|
+
|
|
|
139
|
+
+- createWidgetElement() for each widget (pre-creation)
|
|
140
|
+
| +- Create DOM element, position absolute, hidden
|
|
141
|
+
| +- Track blob URLs for cleanup
|
|
142
|
+
|
|
|
143
|
+
+- startRegion() for each region
|
|
144
|
+
| +- Canvas: show ALL widgets at once, timer = max duration
|
|
145
|
+
| +- Normal: cycle widgets with transitions
|
|
146
|
+
|
|
|
147
|
+
+- startLayoutTimerWhenReady()
|
|
148
|
+
| +- Wait for video.playing / img.load (or 10s timeout)
|
|
149
|
+
| +- Start layout duration timer
|
|
150
|
+
|
|
|
151
|
+
+- At 75%: emit 'request-next-layout-preload'
|
|
152
|
+
|
|
|
153
|
+
+- Layout duration expires
|
|
154
|
+
+- emit('layoutEnd', layoutId)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Element reuse flow
|
|
158
|
+
|
|
159
|
+
1. All widget elements pre-created during `renderLayout()` -- hidden, opacity 0
|
|
160
|
+
2. Cycling: `renderWidget()` shows current, `stopWidget()` hides (same elements)
|
|
161
|
+
3. No DOM destruction/recreation -- smooth transitions, instant replay
|
|
162
|
+
4. When layout evicted from pool: blob URLs revoked, elements removed
|
|
163
|
+
|
|
164
|
+
### Ready-wait gating
|
|
165
|
+
|
|
166
|
+
Layout timer starts only when all initial widgets are loaded:
|
|
167
|
+
- Video: waits for `playing` event
|
|
168
|
+
- Image: waits for `load` event
|
|
169
|
+
- Text/embedded: ready immediately
|
|
170
|
+
- Timeout: 10s per widget (don't block on broken media)
|
|
171
|
+
|
|
172
|
+
## Transitions
|
|
173
|
+
|
|
174
|
+
Defined per-widget in XLF options:
|
|
175
|
+
|
|
176
|
+
**Fade:** `fade`, `fadein`, `fadeout` -- opacity animation
|
|
177
|
+
|
|
178
|
+
**Fly:** `fly`, `flyin`, `flyout` -- translate animation with compass direction:
|
|
179
|
+
- `N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`
|
|
180
|
+
|
|
181
|
+
```xml
|
|
182
|
+
<media duration="5">
|
|
183
|
+
<options>
|
|
184
|
+
<transIn>fly</transIn>
|
|
185
|
+
<transInDuration>1000</transInDuration>
|
|
186
|
+
<transInDirection>NE</transInDirection>
|
|
187
|
+
<transOut>fade</transOut>
|
|
188
|
+
<transOutDuration>500</transOutDuration>
|
|
189
|
+
</options>
|
|
190
|
+
</media>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Interactive Actions
|
|
194
|
+
|
|
195
|
+
Actions defined in XLF at layout, region, or widget level:
|
|
196
|
+
|
|
197
|
+
**Trigger types:** `touch` (click), `keyboard:KEY` (e.g., `keyboard:n`, `keyboard:Enter`)
|
|
198
|
+
|
|
199
|
+
**Action types:** `navWidget` (jump to widget), `nextWidget`, `previousWidget`, `shellCommand`
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
renderer.on('action-trigger', (actionData) => {
|
|
203
|
+
console.log(`Action: ${actionData.actionType}`, actionData);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Programmatic navigation
|
|
207
|
+
renderer.navigateToWidget('target-widget-id');
|
|
208
|
+
renderer.nextWidget('region-id');
|
|
209
|
+
renderer.previousWidget('region-id');
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Events
|
|
213
|
+
|
|
214
|
+
| Event | Args | Description |
|
|
215
|
+
|-------|------|-------------|
|
|
216
|
+
| `layoutStart` | `(layoutId, layout)` | Layout DOM built and playback started |
|
|
217
|
+
| `layoutEnd` | `(layoutId)` | Layout duration expired |
|
|
218
|
+
| `layoutDurationUpdated` | `(layoutId, newDuration)` | Video metadata revealed actual duration |
|
|
219
|
+
| `widgetStart` | `({ widgetId, regionId, layoutId, type, duration, enableStat })` | Widget now visible |
|
|
220
|
+
| `widgetEnd` | `({ widgetId, regionId, layoutId, type, enableStat })` | Widget hidden/cycled |
|
|
221
|
+
| `widgetCommand` | `({ commandCode, commandString, widgetId, regionId, layoutId })` | Shell command triggered |
|
|
222
|
+
| `widgetAction` | `({ type, widgetId, layoutId, regionId, url })` | Widget webhook URL |
|
|
223
|
+
| `videoError` | `({ storedAs, fileId, errorCode, errorMessage })` | Video playback error |
|
|
224
|
+
| `overlayStart` | `(overlayId, layout)` | Overlay rendered |
|
|
225
|
+
| `overlayEnd` | `(overlayId)` | Overlay finished |
|
|
226
|
+
| `action-trigger` | `({ actionType, triggerType, targetId, commandCode })` | Touch/keyboard action fired |
|
|
227
|
+
| `request-next-layout-preload` | `()` | 75% through layout -- preload next |
|
|
228
|
+
| `error` | `({ type, error, layoutId, widgetId })` | Generic error |
|
|
229
|
+
|
|
230
|
+
## API Reference
|
|
231
|
+
|
|
232
|
+
### Constructor
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
new RendererLite(config, container, options?)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
| Parameter | Type | Description |
|
|
239
|
+
|-----------|------|-------------|
|
|
240
|
+
| `config` | Object | `{ cmsUrl, hardwareKey }` |
|
|
241
|
+
| `container` | HTMLElement | DOM element to render into |
|
|
242
|
+
| `options.getWidgetHtml` | Function? | `(widget) => htmlString` -- fetch widget HTML from cache |
|
|
243
|
+
| `options.fileIdToSaveAs` | Map? | Map of fileId to storedAs filename |
|
|
244
|
+
|
|
245
|
+
### Methods
|
|
246
|
+
|
|
247
|
+
| Method | Returns | Description |
|
|
248
|
+
|--------|---------|-------------|
|
|
249
|
+
| `renderLayout(xlfXml, layoutId)` | Promise | Parse, build, and start layout. Instant swap if preloaded. |
|
|
250
|
+
| `preloadLayout(xlfXml, layoutId)` | Promise<bool> | Pre-build layout as hidden warm entry |
|
|
251
|
+
| `hasPreloadedLayout(layoutId)` | boolean | Check if layout is in preload pool |
|
|
252
|
+
| `renderOverlay(xlfXml, layoutId, priority)` | Promise | Render overlay on top |
|
|
253
|
+
| `stopCurrentLayout()` | void | Stop main layout |
|
|
254
|
+
| `stopOverlay(layoutId)` | void | Stop and remove overlay |
|
|
255
|
+
| `stopAllOverlays()` | void | Stop all active overlays |
|
|
256
|
+
| `navigateToWidget(widgetId)` | void | Jump to specific widget |
|
|
257
|
+
| `nextWidget(regionId?)` | void | Advance to next widget |
|
|
258
|
+
| `previousWidget(regionId?)` | void | Go back to previous widget |
|
|
259
|
+
| `pause()` | void | Pause media and widget cycling |
|
|
260
|
+
| `resume()` | void | Resume playback |
|
|
261
|
+
| `cleanup()` | void | Stop all, clear pool, revoke blob URLs |
|
|
48
262
|
|
|
49
263
|
## Dependencies
|
|
50
264
|
|
|
51
|
-
- `@xiboplayer/utils`
|
|
52
|
-
- `pdfjs-dist`
|
|
265
|
+
- `@xiboplayer/utils` -- logger, events
|
|
266
|
+
- `pdfjs-dist` -- PDF rendering (dynamic import)
|
|
267
|
+
- `hls.js` -- HLS streaming (dynamic import)
|
|
53
268
|
|
|
54
269
|
---
|
|
55
270
|
|
|
@@ -464,7 +464,7 @@ XLF → Parse → Pre-create Elements → Toggle Visibility → Transitions
|
|
|
464
464
|
|
|
465
465
|
**RendererLite successfully implements the Arexibo pattern** and adds significant performance improvements through parallelization. The implementation is production-ready with minor improvements needed for blob URL lifecycle management.
|
|
466
466
|
|
|
467
|
-
**Feature Parity**:
|
|
467
|
+
**Feature Parity**: 100% (all widget types, transitions, interactive control, shell commands)
|
|
468
468
|
**Performance**: Exceeds XLR and Arexibo benchmarks
|
|
469
469
|
**Memory**: Stable with Arexibo pattern correctly implemented
|
|
470
470
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"nanoevents": "^9.1.0",
|
|
15
15
|
"pdfjs-dist": "^4.10.38",
|
|
16
|
-
"@xiboplayer/
|
|
17
|
-
"@xiboplayer/
|
|
18
|
-
"@xiboplayer/schedule": "0.6.
|
|
16
|
+
"@xiboplayer/cache": "0.6.4",
|
|
17
|
+
"@xiboplayer/utils": "0.6.4",
|
|
18
|
+
"@xiboplayer/schedule": "0.6.4"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"vitest": "^2.0.0",
|
package/src/renderer-lite.js
CHANGED
|
@@ -2151,7 +2151,7 @@ export class RendererLite {
|
|
|
2151
2151
|
// NOT update the current layout's duration with a different layout's video.
|
|
2152
2152
|
const createdForLayoutId = this.currentLayoutId;
|
|
2153
2153
|
const onLoadedMetadata = () => {
|
|
2154
|
-
const videoDuration =
|
|
2154
|
+
const videoDuration = video.duration;
|
|
2155
2155
|
this.log.info(`Video ${storedAs} duration detected: ${videoDuration}s`);
|
|
2156
2156
|
|
|
2157
2157
|
if (widget.duration === 0 || widget.useDuration === 0) {
|