@xiboplayer/renderer 0.6.3 → 0.6.5

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 CHANGED
@@ -1,19 +1,53 @@
1
1
  # @xiboplayer/renderer
2
2
 
3
- **XLF layout rendering engine for Xibo digital signage.**
3
+ **Fast, memory-efficient XLF layout rendering engine for digital signage.**
4
4
 
5
5
  ## Overview
6
6
 
7
- RendererLite parses Xibo Layout Format (XLF) files and builds a live DOM with:
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** video (MP4/HLS), images, PDF (via PDF.js), text/ticker, web pages, clock, calendar, weather
10
- - **Transitions** fade and fly (8-direction compass) via Web Animations API
11
- - **Interactive actions** touch/click and keyboard triggers for widget navigation, layout jumps, and commands
12
- - **Layout preloading** 2-layout pool pre-builds upcoming layouts at 75% of current duration for zero-gap transitions
13
- - **Proportional scaling** ResizeObserver-based scaling to fit any screen resolution
14
- - **Overlay support** multiple simultaneous overlay layouts with independent z-index (1000+)
15
- - **Absolute widget positioning** widget elements use `position: absolute` within regions to layer correctly in multi-widget regions
16
- - **Animation cleanup** `fill: forwards` animations cancelled between widgets to prevent stale visual state (e.g. video hidden after PDF)
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
- container: document.getElementById('player'),
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
- // Render a layout from parsed XLF
34
- await renderer.renderLayout(xlf, { mediaBaseUrl: '/cache/' });
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
- | Widget | Implementation |
40
- |--------|---------------|
41
- | Video | `<video>` with native HLS (Safari) + hls.js fallback, pause-on-last-frame |
42
- | Image | `<img>` with CMS scaleType mapping (center->contain, stretch->fill, fit->cover), blob URL from cache |
43
- | PDF | PDF.js canvas rendering (dynamically imported) |
44
- | Text / Ticker | iframe with CMS-rendered HTML via GetResource |
45
- | Web page | bare `<iframe src="...">` |
46
- | Clock, Calendar, Weather | iframe via GetResource (server-rendered) |
47
- | All other CMS widgets | Generic iframe via GetResource |
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` logger, events
52
- - `pdfjs-dist` PDF rendering
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**: ~98% (missing only widget action event propagation)
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",
3
+ "version": "0.6.5",
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/utils": "0.6.3",
17
- "@xiboplayer/cache": "0.6.3",
18
- "@xiboplayer/schedule": "0.6.3"
16
+ "@xiboplayer/cache": "0.6.5",
17
+ "@xiboplayer/schedule": "0.6.5",
18
+ "@xiboplayer/utils": "0.6.5"
19
19
  },
20
20
  "devDependencies": {
21
21
  "vitest": "^2.0.0",
@@ -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 = Math.floor(video.duration);
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) {