@xiboplayer/schedule 0.6.13 → 0.7.1
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 +2 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -1
- package/src/overlays.test.js +241 -0
- package/src/schedule.js +27 -2
- package/src/timeline.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/schedule",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Complete scheduling solution: campaigns, dayparting, interrupts, and overlays",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"./overlays": "./src/overlays.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@xiboplayer/utils": "0.
|
|
15
|
+
"@xiboplayer/utils": "0.7.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0"
|
package/src/index.d.ts
CHANGED
|
@@ -17,5 +17,6 @@ export class ScheduleManager {
|
|
|
17
17
|
export const scheduleManager: ScheduleManager;
|
|
18
18
|
|
|
19
19
|
export function calculateTimeline(queue: Array<{layoutId: string, duration: number}>, queuePosition: number, options?: any): any[];
|
|
20
|
+
export function parseLayoutFile(f: string | number): number;
|
|
20
21
|
export function parseLayoutDuration(xlf: string, videoDurations?: Map<string, number> | null): { duration: number; isDynamic: boolean };
|
|
21
22
|
export function buildScheduleQueue(schedule: any, durations: Map<string, number>): any[];
|
package/src/index.js
CHANGED
|
@@ -27,4 +27,4 @@ export { OverlayScheduler } from './overlays.js';
|
|
|
27
27
|
* Offline timeline calculator — duration parser + timeline simulator
|
|
28
28
|
* @module @xiboplayer/schedule/timeline
|
|
29
29
|
*/
|
|
30
|
-
export { calculateTimeline, parseLayoutDuration, buildScheduleQueue } from './timeline.js';
|
|
30
|
+
export { calculateTimeline, parseLayoutDuration, parseLayoutFile, buildScheduleQueue } from './timeline.js';
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { OverlayScheduler } from './overlays.js';
|
|
5
|
+
|
|
6
|
+
function makeOverlay(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
file: 100,
|
|
9
|
+
fromdt: '2026-01-01T00:00:00',
|
|
10
|
+
todt: '2026-12-31T23:59:59',
|
|
11
|
+
priority: 0,
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('OverlayScheduler', () => {
|
|
17
|
+
let scheduler;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
scheduler = new OverlayScheduler();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ── Constructor ────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe('constructor', () => {
|
|
26
|
+
it('initializes with empty overlays', () => {
|
|
27
|
+
expect(scheduler.overlays).toEqual([]);
|
|
28
|
+
expect(scheduler.displayProperties).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ── setOverlays ───────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe('setOverlays', () => {
|
|
35
|
+
it('stores overlay list', () => {
|
|
36
|
+
scheduler.setOverlays([makeOverlay()]);
|
|
37
|
+
expect(scheduler.overlays).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('handles null/undefined', () => {
|
|
41
|
+
scheduler.setOverlays(null);
|
|
42
|
+
expect(scheduler.overlays).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('replaces previous overlays', () => {
|
|
46
|
+
scheduler.setOverlays([makeOverlay({ file: 1 })]);
|
|
47
|
+
scheduler.setOverlays([makeOverlay({ file: 2 }), makeOverlay({ file: 3 })]);
|
|
48
|
+
expect(scheduler.overlays).toHaveLength(2);
|
|
49
|
+
expect(scheduler.overlays[0].file).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── isTimeActive ──────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
describe('isTimeActive', () => {
|
|
56
|
+
it('returns true when now is within time window', () => {
|
|
57
|
+
const overlay = makeOverlay({
|
|
58
|
+
fromdt: '2026-03-01T00:00:00',
|
|
59
|
+
todt: '2026-03-31T23:59:59',
|
|
60
|
+
});
|
|
61
|
+
const now = new Date('2026-03-15T12:00:00');
|
|
62
|
+
expect(scheduler.isTimeActive(overlay, now)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns false when now is before fromdt', () => {
|
|
66
|
+
const overlay = makeOverlay({ fromdt: '2026-06-01T00:00:00' });
|
|
67
|
+
const now = new Date('2026-05-01T12:00:00');
|
|
68
|
+
expect(scheduler.isTimeActive(overlay, now)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns false when now is after todt', () => {
|
|
72
|
+
const overlay = makeOverlay({ todt: '2026-01-01T00:00:00' });
|
|
73
|
+
const now = new Date('2026-06-01T12:00:00');
|
|
74
|
+
expect(scheduler.isTimeActive(overlay, now)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns true when no time bounds set', () => {
|
|
78
|
+
const overlay = { file: 1 }; // no fromdt/todt
|
|
79
|
+
expect(scheduler.isTimeActive(overlay, new Date())).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('supports toDt (camelCase) alias', () => {
|
|
83
|
+
const overlay = { file: 1, fromDt: '2026-01-01', toDt: '2026-12-31' };
|
|
84
|
+
expect(scheduler.isTimeActive(overlay, new Date('2026-06-15'))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── getCurrentOverlays ────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe('getCurrentOverlays', () => {
|
|
91
|
+
it('returns empty array when no overlays set', () => {
|
|
92
|
+
expect(scheduler.getCurrentOverlays()).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns only overlays within time window', () => {
|
|
96
|
+
scheduler.setOverlays([
|
|
97
|
+
makeOverlay({ file: 1, fromdt: '2026-01-01', todt: '2026-06-30' }),
|
|
98
|
+
makeOverlay({ file: 2, fromdt: '2026-07-01', todt: '2026-12-31' }),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// Mock Date to be in Q1
|
|
102
|
+
const origDate = global.Date;
|
|
103
|
+
global.Date = class extends origDate {
|
|
104
|
+
constructor(...args) {
|
|
105
|
+
if (args.length === 0) return new origDate('2026-03-15T12:00:00');
|
|
106
|
+
return new origDate(...args);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const active = scheduler.getCurrentOverlays();
|
|
111
|
+
expect(active).toHaveLength(1);
|
|
112
|
+
expect(active[0].file).toBe(1);
|
|
113
|
+
|
|
114
|
+
global.Date = origDate;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('sorts by priority descending', () => {
|
|
118
|
+
scheduler.setOverlays([
|
|
119
|
+
makeOverlay({ file: 1, priority: 5 }),
|
|
120
|
+
makeOverlay({ file: 2, priority: 10 }),
|
|
121
|
+
makeOverlay({ file: 3, priority: 1 }),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
// All overlays are within 2026 time window
|
|
125
|
+
const origDate = global.Date;
|
|
126
|
+
global.Date = class extends origDate {
|
|
127
|
+
constructor(...args) {
|
|
128
|
+
if (args.length === 0) return new origDate('2026-06-15T12:00:00');
|
|
129
|
+
return new origDate(...args);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const active = scheduler.getCurrentOverlays();
|
|
134
|
+
expect(active[0].priority).toBe(10);
|
|
135
|
+
expect(active[1].priority).toBe(5);
|
|
136
|
+
expect(active[2].priority).toBe(1);
|
|
137
|
+
|
|
138
|
+
global.Date = origDate;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('defaults priority to 0 when not set', () => {
|
|
142
|
+
scheduler.setOverlays([
|
|
143
|
+
makeOverlay({ file: 1 }), // no priority
|
|
144
|
+
makeOverlay({ file: 2, priority: 5 }),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const origDate = global.Date;
|
|
148
|
+
global.Date = class extends origDate {
|
|
149
|
+
constructor(...args) {
|
|
150
|
+
if (args.length === 0) return new origDate('2026-06-15T12:00:00');
|
|
151
|
+
return new origDate(...args);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const active = scheduler.getCurrentOverlays();
|
|
156
|
+
expect(active[0].file).toBe(2); // priority 5 first
|
|
157
|
+
expect(active[1].file).toBe(1); // priority 0
|
|
158
|
+
|
|
159
|
+
global.Date = origDate;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('filters by geo-fence when isGeoAware', () => {
|
|
163
|
+
const mockScheduleManager = {
|
|
164
|
+
isWithinGeoFence: () => false,
|
|
165
|
+
};
|
|
166
|
+
scheduler.setScheduleManager(mockScheduleManager);
|
|
167
|
+
|
|
168
|
+
scheduler.setOverlays([
|
|
169
|
+
makeOverlay({ file: 1, isGeoAware: true, geoLocation: { lat: 41, lng: 2 } }),
|
|
170
|
+
makeOverlay({ file: 2 }), // not geo-aware
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const origDate = global.Date;
|
|
174
|
+
global.Date = class extends origDate {
|
|
175
|
+
constructor(...args) {
|
|
176
|
+
if (args.length === 0) return new origDate('2026-06-15T12:00:00');
|
|
177
|
+
return new origDate(...args);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const active = scheduler.getCurrentOverlays();
|
|
182
|
+
expect(active).toHaveLength(1);
|
|
183
|
+
expect(active[0].file).toBe(2);
|
|
184
|
+
|
|
185
|
+
global.Date = origDate;
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ── getOverlayByFile ──────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
describe('getOverlayByFile', () => {
|
|
192
|
+
it('finds overlay by file ID', () => {
|
|
193
|
+
scheduler.setOverlays([makeOverlay({ file: 42 })]);
|
|
194
|
+
expect(scheduler.getOverlayByFile(42)).not.toBeNull();
|
|
195
|
+
expect(scheduler.getOverlayByFile(42).file).toBe(42);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns null for unknown file', () => {
|
|
199
|
+
expect(scheduler.getOverlayByFile(999)).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── shouldCheckOverlays ───────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
describe('shouldCheckOverlays', () => {
|
|
206
|
+
it('returns true when no last check', () => {
|
|
207
|
+
expect(scheduler.shouldCheckOverlays(null)).toBe(true);
|
|
208
|
+
expect(scheduler.shouldCheckOverlays(undefined)).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns true after 60+ seconds', () => {
|
|
212
|
+
const lastCheck = Date.now() - 61000;
|
|
213
|
+
expect(scheduler.shouldCheckOverlays(lastCheck)).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('returns false within 60 seconds', () => {
|
|
217
|
+
const lastCheck = Date.now() - 30000;
|
|
218
|
+
expect(scheduler.shouldCheckOverlays(lastCheck)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── clear / processOverlays ───────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe('clear', () => {
|
|
225
|
+
it('removes all overlays', () => {
|
|
226
|
+
scheduler.setOverlays([makeOverlay()]);
|
|
227
|
+
scheduler.clear();
|
|
228
|
+
expect(scheduler.overlays).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('processOverlays', () => {
|
|
233
|
+
it('sets overlays and returns layouts unchanged', () => {
|
|
234
|
+
const layouts = [{ id: 1 }, { id: 2 }];
|
|
235
|
+
const overlays = [makeOverlay({ file: 10 })];
|
|
236
|
+
const result = scheduler.processOverlays(layouts, overlays);
|
|
237
|
+
expect(result).toBe(layouts);
|
|
238
|
+
expect(scheduler.overlays).toHaveLength(1);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
package/src/schedule.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { createLogger } from '@xiboplayer/utils';
|
|
8
8
|
import { evaluateCriteria } from './criteria.js';
|
|
9
|
-
import { buildScheduleQueue } from './timeline.js';
|
|
9
|
+
import { buildScheduleQueue, parseLayoutFile } from './timeline.js';
|
|
10
10
|
|
|
11
11
|
const log = createLogger('Schedule');
|
|
12
12
|
|
|
@@ -63,7 +63,7 @@ export class ScheduleManager {
|
|
|
63
63
|
const globalDeps = this.schedule.dependants || [];
|
|
64
64
|
|
|
65
65
|
const addLayout = (layout) => {
|
|
66
|
-
const id =
|
|
66
|
+
const id = parseLayoutFile(layout.file || layout.id);
|
|
67
67
|
const deps = [...globalDeps, ...(layout.dependants || [])];
|
|
68
68
|
if (deps.length > 0) map.set(id, deps);
|
|
69
69
|
};
|
|
@@ -691,6 +691,31 @@ export class ScheduleManager {
|
|
|
691
691
|
* @param {Object} [options]
|
|
692
692
|
* @returns {{ layoutId: string, duration: number } | null}
|
|
693
693
|
*/
|
|
694
|
+
/**
|
|
695
|
+
* Get current queue position.
|
|
696
|
+
* @returns {number}
|
|
697
|
+
*/
|
|
698
|
+
getQueuePosition() {
|
|
699
|
+
return this._queuePosition;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Rewind the queue by N positions (wraps around).
|
|
704
|
+
* Used by advanceToPreviousLayout to go back in the schedule.
|
|
705
|
+
* @param {number} steps - Number of positions to rewind
|
|
706
|
+
* @param {Map<string, number>} durations
|
|
707
|
+
* @param {Object} [options]
|
|
708
|
+
* @returns {{ layoutId: string, duration: number } | null}
|
|
709
|
+
*/
|
|
710
|
+
rewindQueue(steps, durations, options = {}) {
|
|
711
|
+
const { queue } = this.getScheduleQueue(durations, options);
|
|
712
|
+
if (queue.length === 0) return null;
|
|
713
|
+
this._queuePosition = (this._queuePosition - steps + queue.length * steps) % queue.length;
|
|
714
|
+
const entry = queue[this._queuePosition];
|
|
715
|
+
this._queuePosition = (this._queuePosition + 1) % queue.length;
|
|
716
|
+
return entry;
|
|
717
|
+
}
|
|
718
|
+
|
|
694
719
|
peekNextInQueue(durations, options = {}) {
|
|
695
720
|
const { queue } = this.getScheduleQueue(durations, options);
|
|
696
721
|
if (queue.length === 0) return null;
|
package/src/timeline.js
CHANGED
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
* @param {Map<string, number>|null} [videoDurations=null] - Optional map of fileId → probed duration in seconds
|
|
28
28
|
* @returns {{ duration: number, isDynamic: boolean }} Duration in seconds and whether any widget has useDuration=0
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* Extract numeric layout ID from a schedule filename like "123.xlf" or "123"
|
|
32
|
+
* @param {string|number} f - Layout file reference
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
export function parseLayoutFile(f) {
|
|
36
|
+
return parseInt(String(f).replace('.xlf', ''), 10);
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
export function parseLayoutDuration(xlfXml, videoDurations = null) {
|
|
31
40
|
const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');
|
|
32
41
|
const layoutEl = doc.querySelector('layout');
|