@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.
- package/docs/README.md +102 -0
- package/docs/XIBO_CAMPAIGNS_AND_PRIORITY.md +600 -0
- package/docs/advanced-features.md +425 -0
- package/docs/integration.md +284 -0
- package/docs/interrupts-implementation.md +357 -0
- package/package.json +41 -0
- package/src/criteria.js +135 -0
- package/src/criteria.test.js +376 -0
- package/src/index.js +20 -0
- package/src/integration.test.js +351 -0
- package/src/interrupts.js +298 -0
- package/src/interrupts.test.js +482 -0
- package/src/overlays.js +174 -0
- package/src/schedule.dayparting.test.js +390 -0
- package/src/schedule.js +509 -0
- package/src/schedule.test.js +505 -0
- package/vitest.config.js +8 -0
|
@@ -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
|