@xiboplayer/schedule 0.1.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.
@@ -0,0 +1,425 @@
1
+ # Schedule-Advanced Package
2
+
3
+ Advanced scheduling features for Xibo Player including interrupt layouts (shareOfVoice) and overlays.
4
+
5
+ ## Features
6
+
7
+ ### Interrupt Layouts (Share of Voice) ✅
8
+
9
+ Fully implemented and tested. Layouts with `shareOfVoice > 0` play for a percentage of each hour.
10
+
11
+ **Use cases:**
12
+ - Advertising: Display ads for X% of each hour
13
+ - Emergency alerts: Show warnings during incidents
14
+ - Promotional content: Feature sales during campaigns
15
+ - Time-based content mixing: Combine regular + special content
16
+
17
+ **Example:**
18
+ ```javascript
19
+ {
20
+ id: 1,
21
+ file: 100,
22
+ duration: 30,
23
+ shareOfVoice: 10, // 10% of hour = 360 seconds
24
+ priority: 10
25
+ }
26
+ ```
27
+
28
+ ### Overlay Layouts 🚧
29
+
30
+ Partially implemented (scheduling only, rendering not yet complete).
31
+
32
+ **Use cases:**
33
+ - Emergency alerts on top of content
34
+ - Weather overlays
35
+ - Social media feeds
36
+ - Ticker tapes
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm install @xiboplayer/schedule-advanced
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```javascript
47
+ import { ScheduleManager } from '@xiboplayer/schedule';
48
+ import { InterruptScheduler } from '@xiboplayer/schedule-advanced';
49
+
50
+ // Create interrupt scheduler
51
+ const interruptScheduler = new InterruptScheduler();
52
+
53
+ // Pass to schedule manager
54
+ const scheduleManager = new ScheduleManager({
55
+ interruptScheduler
56
+ });
57
+
58
+ // Set schedule data
59
+ scheduleManager.setSchedule(scheduleData);
60
+
61
+ // Get layouts (automatically processes interrupts)
62
+ const layouts = scheduleManager.getCurrentLayouts();
63
+ ```
64
+
65
+ ## Documentation
66
+
67
+ - **[Integration Guide](./INTEGRATION.md)** - Complete integration examples
68
+ - **[API Reference](#api-reference)** - Detailed API documentation
69
+ - **[Algorithm Details](#algorithm)** - How shareOfVoice works
70
+ - **[Testing](#testing)** - Running and writing tests
71
+
72
+ ## How ShareOfVoice Works
73
+
74
+ ### Algorithm Overview
75
+
76
+ 1. **Separation**: Separate interrupts (shareOfVoice > 0) from normal layouts
77
+ 2. **Calculation**: For each interrupt, calculate required plays:
78
+ - Required seconds = `(shareOfVoice / 100) * 3600`
79
+ - Required plays = `required_seconds / layout_duration`
80
+ 3. **Filling**: Fill remaining time with normal layouts
81
+ 4. **Interleaving**: Distribute interrupts evenly throughout the hour
82
+
83
+ ### Example
84
+
85
+ **Input:**
86
+ ```javascript
87
+ [
88
+ { file: 10, duration: 60, shareOfVoice: 10 }, // Interrupt
89
+ { file: 20, duration: 60, shareOfVoice: 0 } // Normal
90
+ ]
91
+ ```
92
+
93
+ **Calculation:**
94
+ - Interrupt requirement: 10% of 3600s = 360s
95
+ - Interrupt plays: 360s / 60s = 6 plays
96
+ - Remaining time: 3600s - 360s = 3240s
97
+ - Normal plays: 3240s / 60s = 54 plays
98
+
99
+ **Output (60 layouts):**
100
+ ```
101
+ [20, 20, 20, 20, 20, 20, 20, 20, 10, // 9 layouts
102
+ 20, 20, 20, 20, 20, 20, 20, 20, 10, // +9
103
+ 20, 20, 20, 20, 20, 20, 20, 20, 10, // +9
104
+ ... // etc
105
+ 20, 20, 20, 20, 20, 20, 20, 20, 10] // Last 9
106
+ ```
107
+
108
+ Interrupts are evenly distributed (every 9 layouts).
109
+
110
+ ### Multiple Interrupts
111
+
112
+ When multiple interrupts exist, they all get their required time:
113
+
114
+ **Input:**
115
+ ```javascript
116
+ [
117
+ { file: 10, duration: 60, shareOfVoice: 25 }, // Interrupt 1
118
+ { file: 20, duration: 60, shareOfVoice: 25 }, // Interrupt 2
119
+ { file: 30, duration: 60, shareOfVoice: 0 } // Normal
120
+ ]
121
+ ```
122
+
123
+ **Calculation:**
124
+ - Int1 requirement: 25% = 900s (15 plays)
125
+ - Int2 requirement: 25% = 900s (15 plays)
126
+ - Remaining: 1800s (30 normal plays)
127
+ - Total: 60 layouts
128
+
129
+ **Output:**
130
+ Interrupts and normal layouts interleaved evenly.
131
+
132
+ ## API Reference
133
+
134
+ ### InterruptScheduler
135
+
136
+ #### Constructor
137
+ ```javascript
138
+ const scheduler = new InterruptScheduler();
139
+ ```
140
+
141
+ #### Methods
142
+
143
+ ##### `isInterrupt(layout)`
144
+ Check if layout has shareOfVoice > 0.
145
+
146
+ **Parameters:**
147
+ - `layout` (Object) - Layout object with shareOfVoice property
148
+
149
+ **Returns:** `boolean`
150
+
151
+ **Example:**
152
+ ```javascript
153
+ const isInt = scheduler.isInterrupt({ shareOfVoice: 10 });
154
+ // true
155
+ ```
156
+
157
+ ##### `processInterrupts(normalLayouts, interruptLayouts)`
158
+ Process interrupts and combine with normal layouts for one hour.
159
+
160
+ **Parameters:**
161
+ - `normalLayouts` (Array) - Normal layouts
162
+ - `interruptLayouts` (Array) - Interrupt layouts with shareOfVoice
163
+
164
+ **Returns:** `Array` - Combined layout loop for the hour
165
+
166
+ **Example:**
167
+ ```javascript
168
+ const normal = [{ file: 10, duration: 60 }];
169
+ const interrupts = [{ file: 20, duration: 60, shareOfVoice: 10 }];
170
+ const loop = scheduler.processInterrupts(normal, interrupts);
171
+ // Returns 60-layout array with 6 interrupts, 54 normal
172
+ ```
173
+
174
+ ##### `separateLayouts(layouts)`
175
+ Separate layouts into normal and interrupt arrays.
176
+
177
+ **Parameters:**
178
+ - `layouts` (Array) - Mixed layouts
179
+
180
+ **Returns:** `{ normalLayouts, interruptLayouts }`
181
+
182
+ **Example:**
183
+ ```javascript
184
+ const { normalLayouts, interruptLayouts } = scheduler.separateLayouts(allLayouts);
185
+ ```
186
+
187
+ ##### `getRequiredSeconds(layout)`
188
+ Calculate required seconds per hour for an interrupt.
189
+
190
+ **Parameters:**
191
+ - `layout` (Object) - Layout with shareOfVoice
192
+
193
+ **Returns:** `number` - Required seconds
194
+
195
+ **Example:**
196
+ ```javascript
197
+ const required = scheduler.getRequiredSeconds({ shareOfVoice: 25 });
198
+ // 900 (25% of 3600)
199
+ ```
200
+
201
+ ##### `resetCommittedDurations()`
202
+ Reset committed duration tracking. Call this every hour to reset counters.
203
+
204
+ **Example:**
205
+ ```javascript
206
+ // Every hour
207
+ setInterval(() => {
208
+ scheduler.resetCommittedDurations();
209
+ }, 3600000);
210
+ ```
211
+
212
+ ### OverlayScheduler
213
+
214
+ #### Constructor
215
+ ```javascript
216
+ const overlayScheduler = new OverlayScheduler();
217
+ ```
218
+
219
+ #### Methods
220
+
221
+ ##### `setOverlays(overlays)`
222
+ Update overlays from schedule data.
223
+
224
+ **Parameters:**
225
+ - `overlays` (Array) - Overlay objects from XMDS
226
+
227
+ ##### `getCurrentOverlays()`
228
+ Get currently active overlays.
229
+
230
+ **Returns:** `Array` - Active overlays sorted by priority
231
+
232
+ ##### `isTimeActive(overlay, now)`
233
+ Check if overlay is within its time window.
234
+
235
+ **Parameters:**
236
+ - `overlay` (Object) - Overlay object
237
+ - `now` (Date) - Current time
238
+
239
+ **Returns:** `boolean`
240
+
241
+ ## Testing
242
+
243
+ ### Run Tests
244
+
245
+ ```bash
246
+ # Run all tests
247
+ npm test
248
+
249
+ # Run specific test file
250
+ npm test src/interrupts.test.js
251
+ npm test src/integration.test.js
252
+
253
+ # Watch mode
254
+ npm run test:watch
255
+
256
+ # Coverage report
257
+ npm run test:coverage
258
+ ```
259
+
260
+ ### Test Coverage
261
+
262
+ **Current coverage:**
263
+ - 47 total tests
264
+ - 33 interrupt scheduler tests
265
+ - 14 integration tests
266
+ - All edge cases covered
267
+
268
+ **Test categories:**
269
+ - Basic functionality (isInterrupt, getRequiredSeconds, etc.)
270
+ - ShareOfVoice calculation (10%, 50%, 100%)
271
+ - Multiple interrupts
272
+ - Interleaving algorithm
273
+ - Edge cases (no normal layouts, >100% total, etc.)
274
+ - Integration with ScheduleManager
275
+ - Priority handling
276
+ - Campaign support
277
+ - Real-world scenarios
278
+
279
+ ### Writing Tests
280
+
281
+ Example test:
282
+
283
+ ```javascript
284
+ import { InterruptScheduler } from './interrupts.js';
285
+
286
+ it('should handle 10% shareOfVoice', () => {
287
+ const scheduler = new InterruptScheduler();
288
+ const normal = [{ file: 10, duration: 60 }];
289
+ const interrupts = [{ file: 20, duration: 60, shareOfVoice: 10 }];
290
+
291
+ const result = scheduler.processInterrupts(normal, interrupts);
292
+
293
+ const interruptCount = result.filter(l => l.file === 20).length;
294
+ expect(interruptCount).toBe(6); // 360s / 60s
295
+ });
296
+ ```
297
+
298
+ ## Performance
299
+
300
+ ### Benchmarks
301
+
302
+ - **Processing time**: < 10ms for 100 layouts
303
+ - **Memory usage**: O(n) where n is number of layouts
304
+ - **Time complexity**: O(n) for separation and interleaving
305
+
306
+ ### Optimization Tips
307
+
308
+ 1. **Reuse scheduler instance** - Create once, reuse multiple times
309
+ 2. **Reset hourly** - Call `resetCommittedDurations()` every hour
310
+ 3. **Batch updates** - Process all schedule changes at once
311
+
312
+ ## Upstream Reference
313
+
314
+ Based on upstream Xibo Electron player implementation:
315
+
316
+ **File:** `electron-player/src/main/common/scheduleManager.ts`
317
+ **Lines:** 181-321
318
+ **Version:** Latest as of 2026-02-10
319
+
320
+ **Differences from upstream:**
321
+ - JavaScript (not TypeScript)
322
+ - Modular design (separate package)
323
+ - Enhanced logging via @xiboplayer/utils
324
+ - More comprehensive test coverage
325
+ - Cleaner API surface
326
+
327
+ ## Debugging
328
+
329
+ Enable debug logging:
330
+
331
+ ```javascript
332
+ // Browser console
333
+ localStorage.setItem('xibo:log:level', 'debug');
334
+
335
+ // Programmatically
336
+ import { config } from '@xiboplayer/utils';
337
+ config.set('logLevel', 'debug');
338
+ ```
339
+
340
+ **Debug output:**
341
+ ```
342
+ [schedule-advanced:interrupts] Processing 2 interrupt layouts with 3 normal layouts
343
+ [schedule-advanced:interrupts] DEBUG: Resolved 15 interrupt plays (900s total)
344
+ [schedule-advanced:interrupts] DEBUG: Resolved 45 normal plays (2700s target)
345
+ [schedule-advanced:interrupts] DEBUG: Interleaving: pickCount=45, normalPick=1, interruptPick=3
346
+ [schedule-advanced:interrupts] DEBUG: Interleaved 60 layouts, total duration: 3600s
347
+ [schedule-advanced:interrupts] Final loop: 60 layouts (45 normal + 15 interrupts)
348
+ ```
349
+
350
+ ## Troubleshooting
351
+
352
+ ### Issue: Interrupts not playing
353
+
354
+ **Check:**
355
+ 1. InterruptScheduler passed to ScheduleManager?
356
+ 2. Layouts have shareOfVoice > 0?
357
+ 3. Priority filtering removing interrupts?
358
+
359
+ **Debug:**
360
+ ```javascript
361
+ const { normalLayouts, interruptLayouts } = scheduler.separateLayouts(allLayouts);
362
+ console.log('Interrupts:', interruptLayouts);
363
+ ```
364
+
365
+ ### Issue: Wrong number of plays
366
+
367
+ **Check:**
368
+ 1. Layout duration vs shareOfVoice percentage
369
+ 2. Rounding (algorithm may overshoot slightly)
370
+ 3. Multiple interrupts summing > 100%
371
+
372
+ **Debug:**
373
+ ```javascript
374
+ const required = scheduler.getRequiredSeconds(layout);
375
+ const plays = required / layout.duration;
376
+ console.log('Required plays:', plays);
377
+ ```
378
+
379
+ ### Issue: Performance problems
380
+
381
+ **Check:**
382
+ 1. Too many layouts (>1000)?
383
+ 2. Creating new scheduler instance every time?
384
+ 3. Not resetting committed durations?
385
+
386
+ **Solution:**
387
+ ```javascript
388
+ // Create once
389
+ const scheduler = new InterruptScheduler();
390
+
391
+ // Reset hourly
392
+ setInterval(() => scheduler.resetCommittedDurations(), 3600000);
393
+ ```
394
+
395
+ ## Contributing
396
+
397
+ See main repository [CONTRIBUTING.md](../../../CONTRIBUTING.md).
398
+
399
+ **Areas for contribution:**
400
+ - Overlay rendering implementation
401
+ - Criteria-based scheduling
402
+ - Geo-location awareness
403
+ - Additional test coverage
404
+ - Performance optimizations
405
+
406
+ ## Support
407
+
408
+ - **Issues**: https://github.com/xibo/xibo-players/issues
409
+ - **Discussions**: https://github.com/xibo/xibo-players/discussions
410
+ - **Documentation**: https://xibo.org.uk/docs/
411
+
412
+ ## Changelog
413
+
414
+ ### 0.9.0 (2026-02-10)
415
+
416
+ - ✅ Initial release
417
+ - ✅ Full interrupt layout (shareOfVoice) implementation
418
+ - ✅ 47 comprehensive tests (all passing)
419
+ - ✅ Integration with @xiboplayer/schedule
420
+ - ✅ Based on upstream electron-player algorithm
421
+ - 🚧 Overlay layout scheduling (rendering not complete)
422
+
423
+ ## License
424
+
425
+ AGPL-3.0-or-later
@@ -0,0 +1,284 @@
1
+ # Schedule-Advanced Integration Guide
2
+
3
+ ## Overview
4
+
5
+ The `@xiboplayer/schedule-advanced` package provides advanced scheduling features including interrupt layouts (shareOfVoice) and overlays. It integrates with the base `@xiboplayer/schedule` package to extend its functionality.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @xiboplayer/schedule-advanced
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Basic Setup (without interrupts)
16
+
17
+ ```javascript
18
+ import { ScheduleManager } from '@xiboplayer/schedule';
19
+
20
+ const scheduleManager = new ScheduleManager();
21
+ scheduleManager.setSchedule(scheduleData);
22
+
23
+ const layouts = scheduleManager.getCurrentLayouts();
24
+ ```
25
+
26
+ ### With Interrupt Support
27
+
28
+ ```javascript
29
+ import { ScheduleManager } from '@xiboplayer/schedule';
30
+ import { InterruptScheduler } from '@xiboplayer/schedule-advanced';
31
+
32
+ // Create interrupt scheduler
33
+ const interruptScheduler = new InterruptScheduler();
34
+
35
+ // Pass to schedule manager
36
+ const scheduleManager = new ScheduleManager({
37
+ interruptScheduler
38
+ });
39
+
40
+ scheduleManager.setSchedule(scheduleData);
41
+
42
+ // Now getCurrentLayouts() will process interrupts automatically
43
+ const layouts = scheduleManager.getCurrentLayouts();
44
+ ```
45
+
46
+ ## Interrupt Layouts (Share of Voice)
47
+
48
+ ### What are Interrupts?
49
+
50
+ Interrupt layouts are layouts with a `shareOfVoice` property > 0. They must play for a specific percentage of each hour, interleaved with normal layouts.
51
+
52
+ **Example use cases:**
53
+ - Advertising: Display ads for 10% of each hour
54
+ - Emergency alerts: Show warnings for 25% of hour during emergencies
55
+ - Promotional content: Feature special offers for 15% of hour during sales
56
+
57
+ ### How it Works
58
+
59
+ 1. **Separation**: Layouts are separated into normal and interrupt arrays
60
+ 2. **Calculation**: For each interrupt, calculate required plays based on shareOfVoice percentage
61
+ 3. **Filling**: Fill remaining time with normal layouts
62
+ 4. **Interleaving**: Distribute interrupts evenly throughout the hour
63
+
64
+ ### Example Schedule Data
65
+
66
+ ```javascript
67
+ const scheduleData = {
68
+ layouts: [
69
+ {
70
+ id: 1,
71
+ file: 10,
72
+ duration: 60,
73
+ shareOfVoice: 10, // 10% of hour = 360 seconds
74
+ priority: 10,
75
+ fromdt: '2026-01-01 00:00:00',
76
+ todt: '2027-01-01 00:00:00'
77
+ },
78
+ {
79
+ id: 2,
80
+ file: 20,
81
+ duration: 120,
82
+ shareOfVoice: 0, // Normal layout
83
+ priority: 10,
84
+ fromdt: '2026-01-01 00:00:00',
85
+ todt: '2027-01-01 00:00:00'
86
+ }
87
+ ]
88
+ };
89
+ ```
90
+
91
+ **Result**: Layout 10 plays 6 times (360s), Layout 20 fills remaining 3240s (27 plays).
92
+
93
+ ### ShareOfVoice Calculation
94
+
95
+ - `shareOfVoice` is a percentage (0-100)
96
+ - Required time = `(shareOfVoice / 100) * 3600` seconds
97
+ - Required plays = `required_time / layout_duration`
98
+
99
+ **Examples:**
100
+ - 10% shareOfVoice = 360 seconds per hour
101
+ - 50% shareOfVoice = 1800 seconds per hour
102
+ - 100% shareOfVoice = 3600 seconds per hour (entire hour)
103
+
104
+ ### Multiple Interrupts
105
+
106
+ When multiple interrupts exist, they are all satisfied:
107
+
108
+ ```javascript
109
+ const scheduleData = {
110
+ layouts: [
111
+ { id: 1, file: 10, duration: 30, shareOfVoice: 15 }, // 15% = 540s
112
+ { id: 2, file: 20, duration: 30, shareOfVoice: 10 }, // 10% = 360s
113
+ { id: 3, file: 30, duration: 60, shareOfVoice: 0 } // Normal, fills remaining 2700s
114
+ ]
115
+ };
116
+ ```
117
+
118
+ **Result**:
119
+ - Layout 10: 18 plays (540s)
120
+ - Layout 20: 12 plays (360s)
121
+ - Layout 30: 45 plays (2700s)
122
+ - Total: 75 layouts, evenly interleaved
123
+
124
+ ### Edge Cases
125
+
126
+ #### All Interrupts (>= 100% total shareOfVoice)
127
+
128
+ If interrupts consume the entire hour (total shareOfVoice >= 100%), no normal layouts play:
129
+
130
+ ```javascript
131
+ const scheduleData = {
132
+ layouts: [
133
+ { id: 1, file: 10, duration: 60, shareOfVoice: 60 },
134
+ { id: 2, file: 20, duration: 60, shareOfVoice: 60 }
135
+ ]
136
+ };
137
+ // Result: Only interrupts play (no room for normal layouts)
138
+ ```
139
+
140
+ #### No Normal Layouts
141
+
142
+ If only interrupts exist, they fill the entire hour:
143
+
144
+ ```javascript
145
+ const scheduleData = {
146
+ layouts: [
147
+ { id: 1, file: 10, duration: 60, shareOfVoice: 25 }
148
+ ]
149
+ };
150
+ // Result: Layout repeats to fill entire hour
151
+ ```
152
+
153
+ ## Overlay Layouts
154
+
155
+ Overlays are layouts that display **on top** of main layouts (not yet fully implemented).
156
+
157
+ ```javascript
158
+ import { OverlayScheduler } from '@xiboplayer/schedule-advanced';
159
+
160
+ const overlayScheduler = new OverlayScheduler();
161
+ overlayScheduler.setOverlays(overlayData);
162
+
163
+ const activeOverlays = overlayScheduler.getCurrentOverlays();
164
+ ```
165
+
166
+ **Note**: Overlay rendering is not yet implemented in the player. This provides the scheduling logic only.
167
+
168
+ ## Testing
169
+
170
+ Run the comprehensive test suite:
171
+
172
+ ```bash
173
+ cd packages/schedule-advanced
174
+ npm test
175
+ ```
176
+
177
+ **Test coverage:**
178
+ - 33 tests for InterruptScheduler
179
+ - Tests cover all edge cases, multiple interrupts, interleaving, and real-world scenarios
180
+
181
+ ## Debugging
182
+
183
+ Enable debug logging:
184
+
185
+ ```javascript
186
+ // In browser console or Node.js
187
+ localStorage.setItem('xibo:log:level', 'debug');
188
+
189
+ // Or set programmatically
190
+ import { config } from '@xiboplayer/utils';
191
+ config.set('logLevel', 'debug');
192
+ ```
193
+
194
+ **Debug output example:**
195
+ ```
196
+ [schedule-advanced:interrupts] Processing 2 interrupt layouts with 3 normal layouts
197
+ [schedule-advanced:interrupts] DEBUG: Resolved 15 interrupt plays (900s total)
198
+ [schedule-advanced:interrupts] DEBUG: Resolved 45 normal plays (2700s target)
199
+ [schedule-advanced:interrupts] DEBUG: Interleaving: pickCount=45, normalPick=1, interruptPick=3
200
+ [schedule-advanced:interrupts] DEBUG: Interleaved 60 layouts, total duration: 3600s
201
+ [schedule-advanced:interrupts] Final loop: 60 layouts (45 normal + 15 interrupts)
202
+ ```
203
+
204
+ ## API Reference
205
+
206
+ ### InterruptScheduler
207
+
208
+ #### Methods
209
+
210
+ ##### `isInterrupt(layout)`
211
+ Check if a layout is an interrupt (shareOfVoice > 0).
212
+
213
+ **Returns**: `boolean`
214
+
215
+ ##### `processInterrupts(normalLayouts, interruptLayouts)`
216
+ Process interrupts and combine with normal layouts.
217
+
218
+ **Returns**: `Array` - Combined layout loop for the hour
219
+
220
+ ##### `separateLayouts(layouts)`
221
+ Separate layouts into normal and interrupt arrays.
222
+
223
+ **Returns**: `{ normalLayouts, interruptLayouts }`
224
+
225
+ ##### `resetCommittedDurations()`
226
+ Reset interrupt duration tracking (call every hour).
227
+
228
+ ##### `getRequiredSeconds(layout)`
229
+ Calculate required seconds per hour for an interrupt.
230
+
231
+ **Returns**: `number`
232
+
233
+ ### OverlayScheduler
234
+
235
+ #### Methods
236
+
237
+ ##### `setOverlays(overlays)`
238
+ Update overlays from schedule data.
239
+
240
+ ##### `getCurrentOverlays()`
241
+ Get currently active overlays.
242
+
243
+ **Returns**: `Array` - Active overlay objects sorted by priority
244
+
245
+ ##### `isTimeActive(overlay, now)`
246
+ Check if overlay is within its time window.
247
+
248
+ **Returns**: `boolean`
249
+
250
+ ## Performance
251
+
252
+ The interrupt algorithm is highly efficient:
253
+
254
+ - **Time complexity**: O(n) where n is number of layouts
255
+ - **Space complexity**: O(n) for result array
256
+ - **Typical processing time**: < 10ms for 100 layouts
257
+
258
+ ## Compatibility
259
+
260
+ - **Node.js**: >= 18.0.0
261
+ - **Browsers**: All modern browsers (ES2020+)
262
+ - **Xibo CMS**: Compatible with Xibo CMS 3.x and 4.x schedules
263
+
264
+ ## Upstream Reference
265
+
266
+ This implementation is based on the upstream Xibo Electron player:
267
+
268
+ **Source**: `upstream_players/electron-player/src/main/common/scheduleManager.ts` (lines 181-321)
269
+
270
+ **Differences**:
271
+ - Adapted to JavaScript (from TypeScript)
272
+ - Uses modular logger from @xiboplayer/utils
273
+ - Simplified API for easier integration
274
+ - More comprehensive test coverage
275
+
276
+ ## Support
277
+
278
+ For issues or questions:
279
+ - GitHub Issues: https://github.com/xibo/xibo-players/issues
280
+ - Documentation: /packages/schedule-advanced/docs/
281
+
282
+ ## License
283
+
284
+ AGPL-3.0-or-later