mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 03:43:50 +00:00
refactor: roll mode
* chore: escalate stack trace to console * refactor: event finding in roll * docs: initial roll specification * refactor: call to roll * refactor: normalise index * refactor: roll into next event * refactor: hot reload on waiting to roll * refactor: wait to roll next
This commit is contained in:
@@ -9,7 +9,7 @@ type UpdateCallbackFn = (updateResult: UpdateResult) => void;
|
||||
/**
|
||||
* Service manages Ontime's main timer
|
||||
*/
|
||||
export class TimerService {
|
||||
export class EventTimer {
|
||||
private readonly _interval: NodeJS.Timeout;
|
||||
/** how often we recalculate */
|
||||
static _refreshInterval: number;
|
||||
@@ -26,15 +26,14 @@ export class TimerService {
|
||||
* @param {function} [timerConfig.onUpdateCallback] how often we update the socket
|
||||
*/
|
||||
constructor(timerConfig: { refresh: number; updateInterval: number }) {
|
||||
TimerService._refreshInterval = timerConfig.refresh;
|
||||
EventTimer._refreshInterval = timerConfig.refresh;
|
||||
this._interval = setInterval(() => {
|
||||
this.update();
|
||||
}, TimerService._refreshInterval);
|
||||
}, EventTimer._refreshInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows setting a callback for when the timer updates
|
||||
* @param callback
|
||||
*/
|
||||
setOnUpdateCallback(callback: (updateResult: UpdateResult) => void) {
|
||||
this.onUpdateCallback = callback;
|
||||
@@ -73,7 +72,6 @@ export class TimerService {
|
||||
|
||||
/**
|
||||
* Adds time to running timer by given amount
|
||||
* @param {number} amount
|
||||
*/
|
||||
addTime(amount: number): boolean {
|
||||
if (!runtimeState.addTime(amount)) {
|
||||
@@ -104,10 +102,9 @@ export class TimerService {
|
||||
|
||||
/**
|
||||
* Loads roll information into timer service
|
||||
* @param {OntimeEvent[]} rundown -- list of events to run
|
||||
*/
|
||||
roll(rundown: OntimeRundown) {
|
||||
runtimeState.roll(rundown);
|
||||
return runtimeState.roll(rundown);
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
@@ -0,0 +1,448 @@
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { loadRoll } from '../rollUtils.js';
|
||||
|
||||
const baseEvent = {
|
||||
type: SupportedEvent.Event,
|
||||
skip: false,
|
||||
};
|
||||
|
||||
function makeOntimeEvent(patch: Partial<OntimeEvent>): OntimeEvent {
|
||||
return {
|
||||
...baseEvent,
|
||||
...patch,
|
||||
} as OntimeEvent;
|
||||
}
|
||||
|
||||
function prepareTimedEvents(events: Partial<OntimeEvent>[]): OntimeEvent[] {
|
||||
return events.map(makeOntimeEvent);
|
||||
}
|
||||
|
||||
describe('loadRoll()', () => {
|
||||
const eventlist = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 5,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timeStart: 30,
|
||||
timeEnd: 40,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timeStart: 40,
|
||||
timeEnd: 50,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
timeStart: 50,
|
||||
timeEnd: 60,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
timeStart: 60,
|
||||
timeEnd: 70,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
timeStart: 70,
|
||||
timeEnd: 80,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
|
||||
it('should roll to the day after if timer is at 100', () => {
|
||||
const now = 100;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should be waiting to start if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the first event if timer is at 5', () => {
|
||||
const now = 5;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the second event if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
event: timedEvents[1],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the third event if timer is at 10', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the fifth event if timer is at 49', () => {
|
||||
const now = 49;
|
||||
const expected = {
|
||||
event: timedEvents[4],
|
||||
index: 4,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the seventh event if timer is at 63', () => {
|
||||
const now = 63;
|
||||
const expected = {
|
||||
event: timedEvents[6],
|
||||
index: 6,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should start the eight event if timer is at 75', () => {
|
||||
const now = 75;
|
||||
const expected = {
|
||||
event: timedEvents[7],
|
||||
index: 7,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() handle edge cases with midnight', () => {
|
||||
it('should find an event that crosses midnight', () => {
|
||||
const now = 23 * MILLIS_PER_HOUR;
|
||||
const eventlist = [
|
||||
{
|
||||
id: '0',
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 20 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 1 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 1 * MILLIS_PER_HOUR,
|
||||
timeEnd: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timeStart: 1 * MILLIS_PER_HOUR,
|
||||
timeEnd: 2 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should not skip to the second day', () => {
|
||||
/**
|
||||
* NOTE: this is a potentially contentious decision
|
||||
*
|
||||
* The idea here is that it makes no sense for us to jump to the second / third day on activating roll
|
||||
* if the user wants to skip a portion of the rundown, they can manually jump to the event and activate roll
|
||||
*
|
||||
* On our side, this simplifies logic and makes behaviour more predictable
|
||||
*/
|
||||
const now = 8 * MILLIS_PER_HOUR;
|
||||
const eventlist = [
|
||||
{
|
||||
id: '0',
|
||||
timeStart: 21 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 3 * MILLIS_PER_HOUR,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 3 * MILLIS_PER_HOUR,
|
||||
timeEnd: 10 * MILLIS_PER_HOUR,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() handle edge cases with before and after start', () => {
|
||||
it('should prepare first event, if we are not yet in the rundown start', () => {
|
||||
const now = 7 * MILLIS_PER_HOUR;
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 10 * MILLIS_PER_HOUR,
|
||||
timeEnd: 11 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should prepare first event, if we are over the rundown end', () => {
|
||||
const now = 18 * MILLIS_PER_HOUR;
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 10 * MILLIS_PER_HOUR,
|
||||
timeEnd: 11 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('should account for a rundown that goes through midnight', () => {
|
||||
const now = 1 * MILLIS_PER_HOUR;
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 10 * MILLIS_PER_HOUR,
|
||||
timeEnd: 2 * MILLIS_PER_HOUR,
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state.isPending).toBeUndefined();
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('loads upcoming event while waiting to roll', () => {
|
||||
const now = 6000; // 00:01
|
||||
const singleEventList = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 72000000, // 20:00
|
||||
timeEnd: 72010000, // 20:10
|
||||
isPublic: true,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: singleEventList[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
const state = loadRoll(singleEventList, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRoll() test that roll behaviour with overlapping times', () => {
|
||||
const eventlist = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 10,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 10,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const timedEvents = prepareTimedEvents(eventlist);
|
||||
|
||||
it('if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
event: timedEvents[0],
|
||||
index: 0,
|
||||
isPending: true,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 10, it ignores events with 0 duration', () => {
|
||||
const now = 10;
|
||||
const expected = {
|
||||
event: timedEvents[1],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
event: timedEvents[1],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 20', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 25', () => {
|
||||
const now = 25;
|
||||
const expected = {
|
||||
event: timedEvents[2],
|
||||
index: 2,
|
||||
};
|
||||
|
||||
const state = loadRoll(timedEvents, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// issue #58
|
||||
describe('loadRoll() test that roll behaviour multi day event edge cases', () => {
|
||||
it('should recognise a playing event where its schedule spans over midnight', () => {
|
||||
const now = 66600000; // 19:30
|
||||
const eventlist = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
isPublic: false,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: eventlist[0],
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const state = loadRoll(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if the start time is the day after end time, and both are later than now', () => {
|
||||
const now = 66840000; // 19:34
|
||||
const eventlist = [
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
isPublic: false,
|
||||
}),
|
||||
];
|
||||
const expected = {
|
||||
event: eventlist[0],
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const state = loadRoll(eventlist, now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
import { MILLIS_PER_HOUR, dayInMs, millisToString } from 'ontime-utils';
|
||||
import { EndAction, OntimeEvent, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||
|
||||
import {
|
||||
getCurrent,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
getTimerPhase,
|
||||
getTotalDuration,
|
||||
@@ -696,520 +695,6 @@ describe('skippedOutOfEvent()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRollTimers()', () => {
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 5,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 20,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timeStart: 30,
|
||||
timeEnd: 40,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timeStart: 40,
|
||||
timeEnd: 50,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
timeStart: 50,
|
||||
timeEnd: 60,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
timeStart: 60,
|
||||
timeEnd: 70,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
timeStart: 70,
|
||||
timeEnd: 80,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
it('if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 5,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 5', () => {
|
||||
const now = 5;
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: eventlist[0].id,
|
||||
publicIndex: null,
|
||||
nextIndex: 1,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 5,
|
||||
nextEvent: eventlist[1],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: eventlist[0],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
nowIndex: 1,
|
||||
nowId: eventlist[1].id,
|
||||
publicIndex: null,
|
||||
nextIndex: 2,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 5,
|
||||
nextEvent: eventlist[2],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: eventlist[1],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 20', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
nowIndex: 2,
|
||||
nowId: eventlist[2].id,
|
||||
publicIndex: null,
|
||||
nextIndex: 3,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: 10,
|
||||
nextEvent: eventlist[3],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: eventlist[2],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 49', () => {
|
||||
const now = 49;
|
||||
const expected = {
|
||||
nowIndex: 4,
|
||||
nowId: eventlist[4].id,
|
||||
publicIndex: 4,
|
||||
nextIndex: 5,
|
||||
publicNextIndex: 6,
|
||||
timeToNext: 1,
|
||||
nextEvent: eventlist[5],
|
||||
nextPublicEvent: eventlist[6],
|
||||
currentEvent: eventlist[4],
|
||||
currentPublicEvent: eventlist[4],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 63', () => {
|
||||
const now = 63;
|
||||
const expected = {
|
||||
nowIndex: 6,
|
||||
nowId: eventlist[6].id,
|
||||
publicIndex: 6,
|
||||
nextIndex: 7,
|
||||
publicNextIndex: null,
|
||||
timeToNext: 7,
|
||||
nextEvent: eventlist[7],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[6],
|
||||
currentPublicEvent: eventlist[6],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 75', () => {
|
||||
const now = 75;
|
||||
const expected = {
|
||||
nowIndex: 7,
|
||||
nowId: eventlist[7].id,
|
||||
publicIndex: 6,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[7],
|
||||
currentPublicEvent: eventlist[6],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 100 we roll to day after', () => {
|
||||
const now = 100;
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 4,
|
||||
timeToNext: dayInMs - now + eventlist[0].timeStart!,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[4],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 39600000, // 11:00
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 64800000; // 18:00
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 0,
|
||||
timeToNext: dayInMs - now + singleEventList[0].timeStart!,
|
||||
nextEvent: singleEventList[0],
|
||||
nextPublicEvent: singleEventList[0],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 3600000, // 01:00
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 60000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: singleEventList[0].id,
|
||||
publicIndex: 0,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: singleEventList[0],
|
||||
currentPublicEvent: singleEventList[0],
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
it('handles rolls to next day with real values', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 36000000, // 10:00
|
||||
timeEnd: 3600000, // 01:00
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 60000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: singleEventList[0].id,
|
||||
publicIndex: 0,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: singleEventList[0],
|
||||
currentPublicEvent: singleEventList[0],
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('loads upcoming event while waiting to roll', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 72000000, // 20:00
|
||||
timeEnd: 72010000, // 20:10
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 6000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 0,
|
||||
timeToNext: 72000000 - now,
|
||||
nextEvent: singleEventList[0],
|
||||
nextPublicEvent: singleEventList[0],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles roll that goes over midnight', () => {
|
||||
const singleEventList: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 72000000, // 20:00
|
||||
timeEnd: 60000, // 00:10
|
||||
isPublic: true,
|
||||
},
|
||||
];
|
||||
const now = 6000; // 00:01
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: singleEventList[0].id,
|
||||
publicIndex: 0,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: singleEventList[0],
|
||||
currentPublicEvent: singleEventList[0],
|
||||
};
|
||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRollTimers() test that roll behaviour with overlapping times', () => {
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 10,
|
||||
timeEnd: 10,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timeStart: 10,
|
||||
timeEnd: 30,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
it('if timer is at 0', () => {
|
||||
const now = 0;
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
nowId: null,
|
||||
publicIndex: null,
|
||||
nextIndex: 0,
|
||||
publicNextIndex: 1,
|
||||
timeToNext: 10,
|
||||
nextEvent: eventlist[0],
|
||||
nextPublicEvent: eventlist[1],
|
||||
currentEvent: null,
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 10', () => {
|
||||
const now = 10;
|
||||
const expected = {
|
||||
nowIndex: 1,
|
||||
nowId: eventlist[1].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: 2,
|
||||
publicNextIndex: null,
|
||||
timeToNext: 0,
|
||||
nextEvent: eventlist[2],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[1],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 15', () => {
|
||||
const now = 15;
|
||||
const expected = {
|
||||
nowIndex: 1,
|
||||
nowId: eventlist[1].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: 2,
|
||||
publicNextIndex: null,
|
||||
timeToNext: -5,
|
||||
nextEvent: eventlist[2],
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[1],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 20', () => {
|
||||
const now = 20;
|
||||
const expected = {
|
||||
nowIndex: 2,
|
||||
nowId: eventlist[2].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[2],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if timer is at 25', () => {
|
||||
const now = 25;
|
||||
const expected = {
|
||||
nowIndex: 2,
|
||||
nowId: eventlist[2].id,
|
||||
publicIndex: 1,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[2],
|
||||
currentPublicEvent: eventlist[1],
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// issue #58
|
||||
describe('getRollTimers() test that roll behaviour multi day event edge cases', () => {
|
||||
it('if the start time is the day after end time, and start time is earlier than now', () => {
|
||||
const now = 66600000; // 19:30
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
nowId: '1',
|
||||
publicIndex: null,
|
||||
nextIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
nextEvent: null,
|
||||
nextPublicEvent: null,
|
||||
currentEvent: eventlist[0],
|
||||
currentPublicEvent: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('if the start time is the day after end time, and both are later than now', () => {
|
||||
const now = 66840000; // 19:34
|
||||
const eventlist: Partial<OntimeEvent>[] = [
|
||||
{
|
||||
id: '1',
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
currentEvent: {
|
||||
id: '1',
|
||||
isPublic: false,
|
||||
timeEnd: 66900000,
|
||||
timeStart: 67200000,
|
||||
},
|
||||
currentPublicEvent: null,
|
||||
nextEvent: null,
|
||||
nextIndex: null,
|
||||
nextPublicEvent: null,
|
||||
nowId: '1',
|
||||
nowIndex: 0,
|
||||
publicIndex: null,
|
||||
publicNextIndex: null,
|
||||
timeToNext: null,
|
||||
};
|
||||
|
||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
||||
expect(state).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('normaliseEndTime()', () => {
|
||||
const t1 = {
|
||||
start: 10,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { dayInMs, getFirstEvent, getLastEvent } from 'ontime-utils';
|
||||
import { OntimeEvent, MaybeNumber, PlayableEvent, isPlayableEvent } from 'ontime-types';
|
||||
|
||||
import { normaliseEndTime } from './timerUtils.js';
|
||||
|
||||
/**
|
||||
* Finds current event in a rolling rundown
|
||||
*/
|
||||
export function loadRoll(
|
||||
timedEvents: OntimeEvent[],
|
||||
timeNow: number,
|
||||
): {
|
||||
event: PlayableEvent | null;
|
||||
index: MaybeNumber;
|
||||
isPending?: boolean;
|
||||
} {
|
||||
const { firstEvent } = getFirstEvent(timedEvents);
|
||||
const { lastEvent } = getLastEvent(timedEvents);
|
||||
|
||||
if (!firstEvent || !lastEvent) {
|
||||
return { event: null, index: null };
|
||||
}
|
||||
|
||||
// check that the rundown wraps around midnight
|
||||
const wrapsAroundMidnight = firstEvent.timeStart > lastEvent.timeEnd;
|
||||
|
||||
if (!wrapsAroundMidnight) {
|
||||
// check whether we are before or after the rundown
|
||||
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
||||
const isAfterRundown = timeNow > lastNormalEnd;
|
||||
const isBeforeRundown = timeNow < firstEvent.timeStart && !isAfterRundown;
|
||||
|
||||
if (isAfterRundown || isBeforeRundown) {
|
||||
return { event: firstEvent, index: 0, isPending: true };
|
||||
}
|
||||
}
|
||||
|
||||
// we know we are in the middle of the rundown and we need to find the current event
|
||||
// account for number of times we went over midnight
|
||||
let daySpan = 0;
|
||||
|
||||
for (let i = 0; i < timedEvents.length; i++) {
|
||||
const event = timedEvents[i];
|
||||
|
||||
if (!isPlayableEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we check if event crosses midnight
|
||||
if (event.timeStart > event.timeEnd) {
|
||||
daySpan++;
|
||||
}
|
||||
|
||||
const correctedDays = dayInMs * daySpan;
|
||||
const correctedStart = event.timeStart + correctedDays;
|
||||
const correctedEnd = event.timeEnd + correctedDays;
|
||||
|
||||
/**
|
||||
* there are 3 possible states for an event
|
||||
* 1. event is already finished
|
||||
* 2. event is running
|
||||
* 3. event is in the future
|
||||
*/
|
||||
|
||||
// 1. event is already finished
|
||||
// when does the event end (handle midnight)
|
||||
const normalEnd = normaliseEndTime(correctedStart, correctedEnd);
|
||||
if (normalEnd <= timeNow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. event is running and is the first event in our time slot
|
||||
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
|
||||
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
|
||||
if (hasStarted) {
|
||||
return { event, index: i };
|
||||
}
|
||||
|
||||
// 3. event will run in the future
|
||||
// we set the isPending flag to indicate that the event is currently playing
|
||||
return { event, index: i, isPending: true };
|
||||
}
|
||||
|
||||
// in case we were unable to find anything, we load the first event
|
||||
return { event: firstEvent, index: 0, isPending: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, checks whether the event start is the day after
|
||||
*/
|
||||
export function normaliseRollStart(start: number, clock: number) {
|
||||
return start < clock ? start + dayInMs : start;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
import { getPlayableEvents } from './rundownUtils.js';
|
||||
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
type PatchWithId = (Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) & { id: string };
|
||||
@@ -215,8 +215,8 @@ export async function swapEvents(from: string, to: string) {
|
||||
* Called when we make changes to the rundown object
|
||||
*/
|
||||
function updateRuntimeOnChange() {
|
||||
const playableEvents = getPlayableEvents();
|
||||
const numEvents = playableEvents.length;
|
||||
const timedEvents = getTimedEvents();
|
||||
const numEvents = timedEvents.length;
|
||||
const metadata = cache.getMetadata();
|
||||
|
||||
// schedule an update for the end of the event loop
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { OntimeEvent, OntimeRundown, isOntimeEvent, RundownCached, OntimeRundownEntry } from 'ontime-types';
|
||||
import { OntimeEvent, OntimeRundown, RundownCached, OntimeRundownEntry, PlayableEvent } from 'ontime-types';
|
||||
import { filterPlayable, filterTimedEvents } from 'ontime-utils';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
|
||||
/**
|
||||
* returns the normalised rundown
|
||||
*/
|
||||
export function getNormalisedRundown(): RundownCached {
|
||||
return cache.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns entire unfiltered rundown
|
||||
* @return {array}
|
||||
*/
|
||||
export function getRundown(): OntimeRundown {
|
||||
return cache.getPersistedRundown();
|
||||
@@ -16,32 +19,20 @@ export function getRundown(): OntimeRundown {
|
||||
|
||||
/**
|
||||
* returns all events of type OntimeEvent
|
||||
* @return {array}
|
||||
*/
|
||||
export function getTimedEvents(): OntimeEvent[] {
|
||||
return getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
return filterTimedEvents(getRundown());
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all events that can be loaded
|
||||
* @return {array}
|
||||
*/
|
||||
export function getPlayableEvents(): OntimeEvent[] {
|
||||
return getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns number of events that can be loaded
|
||||
* @return {number}
|
||||
*/
|
||||
export function getNumEvents(): number {
|
||||
return getPlayableEvents().length;
|
||||
export function getPlayableEvents(): PlayableEvent[] {
|
||||
return filterPlayable(getRundown());
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an event given its index after filtering for OntimeEvents
|
||||
* @param {number} eventIndex
|
||||
* @return {OntimeEvent | undefined}
|
||||
*/
|
||||
export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
||||
const timedEvents = getTimedEvents();
|
||||
@@ -50,8 +41,6 @@ export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
||||
|
||||
/**
|
||||
* returns first event that matches a given ID
|
||||
* @param {string} eventId
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function getEventWithId(eventId: string): OntimeRundownEntry | undefined {
|
||||
const rundown = getRundown();
|
||||
@@ -60,17 +49,14 @@ export function getEventWithId(eventId: string): OntimeRundownEntry | undefined
|
||||
|
||||
/**
|
||||
* returns first event that matches a given cue
|
||||
* @param {string} targetCue
|
||||
* @param {number} currentEventIndex
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function getNextEventWithCue(targetCue: string, currentEventIndex = 0): OntimeEvent | undefined {
|
||||
const timedEvents = getPlayableEvents();
|
||||
const playableEvents = getPlayableEvents();
|
||||
const lowerCaseCue = targetCue.toLowerCase();
|
||||
|
||||
for (let i = currentEventIndex; i < timedEvents.length; i++) {
|
||||
const event = timedEvents.at(i);
|
||||
if (event && event.cue.toLowerCase() === lowerCaseCue) {
|
||||
for (let i = currentEventIndex; i < playableEvents.length; i++) {
|
||||
const event = playableEvents.at(i);
|
||||
if (event?.cue.toLowerCase() === lowerCaseCue) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
@@ -78,43 +64,41 @@ export function getNextEventWithCue(targetCue: string, currentEventIndex = 0): O
|
||||
|
||||
/**
|
||||
* finds the previous event
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function findPrevious(currentEventId?: string): OntimeEvent | null {
|
||||
const timedEvents = getPlayableEvents();
|
||||
if (!timedEvents || !timedEvents.length) {
|
||||
const playableEvents = getPlayableEvents();
|
||||
if (!playableEvents || !playableEvents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0) ?? null;
|
||||
return playableEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const currentIndex = playableEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = Math.max(currentIndex - 1, 0);
|
||||
const previousEvent = timedEvents.at(newIndex) ?? null;
|
||||
const previousEvent = playableEvents.at(newIndex) ?? null;
|
||||
return previousEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* finds the next event
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
export function findNext(currentEventId?: string): OntimeEvent | null {
|
||||
const timedEvents = getPlayableEvents();
|
||||
if (!timedEvents || !timedEvents.length) {
|
||||
export function findNext(currentEventId?: string): PlayableEvent | null {
|
||||
const playableEvents = getPlayableEvents();
|
||||
if (!playableEvents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0) ?? null;
|
||||
return playableEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const currentIndex = playableEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = currentIndex + 1;
|
||||
const nextEvent = timedEvents.at(newIndex);
|
||||
const nextEvent = playableEvents.at(newIndex);
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
EndAction,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
LogOrigin,
|
||||
MaybeNumber,
|
||||
OntimeEvent,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
TimerLifeCycle,
|
||||
TimerPhase,
|
||||
} from 'ontime-types';
|
||||
import { filterPlayable, millisToString, validatePlayback } from 'ontime-utils';
|
||||
import { millisToString, validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { deepEqual } from 'fast-equals';
|
||||
|
||||
@@ -19,7 +20,7 @@ import type { RuntimeState } from '../../stores/runtimeState.js';
|
||||
import { timerConfig } from '../../config/config.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
import { TimerService } from '../TimerService.js';
|
||||
import { EventTimer } from '../EventTimer.js';
|
||||
import { RestorePoint, restoreService } from '../RestoreService.js';
|
||||
import {
|
||||
findNext,
|
||||
@@ -27,20 +28,20 @@ import {
|
||||
getEventAtIndex,
|
||||
getNextEventWithCue,
|
||||
getEventWithId,
|
||||
getPlayableEvents,
|
||||
getRundown,
|
||||
getTimedEvents,
|
||||
} from '../rundown-service/rundownUtils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
import { integrationService } from '../integration-service/IntegrationService.js';
|
||||
|
||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||
import { skippedOutOfEvent } from '../timerUtils.js';
|
||||
|
||||
/**
|
||||
* Service manages runtime status of app
|
||||
* Coordinating with necessary services
|
||||
*/
|
||||
class RuntimeService {
|
||||
private eventTimer: TimerService;
|
||||
private eventTimer: EventTimer;
|
||||
private lastIntegrationClockUpdate = -1;
|
||||
private lastIntegrationTimerValue = -1;
|
||||
|
||||
@@ -52,8 +53,8 @@ class RuntimeService {
|
||||
/** last known state */
|
||||
static previousState: RuntimeState;
|
||||
|
||||
constructor(timerService: TimerService) {
|
||||
this.eventTimer = timerService;
|
||||
constructor(eventTimer: EventTimer) {
|
||||
this.eventTimer = eventTimer;
|
||||
|
||||
RuntimeService.previousTimerUpdate = -1;
|
||||
RuntimeService.previousTimerValue = -1;
|
||||
@@ -63,7 +64,7 @@ class RuntimeService {
|
||||
|
||||
/** Checks result of an update and notifies integrations as needed */
|
||||
@broadcastResult
|
||||
checkTimerUpdate({ shouldCallRoll, hasTimerFinished }: runtimeState.UpdateResult) {
|
||||
checkTimerUpdate({ hasTimerFinished, hasSecondaryTimerFinished }: runtimeState.UpdateResult) {
|
||||
const newState = runtimeState.getState();
|
||||
|
||||
// 1. find if we need to dispatch integrations related to the phase
|
||||
@@ -82,35 +83,36 @@ class RuntimeService {
|
||||
|
||||
// 2. handle edge cases related to roll
|
||||
if (newState.timer.playback === Playback.Roll) {
|
||||
// check if we need to call roll again
|
||||
const needsEvent =
|
||||
newState.eventNow === null
|
||||
? true
|
||||
: skippedOutOfEvent(newState, this.lastIntegrationClockUpdate, timerConfig.skipLimit);
|
||||
const hasFinishedRoll = hasTimerFinished && shouldCallRoll;
|
||||
if (shouldCallRoll || needsEvent) {
|
||||
if (hasFinishedRoll) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
});
|
||||
}
|
||||
// check if we need to call any side effects
|
||||
|
||||
// we dont call this.roll because we need to bypass the checks
|
||||
const rundown = getRundown();
|
||||
// TODO: by not calling roll, we dont get the events
|
||||
this.eventTimer.roll(rundown);
|
||||
if (hasSecondaryTimerFinished) {
|
||||
// if the secondary timer has finished, we need to call roll
|
||||
// since event is already loaded
|
||||
this.rollLoaded();
|
||||
} else if (hasTimerFinished) {
|
||||
// if the timer has finished, we need to load next and keep rolling
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
});
|
||||
this.loadNext();
|
||||
this.rollLoaded();
|
||||
} else if (skippedOutOfEvent(newState, this.lastIntegrationClockUpdate, timerConfig.skipLimit)) {
|
||||
// if we have skipped out of the event, we will recall roll
|
||||
// to push the playback to the right place
|
||||
// this comes with the caveat that we will lose our runtime data
|
||||
this.roll(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. find if we need to process actions related to the timer finishing
|
||||
if (newState.timer.playback !== Playback.Roll && hasTimerFinished) {
|
||||
if (newState.timer.playback === Playback.Play && hasTimerFinished) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
});
|
||||
|
||||
// handle end action if there was a timer playing
|
||||
// actions are added to the queue stack to ensure that the order of operations is maintained
|
||||
if (newState.timer.playback === Playback.Play && newState.eventNow) {
|
||||
if (newState.eventNow) {
|
||||
if (newState.eventNow.endAction === EndAction.Stop) {
|
||||
setTimeout(this.stop.bind(this), 0);
|
||||
} else if (newState.eventNow.endAction === EndAction.LoadNext) {
|
||||
@@ -177,7 +179,7 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
private isNewNext() {
|
||||
const timedEvents = getPlayableEvents();
|
||||
const timedEvents = getTimedEvents();
|
||||
const state = runtimeState.getState();
|
||||
const now = state.eventNow?.id;
|
||||
const next = state.eventNext?.id;
|
||||
@@ -233,41 +235,41 @@ class RuntimeService {
|
||||
// 3. the edited event replaces next event
|
||||
let isNext = false;
|
||||
|
||||
// TODO: review logic
|
||||
if (safeOption || eventInMemory) {
|
||||
if (state.timer.playback === Playback.Roll) {
|
||||
this.roll();
|
||||
}
|
||||
// load stuff again, but keep running if our events still exist
|
||||
const eventNow = getEventWithId(state.eventNow.id);
|
||||
if (!isOntimeEvent(eventNow)) {
|
||||
if (state.eventNow !== null) {
|
||||
// load stuff again, but keep running if our events still exist
|
||||
const eventNow = getEventWithId(state.eventNow.id);
|
||||
if (!isOntimeEvent(eventNow) || !isPlayableEvent(eventNow)) {
|
||||
return;
|
||||
}
|
||||
const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id;
|
||||
if (onlyChangedNow) {
|
||||
runtimeState.reload(eventNow);
|
||||
} else {
|
||||
const rundown = getRundown();
|
||||
runtimeState.reloadAll(rundown);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id;
|
||||
if (onlyChangedNow) {
|
||||
runtimeState.reload(eventNow);
|
||||
} else {
|
||||
const rundown = getRundown();
|
||||
runtimeState.reloadAll(eventNow, rundown);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Maybe the event will become the next
|
||||
isNext = this.isNewNext();
|
||||
if (isNext) {
|
||||
const rundown = getRundown();
|
||||
runtimeState.loadNext(rundown);
|
||||
const timedEvents = getTimedEvents();
|
||||
runtimeState.loadNext(timedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* makes calls for loading and starting given event
|
||||
* @param {OntimeEvent} event
|
||||
* @param {PlayableEvent} event
|
||||
* @return {boolean} success - whether an event was loaded
|
||||
*/
|
||||
@broadcastResult
|
||||
loadEvent(event: OntimeEvent): boolean {
|
||||
if (event.skip) {
|
||||
if (!isPlayableEvent(event)) {
|
||||
logger.warning(LogOrigin.Playback, `Refused skipped event with ID ${event.id}`);
|
||||
return false;
|
||||
}
|
||||
@@ -407,14 +409,16 @@ class RuntimeService {
|
||||
*/
|
||||
@broadcastResult
|
||||
start(): boolean {
|
||||
const state = runtimeState.getState();
|
||||
const canStart = validatePlayback(state.timer.playback).start;
|
||||
const previousState = runtimeState.getState();
|
||||
const canStart = validatePlayback(previousState.timer.playback).start;
|
||||
if (!canStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const didStart = this.eventTimer?.start() ?? false;
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${state.timer.playback.toUpperCase()}`);
|
||||
const newState = runtimeState.getState();
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
|
||||
|
||||
if (didStart) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
@@ -505,29 +509,56 @@ class RuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles special case to call roll on a loaded event which we do not want to discard
|
||||
*/
|
||||
rollLoaded() {
|
||||
const rundown = getRundown();
|
||||
try {
|
||||
this.eventTimer.roll(rundown);
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Server, `Roll: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback to roll
|
||||
*/
|
||||
@broadcastResult
|
||||
roll() {
|
||||
const beforeState = runtimeState.getState();
|
||||
const canRoll = validatePlayback(beforeState.timer.playback).roll;
|
||||
if (!canRoll) {
|
||||
roll(skipCheck: boolean = false) {
|
||||
const previousState = runtimeState.getState();
|
||||
if (!skipCheck) {
|
||||
const canRoll = validatePlayback(previousState.timer.playback).roll;
|
||||
if (!canRoll) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const rundown = getRundown();
|
||||
const result = this.eventTimer.roll(rundown);
|
||||
if (result.eventId !== previousState.eventNow?.id) {
|
||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onLoad);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.didStart) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(LogOrigin.Server, `Roll: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rundown = getRundown();
|
||||
const playableEvents = filterPlayable(rundown);
|
||||
if (playableEvents.length === 0) {
|
||||
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
||||
return;
|
||||
const newState = runtimeState.getState();
|
||||
|
||||
if (previousState.timer.playback !== newState.timer.playback) {
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
|
||||
}
|
||||
|
||||
this.eventTimer.roll(rundown);
|
||||
|
||||
const state = runtimeState.getState();
|
||||
const newState = state.timer.playback;
|
||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -549,7 +580,7 @@ class RuntimeService {
|
||||
// the db would have to change for the event not to exist
|
||||
// we do not know the reason for the crash, so we check anyway
|
||||
const event = getEventWithId(selectedEventId);
|
||||
if (!event || !isOntimeEvent(event)) {
|
||||
if (!isOntimeEvent(event) || !isPlayableEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -570,7 +601,7 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
// calculate at 30fps, refresh at 1fps
|
||||
const eventTimer = new TimerService({
|
||||
const eventTimer = new EventTimer({
|
||||
refresh: timerConfig.updateRate,
|
||||
updateInterval: timerConfig.notificationRate,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MaybeNumber, MaybeString, OntimeEvent, Playback, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { MaybeNumber, Playback, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
import { RuntimeState } from '../stores/runtimeState.js';
|
||||
|
||||
@@ -93,12 +93,11 @@ export function getCurrent(state: RuntimeState): number {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
||||
throw new Error('timerUtils.skippedOutOfEvent: invalid state received');
|
||||
}
|
||||
// we cant have skipped if we havent started
|
||||
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { startedAt, expectedFinish } = state.timer;
|
||||
const { clock } = state;
|
||||
|
||||
@@ -112,155 +111,6 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
||||
return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt);
|
||||
}
|
||||
|
||||
type RollTimers = {
|
||||
nowIndex: MaybeNumber;
|
||||
nowId: MaybeString;
|
||||
publicIndex: MaybeNumber;
|
||||
nextIndex: MaybeNumber;
|
||||
publicNextIndex: MaybeNumber;
|
||||
timeToNext: MaybeNumber;
|
||||
nextEvent: OntimeEvent | null;
|
||||
nextPublicEvent: OntimeEvent | null;
|
||||
currentEvent: OntimeEvent | null;
|
||||
currentPublicEvent: OntimeEvent | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds loading information given a current rundown and time
|
||||
* @param {OntimeEvent[]} playableEvents - List of playable events
|
||||
* @param {number} timeNow - time now in ms
|
||||
*/
|
||||
export const getRollTimers = (
|
||||
playableEvents: OntimeEvent[],
|
||||
timeNow: number,
|
||||
currentIndex?: number | null,
|
||||
): RollTimers => {
|
||||
let nowIndex: MaybeNumber = null; // index of event now
|
||||
let nowId: MaybeString = null; // id of event now
|
||||
let publicIndex: MaybeNumber = null; // index of public event now
|
||||
let nextIndex: MaybeNumber = null; // index of next event
|
||||
let publicNextIndex: MaybeNumber = null; // index of next public event
|
||||
let timeToNext: MaybeNumber = null; // counter: time for next event
|
||||
let publicTimeToNext: MaybeNumber = null; // counter: time for next public event
|
||||
|
||||
const hasLoaded = currentIndex !== null;
|
||||
const canFilter = hasLoaded && currentIndex === playableEvents.length - 1;
|
||||
const filteredRundown = canFilter ? playableEvents.slice(currentIndex) : playableEvents;
|
||||
|
||||
const lastEvent = filteredRundown.at(-1);
|
||||
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
||||
|
||||
let nextEvent: OntimeEvent | null = null;
|
||||
let nextPublicEvent: OntimeEvent | null = null;
|
||||
let currentEvent: OntimeEvent | null = null;
|
||||
let currentPublicEvent: OntimeEvent | null = null;
|
||||
|
||||
if (timeNow > lastNormalEnd) {
|
||||
// we are past last end
|
||||
// preload first and find next
|
||||
|
||||
const firstEvent = filteredRundown.at(0);
|
||||
nextIndex = 0;
|
||||
nextEvent = firstEvent;
|
||||
timeToNext = firstEvent.timeStart + dayInMs - timeNow;
|
||||
|
||||
if (firstEvent.isPublic) {
|
||||
nextPublicEvent = firstEvent;
|
||||
publicNextIndex = 0;
|
||||
} else {
|
||||
// look for next public
|
||||
// dev note: we feel that this is more efficient than filtering
|
||||
// since the next event will likely be close to the one playing
|
||||
for (const event of filteredRundown) {
|
||||
if (event.isPublic) {
|
||||
nextPublicEvent = event;
|
||||
// we need the index before this was sorted
|
||||
publicNextIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// flags: select first event if several overlapping
|
||||
let nowFound = false;
|
||||
// keep track of the end times when looking for public
|
||||
let publicTime = -1;
|
||||
|
||||
for (const event of filteredRundown) {
|
||||
// When does the event end (handle midnight)
|
||||
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
|
||||
|
||||
const hasNotEnded = normalEnd > timeNow;
|
||||
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
|
||||
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
|
||||
|
||||
if (normalEnd <= timeNow) {
|
||||
// event ran already
|
||||
|
||||
if (event.isPublic && normalEnd > publicTime) {
|
||||
// public event might not be the one running
|
||||
publicTime = normalEnd;
|
||||
currentPublicEvent = event;
|
||||
publicIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
} else if (hasNotEnded && hasStarted && !nowFound) {
|
||||
// event is running
|
||||
currentEvent = event;
|
||||
nowIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
nowId = event.id;
|
||||
nowFound = true;
|
||||
|
||||
// it could also be public
|
||||
if (event.isPublic) {
|
||||
publicTime = normalEnd;
|
||||
currentPublicEvent = event;
|
||||
publicIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
} else if (normalEnd > timeNow) {
|
||||
// event will run
|
||||
|
||||
// we already know whats next and next-public
|
||||
if (nextIndex !== null && publicNextIndex !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// look for next events
|
||||
// check how far the start is from now
|
||||
const timeToEventStart = event.timeStart - timeNow;
|
||||
|
||||
// we don't have a next or this one starts sooner than current next
|
||||
if (nextIndex === null || timeToEventStart < timeToNext) {
|
||||
timeToNext = timeToEventStart;
|
||||
nextEvent = event;
|
||||
nextIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
|
||||
if (event.isPublic) {
|
||||
// if we don't have a public next or this one start sooner than assigned next
|
||||
if (publicNextIndex === null || timeToEventStart < publicTimeToNext) {
|
||||
publicTimeToNext = timeToEventStart;
|
||||
nextPublicEvent = event;
|
||||
publicNextIndex = filteredRundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nowIndex,
|
||||
nowId,
|
||||
publicIndex,
|
||||
nextIndex,
|
||||
publicNextIndex,
|
||||
timeToNext,
|
||||
nextEvent,
|
||||
nextPublicEvent,
|
||||
currentEvent,
|
||||
currentPublicEvent,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates difference between the runtime and the schedule of an event
|
||||
* Positive offset is time ahead
|
||||
|
||||
Reference in New Issue
Block a user