@xiboplayer/schedule 0.4.0 → 0.4.3
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/schedule.js +30 -0
- package/src/schedule.test.js +70 -0
- package/src/timeline.js +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/schedule",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Complete scheduling solution: campaigns, dayparting, interrupts, and overlays",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./overlays": "./src/overlays.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@xiboplayer/utils": "0.4.
|
|
14
|
+
"@xiboplayer/utils": "0.4.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"vitest": "^2.0.0"
|
package/src/schedule.js
CHANGED
|
@@ -41,6 +41,36 @@ export class ScheduleManager {
|
|
|
41
41
|
return this.schedule?.dataConnectors || [];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Get dependants map: layoutId → filenames that must be cached before that layout plays.
|
|
46
|
+
* Includes both per-layout and global dependants.
|
|
47
|
+
* Used by download manager to prioritize sub-playlist media alongside its parent layout.
|
|
48
|
+
* @returns {Map<number, string[]>} layoutId → dependant filenames
|
|
49
|
+
*/
|
|
50
|
+
getDependantsMap() {
|
|
51
|
+
const map = new Map();
|
|
52
|
+
if (!this.schedule) return map;
|
|
53
|
+
|
|
54
|
+
const globalDeps = this.schedule.dependants || [];
|
|
55
|
+
|
|
56
|
+
const addLayout = (layout) => {
|
|
57
|
+
const id = parseInt(String(layout.file || layout.id).replace('.xlf', ''), 10);
|
|
58
|
+
const deps = [...globalDeps, ...(layout.dependants || [])];
|
|
59
|
+
if (deps.length > 0) map.set(id, deps);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (this.schedule.layouts) {
|
|
63
|
+
for (const layout of this.schedule.layouts) addLayout(layout);
|
|
64
|
+
}
|
|
65
|
+
if (this.schedule.campaigns) {
|
|
66
|
+
for (const campaign of this.schedule.campaigns) {
|
|
67
|
+
for (const layout of campaign.layouts) addLayout(layout);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return map;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
/**
|
|
45
75
|
* Check if a schedule item is active based on recurrence rules.
|
|
46
76
|
* Supports Week, Day, and Month recurrence types.
|
package/src/schedule.test.js
CHANGED
|
@@ -707,4 +707,74 @@ describe('ScheduleManager - Actions and Commands', () => {
|
|
|
707
707
|
expect(windowMs).toBeGreaterThanOrEqual(60 * 60 * 1000); // at least 1 hour
|
|
708
708
|
});
|
|
709
709
|
});
|
|
710
|
+
|
|
711
|
+
describe('getDependantsMap', () => {
|
|
712
|
+
it('should return empty map when no schedule', () => {
|
|
713
|
+
const map = manager.getDependantsMap();
|
|
714
|
+
expect(map.size).toBe(0);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should collect per-layout dependants from standalone layouts', () => {
|
|
718
|
+
manager.setSchedule({
|
|
719
|
+
layouts: [
|
|
720
|
+
{ file: '472.xlf', dependants: ['11.pdf', 'video.mp4'] },
|
|
721
|
+
{ file: '500.xlf', dependants: ['logo.png'] },
|
|
722
|
+
],
|
|
723
|
+
campaigns: [],
|
|
724
|
+
});
|
|
725
|
+
const map = manager.getDependantsMap();
|
|
726
|
+
expect(map.get(472)).toEqual(['11.pdf', 'video.mp4']);
|
|
727
|
+
expect(map.get(500)).toEqual(['logo.png']);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('should collect per-layout dependants from campaign layouts', () => {
|
|
731
|
+
manager.setSchedule({
|
|
732
|
+
layouts: [],
|
|
733
|
+
campaigns: [
|
|
734
|
+
{
|
|
735
|
+
id: 'c1',
|
|
736
|
+
layouts: [
|
|
737
|
+
{ file: '300.xlf', dependants: ['font.woff2'] },
|
|
738
|
+
],
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
});
|
|
742
|
+
const map = manager.getDependantsMap();
|
|
743
|
+
expect(map.get(300)).toEqual(['font.woff2']);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('should merge global dependants with per-layout dependants', () => {
|
|
747
|
+
manager.setSchedule({
|
|
748
|
+
dependants: ['global-font.woff2'],
|
|
749
|
+
layouts: [
|
|
750
|
+
{ file: '472.xlf', dependants: ['11.pdf'] },
|
|
751
|
+
],
|
|
752
|
+
campaigns: [],
|
|
753
|
+
});
|
|
754
|
+
const map = manager.getDependantsMap();
|
|
755
|
+
expect(map.get(472)).toEqual(['global-font.woff2', '11.pdf']);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should skip layouts with no dependants', () => {
|
|
759
|
+
manager.setSchedule({
|
|
760
|
+
layouts: [
|
|
761
|
+
{ file: '100.xlf', dependants: [] },
|
|
762
|
+
{ file: '200.xlf', dependants: ['bg.jpg'] },
|
|
763
|
+
],
|
|
764
|
+
campaigns: [],
|
|
765
|
+
});
|
|
766
|
+
const map = manager.getDependantsMap();
|
|
767
|
+
expect(map.has(100)).toBe(false);
|
|
768
|
+
expect(map.get(200)).toEqual(['bg.jpg']);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('should handle file IDs without .xlf extension', () => {
|
|
772
|
+
manager.setSchedule({
|
|
773
|
+
layouts: [{ file: '472', dependants: ['11.pdf'] }],
|
|
774
|
+
campaigns: [],
|
|
775
|
+
});
|
|
776
|
+
const map = manager.getDependantsMap();
|
|
777
|
+
expect(map.get(472)).toEqual(['11.pdf']);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
710
780
|
});
|
package/src/timeline.js
CHANGED
|
@@ -153,6 +153,7 @@ function getPlayableLayouts(allLayouts, simPlays, timeMs) {
|
|
|
153
153
|
* @param {Date} [options.from] - Start time (default: now)
|
|
154
154
|
* @param {number} [options.hours] - Hours to simulate (default: 2)
|
|
155
155
|
* @param {number} [options.defaultDuration] - Fallback duration in seconds (default: 60)
|
|
156
|
+
* @param {Date} [options.currentLayoutStartedAt] - When current layout started (adjusts first entry to remaining time)
|
|
156
157
|
* @returns {Array<{layoutFile: string, startTime: Date, endTime: Date, duration: number, isDefault: boolean}>}
|
|
157
158
|
*/
|
|
158
159
|
export function calculateTimeline(schedule, durations, options = {}) {
|
|
@@ -160,8 +161,10 @@ export function calculateTimeline(schedule, durations, options = {}) {
|
|
|
160
161
|
const hours = options.hours || 2;
|
|
161
162
|
const to = new Date(from.getTime() + hours * 3600000);
|
|
162
163
|
const defaultDuration = options.defaultDuration || 60;
|
|
164
|
+
const currentLayoutStartedAt = options.currentLayoutStartedAt || null;
|
|
163
165
|
const timeline = [];
|
|
164
166
|
let currentTime = new Date(from);
|
|
167
|
+
let isFirstEntry = true;
|
|
165
168
|
|
|
166
169
|
// Use getAllLayoutsAtTime if available (new API), fall back to getLayoutsAtTime (old API)
|
|
167
170
|
const hasFullApi = typeof schedule.getAllLayoutsAtTime === 'function';
|
|
@@ -216,7 +219,16 @@ export function calculateTimeline(schedule, durations, options = {}) {
|
|
|
216
219
|
// Round-robin through playable layouts
|
|
217
220
|
for (let i = 0; i < playable.length && currentTime < to && timeline.length < maxEntries; i++) {
|
|
218
221
|
const file = playable[i];
|
|
219
|
-
|
|
222
|
+
let dur = durations.get(file) || defaultDuration;
|
|
223
|
+
|
|
224
|
+
// First entry: use remaining duration if we know when the current layout started
|
|
225
|
+
if (isFirstEntry && currentLayoutStartedAt) {
|
|
226
|
+
const elapsedSec = (from.getTime() - currentLayoutStartedAt.getTime()) / 1000;
|
|
227
|
+
const remaining = Math.max(1, Math.round(dur - elapsedSec));
|
|
228
|
+
dur = remaining;
|
|
229
|
+
isFirstEntry = false;
|
|
230
|
+
}
|
|
231
|
+
|
|
220
232
|
const endMs = currentTime.getTime() + dur * 1000;
|
|
221
233
|
|
|
222
234
|
const entry = {
|