Complete scheduling solution: campaigns, dayparting, interrupts, overlays, and timeline prediction.
Manages all aspects of digital signage scheduling to determine which layouts play when:
- Campaign scheduling -- groups of layouts with time windows and priorities
- Dayparting -- weekly time slots (Mon-Fri 09:00-17:00, evenings, weekends) with midnight-crossing support
- Priority fallback -- higher-priority layouts hide lower-priority ones; rate limiting triggers automatic fallback
-
Rate limiting --
maxPlaysPerHourwith even distribution (prevents bursts, ensures spacing) - Interrupts (Share of Voice) -- layouts that must play X% of each hour, interleaved with normal content
- Overlays -- layouts that appear on top of main layouts without interrupting playback
- Criteria evaluation -- conditional display based on time, weather, custom display properties
- Geo-fencing -- location-based filtering (point + radius, Haversine distance)
- Timeline prediction -- deterministic simulation of future playback for UI overlays
- Default layout -- fallback when no campaigns are active
CMS Schedule XML
|
v
+-------------------------------------+
| Schedule Parser |
| +- campaigns[] |
| +- layouts[] |
| +- overlays[] |
| +- default layout |
+-------------------------------------+
|
v
+-------------------------------------+
| Evaluation Engine |
| +- Recurrence (Week/Day/Month) |
| +- Time windows (dayparting) |
| +- Criteria (weather, properties) |
| +- Geo-fencing |
| +- Priority + rate-limit filtering |
+-------------------------------------+
|
v
+-------------------------------------+
| Schedule Queue Builder (LCM-based) |
| Deterministic round-robin with: |
| +- Rate-limited slots (even spaced)|
| +- Priority fallback |
| +- Default fills gaps |
+-------------------------------------+
|
+-> getCurrentLayouts() -> Renderer
+-> getLayoutsInTimeRange() -> Timeline Overlay
+-> Track play history -> Rate limiting
npm install @xiboplayer/scheduleimport { ScheduleManager } from '@xiboplayer/schedule';
const schedule = new ScheduleManager();
schedule.setSchedule({
campaigns: [
{
id: 1,
priority: 100,
fromdt: '2025-01-01 09:00',
todt: '2025-12-31 17:00',
recurrenceType: 'Week',
recurrenceRepeatsOn: '1,2,3,4,5', // Mon-Fri
layouts: [
{ id: 10, file: '10.xlf', duration: 30 },
{ id: 11, file: '11.xlf', duration: 30 },
],
},
],
default: '99.xlf',
});
const layoutsToPlay = schedule.getCurrentLayouts();
// Business hours: ['10.xlf', '11.xlf']
// After hours: ['99.xlf'] (default)schedule.setSchedule({
layouts: [
{
id: 1,
file: '1.xlf',
recurrenceType: 'Week',
recurrenceRepeatsOn: '1,2,3,4,5,6,7',
fromdt: '1970-01-01 22:00:00', // 10 PM
todt: '1970-01-01 02:00:00', // 2 AM (next day)
},
],
});
// Friday 23:00: returns ['1.xlf']
// Saturday 01:00: returns ['1.xlf'] (midnight crossing works)schedule.setSchedule({
layouts: [
{
id: 1,
file: '1.xlf',
maxPlaysPerHour: 3, // 3 times per hour, evenly spaced
},
],
});
schedule.recordPlay('1'); // Play at 09:00
// Can't play again until 09:20 (60 / 3 = 20 min minimum gap)schedule.setSchedule({
layouts: [
{ id: 1, file: '1.xlf', duration: 30 },
{ id: 2, file: '2.xlf', duration: 30, shareOfVoice: 20 }, // 20% of each hour
],
});
const layouts = schedule.getCurrentLayouts();
// Interleaved: normal, normal, interrupt, normal, normal, interrupt, ...schedule.setSchedule({
layouts: [
{
id: 1,
file: '1.xlf',
criteria: [
{ metric: 'weatherTemp', condition: 'greaterThan', value: '25', type: 'number' },
],
},
],
});
schedule.setWeatherData({ temperature: 28, humidity: 65 });
// Layout 1 displays only when temperature > 25schedule.setLocation(37.7749, -122.4194);
schedule.setSchedule({
layouts: [
{
id: 1,
file: '1.xlf',
isGeoAware: true,
geoLocation: '37.7749,-122.4194,500', // lat,lng,radius_meters
},
],
});
// Returns ['1.xlf'] only if player is within 500mconst timeline = calculateTimeline(queue, queuePosition, {
from: new Date(),
hours: 2,
defaultLayout: schedule.schedule.default,
durations: durations,
});
// Returns:
// [
// { layoutFile: '10.xlf', startTime, endTime, duration: 30, isDefault: false },
// { layoutFile: '11.xlf', startTime, endTime, duration: 30, isDefault: false },
// ...
// ]When getCurrentLayouts() is called:
- Filter time-active items -- campaigns and standalone layouts within their date/time window and recurrence rules
- Apply criteria -- filter by weather, display properties, geo-fencing
-
Apply rate limiting -- exclude layouts that exceeded
maxPlaysPerHour - Find max priority -- only max priority items win
- Extract layouts -- campaigns return all their layouts; standalone layouts contribute themselves
- Process interrupts -- separate interrupt layouts, calculate share-of-voice, interleave
- Return layout files -- ready for the renderer
The queue is a pre-computed, deterministic round-robin cycle:
-
LCM period -- Least Common Multiple of all
maxPlaysPerHourintervals (capped at 2 hours) - Simulation -- walks the period applying priority and rate-limit rules at each step
- Caching -- reused until the active layout set changes
- Predictable -- answers "what's playing in 30 minutes?" offline
| Type | Pattern | Example |
|---|---|---|
| Week | Specific days + time-of-day | Mon-Fri 09:00-17:00 |
| Day | Daily with optional interval | Every 2 days |
| Month | Specific days of month | 1st, 15th (monthly) |
Midnight crossing: 22:00 - 02:00 works across day boundaries.
Built-in metrics: dayOfWeek, dayOfMonth, month, hour, isoDay
Weather metrics: weatherTemp, weatherHumidity, weatherWindSpeed, weatherCondition, weatherCloudCover
Operators: equals, notEquals, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals, contains, startsWith, endsWith, in
- Format:
"lat,lng,radius"(e.g.,"37.7749,-122.4194,500") - Default radius: 500 meters
- Calculation: Haversine formula (great-circle distance)
- Permissive: if no location available, layout displays (fail-open for offline)
new ScheduleManager(options?)| Option | Type | Description |
|---|---|---|
interruptScheduler |
InterruptScheduler? | Optional interrupt handler |
displayProperties |
Object? | Custom display fields from CMS |
| Method | Returns | Description |
|---|---|---|
setSchedule(schedule) |
void | Load schedule from XMDS response |
getCurrentLayouts() |
string[] | Layouts active now |
getLayoutsAtTime(date) |
string[] | Layouts at specific time |
getAllLayoutsAtTime(date) |
Array | All time-active layouts with metadata |
getScheduleQueue(durations) |
{queue, periodSeconds} | Pre-computed round-robin queue |
popNextFromQueue(durations) |
{layoutId, duration} | Pop next entry, advance position |
peekNextInQueue(durations) |
{layoutId, duration} | Peek without advancing |
recordPlay(layoutId) |
void | Track a play for rate limiting |
canPlayLayout(layoutId, max) |
boolean | Check if layout can play now |
setWeatherData(data) |
void | Update weather for criteria |
setLocation(lat, lng) |
void | Set GPS location for geo-fencing |
setDisplayProperties(props) |
void | Set custom display fields |
detectConflicts(options) |
Array | Find priority-shadowing conflicts |
| Method | Returns | Description |
|---|---|---|
setOverlays(overlays) |
void | Update overlay list |
getCurrentOverlays() |
Array | Active overlays (sorted by priority) |
shouldCheckOverlays(lastCheck) |
boolean | Check interval (every 60s) |
import { calculateTimeline, parseLayoutDuration, buildScheduleQueue } from '@xiboplayer/schedule';
const { duration } = parseLayoutDuration(xlfXml, videoDurations?);
const { queue, periodSeconds } = buildScheduleQueue(allLayouts, durations);
const timeline = calculateTimeline(queue, position, { from, hours, defaultLayout, durations });No external dependencies -- fully self-contained scheduling engine.
-
@xiboplayer/utils-- logging only
xiboplayer.org · Part of the XiboPlayer SDK