mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 20:33:47 +00:00
@@ -42,8 +42,8 @@ import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||
import { restoreService } from './services/RestoreService.js';
|
||||
import { messageService } from './services/message-service/MessageService.js';
|
||||
import { populateDemo } from './modules/loadDemo.js';
|
||||
import { getState, updateNumEvents } from './stores/runtimeState.js';
|
||||
import { getNumEvents, setRundown } from './services/rundown-service/RundownService.js';
|
||||
import { getState, updateRundownData } from './stores/runtimeState.js';
|
||||
import { setRundown, getPlayableEvents } from './services/rundown-service/RundownService.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -184,8 +184,7 @@ export const startServer = async () => {
|
||||
setRundown(persistedRundown);
|
||||
|
||||
// TODO: do this on the init of the runtime service
|
||||
const numEvents = getNumEvents();
|
||||
updateNumEvents(numEvents);
|
||||
updateRundownData(getPlayableEvents());
|
||||
|
||||
// load restore point if it exists
|
||||
const maybeRestorePoint = await restoreService.load();
|
||||
|
||||
@@ -37,19 +37,6 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getIndexOf(eventId: string) {
|
||||
return data.rundown.findIndex((e) => e.id === eventId);
|
||||
}
|
||||
|
||||
static getRundownLength() {
|
||||
return data.rundown.length;
|
||||
}
|
||||
|
||||
static async clearRundown() {
|
||||
data.rundown = [];
|
||||
await db.write();
|
||||
}
|
||||
|
||||
static getSettings() {
|
||||
return data.settings;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type RestorePoint = {
|
||||
startedAt: MaybeNumber;
|
||||
addedTime: number;
|
||||
pausedAt: MaybeNumber;
|
||||
firstStart: MaybeNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,10 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof restorePoint.firstStart !== 'number' && restorePoint.pausedAt !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
startedAt: state.timer.startedAt,
|
||||
addedTime: state.timer.addedTime,
|
||||
pausedAt: state._timer.pausedAt,
|
||||
firstStart: state.runtime.actualStart,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('isRestorePoint()', () => {
|
||||
startedAt: 1,
|
||||
addedTime: 2,
|
||||
pausedAt: 3,
|
||||
firstStart: 1,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
|
||||
@@ -22,6 +23,7 @@ describe('isRestorePoint()', () => {
|
||||
startedAt: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
firstStart: 1,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
});
|
||||
@@ -68,6 +70,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: 1234,
|
||||
addedTime: 5678,
|
||||
pausedAt: 9087,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
@@ -84,6 +87,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
@@ -100,6 +104,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: 1234,
|
||||
addedTime: 1234,
|
||||
pausedAt: 1234,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
@@ -118,6 +123,7 @@ describe('RestoreService()', () => {
|
||||
startedAt: 1234,
|
||||
addedTime: 1234,
|
||||
pausedAt: 1234,
|
||||
firstStart: 1234,
|
||||
};
|
||||
|
||||
const restoreService = new RestoreService('/path/to/restore/file');
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getCurrent,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
normaliseEndTime,
|
||||
skippedOutOfEvent,
|
||||
updateRoll,
|
||||
@@ -1370,3 +1371,68 @@ describe('updateRoll()', () => {
|
||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuntimeOffset()', () => {
|
||||
it('calculates the difference between schedule and actual start', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 100,
|
||||
},
|
||||
timer: {
|
||||
startedAt: 150,
|
||||
addedTime: 10,
|
||||
current: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
const offset = getRuntimeOffset(state);
|
||||
expect(offset).toBe(60);
|
||||
});
|
||||
|
||||
it('adds the overtime time of the current timer', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 100,
|
||||
timeEnd: 140,
|
||||
},
|
||||
timer: {
|
||||
startedAt: 100,
|
||||
current: -10,
|
||||
addedTime: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: null,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
const offset = getRuntimeOffset(state);
|
||||
expect(offset).toBe(10);
|
||||
});
|
||||
|
||||
it('accounts for paused time', () => {
|
||||
const state = {
|
||||
eventNow: {
|
||||
id: '1',
|
||||
timeStart: 100,
|
||||
timeEnd: 150,
|
||||
},
|
||||
clock: 150,
|
||||
timer: {
|
||||
startedAt: 100,
|
||||
current: 25,
|
||||
addedTime: 0,
|
||||
},
|
||||
_timer: {
|
||||
pausedAt: 125,
|
||||
},
|
||||
} as RuntimeState;
|
||||
|
||||
const offset = getRuntimeOffset(state);
|
||||
expect(offset).toBe(25);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { block as blockDef, delay as delayDef } from '../../models/eventsDefinit
|
||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { createEvent } from '../../utils/parser.js';
|
||||
import { updateNumEvents } from '../../stores/runtimeState.js';
|
||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
|
||||
import * as cache from './rundownCache.js';
|
||||
@@ -159,8 +159,7 @@ export async function swapEvents(from: string, to: string) {
|
||||
* Called when we make changes to the rundown object
|
||||
*/
|
||||
function updateChangeNumEvents() {
|
||||
const numEvents = getPlayableEvents().length;
|
||||
updateNumEvents(numEvents);
|
||||
updateRundownData(getPlayableEvents());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,6 +285,10 @@ export function findNext(currentEventId?: string): OntimeEvent | null {
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the rundown with the given
|
||||
* @param rundown
|
||||
*/
|
||||
export async function setRundown(rundown: OntimeRundown) {
|
||||
cache.init(rundown);
|
||||
notifyChanges({ timer: true });
|
||||
|
||||
@@ -289,3 +289,23 @@ export const updateRoll = (state: RuntimeState) => {
|
||||
|
||||
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates difference between the runtime and the schedule of an event
|
||||
* @param state
|
||||
* @returns
|
||||
*/
|
||||
export function getRuntimeOffset(state: RuntimeState): number {
|
||||
if (state.eventNow === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { timeStart } = state.eventNow;
|
||||
const { addedTime, current, startedAt } = state.timer;
|
||||
|
||||
const overtime = Math.min(current, 0);
|
||||
const startOffset = startedAt - timeStart;
|
||||
const pausedTime = state._timer.pausedAt === null ? 0 : state.clock - state._timer.pausedAt;
|
||||
|
||||
return startOffset + addedTime + pausedTime + Math.abs(overtime);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { OntimeEvent, Playback } from 'ontime-types';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
import { RuntimeState, clear, getState, load, pause, start, stop } from '../runtimeState.js';
|
||||
import { RuntimeState, addTime, clear, getState, load, pause, start, stop } from '../runtimeState.js';
|
||||
|
||||
const mockEvent = {
|
||||
type: 'event',
|
||||
id: 'mock',
|
||||
cue: 'mock',
|
||||
timeStart: 0,
|
||||
@@ -88,6 +89,7 @@ describe('mutation on runtimeState', () => {
|
||||
expect(newState.timer).toMatchObject({
|
||||
playback: Playback.Play,
|
||||
});
|
||||
expect(newState.runtime.actualStart).toBe(newState.clock);
|
||||
|
||||
// 3. Pause event
|
||||
success = pause();
|
||||
@@ -122,7 +124,7 @@ describe('mutation on runtimeState', () => {
|
||||
);
|
||||
expect(newState._timer.pausedAt).toBeNull();
|
||||
|
||||
// 4. Stop event
|
||||
// 5. Stop event
|
||||
success = stop();
|
||||
expect(success).toBe(true);
|
||||
expect(newState.eventNow).toBe(null);
|
||||
@@ -133,8 +135,55 @@ describe('mutation on runtimeState', () => {
|
||||
expectedFinish: null,
|
||||
startedAt: null,
|
||||
});
|
||||
expect(newState.runtime.actualStart).toBeNull();
|
||||
});
|
||||
|
||||
test('runtime offset', () => {
|
||||
const event1 = { ...mockEvent, id: 'event1', timeStart: 0, timeEnd: 1000, duration: 1000 };
|
||||
const event2 = { ...mockEvent, id: 'event2', timeStart: 1000, timeEnd: 1500, duration: 500 };
|
||||
|
||||
// 1. Load event
|
||||
load(event1, [event1, event2]);
|
||||
let newState = getState();
|
||||
expect(newState.runtime.actualStart).toBeNull();
|
||||
expect(newState.runtime.plannedStart).toBe(0);
|
||||
expect(newState.runtime.plannedEnd).toBe(1500);
|
||||
|
||||
// 2. Start event
|
||||
start();
|
||||
newState = getState();
|
||||
const firstStart = newState.clock;
|
||||
expect(newState.runtime.actualStart).toBe(newState.clock);
|
||||
expect(newState.runtime.offset).toBe(newState.clock - event1.timeStart);
|
||||
expect(newState.runtime.expectedEnd).toBe(newState.runtime.offset + event2.timeEnd);
|
||||
|
||||
// 3. Next event
|
||||
load(event2, [event1, event2]);
|
||||
start();
|
||||
newState = getState();
|
||||
expect(newState.runtime.actualStart).toBe(firstStart);
|
||||
// we are over-under, the difference between the schedule and the actual start
|
||||
const delayBefore = newState.clock - event2.timeStart;
|
||||
expect(newState.runtime.offset).toBe(delayBefore);
|
||||
// finish is the difference between the runtime and the schedule
|
||||
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd + newState.runtime.offset);
|
||||
|
||||
// 4. Add time
|
||||
addTime(10);
|
||||
newState = getState();
|
||||
expect(newState.runtime.offset).toBe(delayBefore + 10);
|
||||
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd + newState.runtime.offset);
|
||||
|
||||
// 5. Stop event
|
||||
stop();
|
||||
newState = getState();
|
||||
expect(newState.runtime.actualStart).toBeNull();
|
||||
expect(newState.runtime.offset).toBe(0);
|
||||
expect(newState.runtime.expectedEnd).toBeNull();
|
||||
});
|
||||
|
||||
test.todo('runtime offset on timers in overtime', () => {});
|
||||
|
||||
test.todo('roll mode', () => {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { Runtime, OntimeEvent, Playback, TimerState, TimerType, MaybeNumber } from 'ontime-types';
|
||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||
import { calculateDuration, dayInMs, getFirstEvent, getLastEvent } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { RestorePoint } from '../services/RestoreService.js';
|
||||
import { getPlayableEvents } from '../services/rundown-service/RundownService.js';
|
||||
import { getCurrent, getExpectedFinish, getRollTimers, skippedOutOfEvent, updateRoll } from '../services/timerUtils.js';
|
||||
import {
|
||||
getCurrent,
|
||||
getExpectedFinish,
|
||||
getRollTimers,
|
||||
getRuntimeOffset,
|
||||
skippedOutOfEvent,
|
||||
updateRoll,
|
||||
} from '../services/timerUtils.js';
|
||||
import { timerConfig } from '../config/config.js';
|
||||
|
||||
const initialRuntime: Runtime = {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
offset: 0,
|
||||
plannedStart: 0,
|
||||
plannedEnd: 0,
|
||||
actualStart: null,
|
||||
expectedEnd: null,
|
||||
};
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
@@ -64,13 +76,13 @@ export function getState(): Readonly<RuntimeState> {
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
// TODO: check that entire state is reset here
|
||||
runtimeState.eventNow = null;
|
||||
runtimeState.publicEventNow = null;
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
|
||||
runtimeState.runtime = { ...initialRuntime };
|
||||
runtimeState.runtime = { ...initialRuntime, actualStart: runtimeState.runtime.actualStart };
|
||||
// TODO: can we cleanup the initialisation of runtime state?
|
||||
runtimeState.runtime.numEvents = fetchNumEvents();
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
@@ -106,11 +118,17 @@ function fetchNumEvents(): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility, allows updating the number of events
|
||||
* Utility, allows updating data derived from the rundown
|
||||
* @param numEvents
|
||||
*/
|
||||
export function updateNumEvents(numEvents: number) {
|
||||
runtimeState.runtime.numEvents = numEvents;
|
||||
export function updateRundownData(playableRundown: OntimeEvent[]) {
|
||||
runtimeState.runtime.numEvents = playableRundown.length;
|
||||
|
||||
const { firstEvent } = getFirstEvent(playableRundown);
|
||||
const { lastEvent } = getLastEvent(playableRundown);
|
||||
|
||||
runtimeState.runtime.plannedStart = firstEvent?.timeStart ?? null;
|
||||
runtimeState.runtime.plannedEnd = lastEvent?.timeEnd ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,9 +137,11 @@ export function updateNumEvents(numEvents: number) {
|
||||
* @param rundown
|
||||
* @param initialData
|
||||
*/
|
||||
export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState>) {
|
||||
export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState & RestorePoint>) {
|
||||
clear();
|
||||
|
||||
updateRundownData(rundown);
|
||||
|
||||
const eventIndex = rundown.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||
|
||||
runtimeState.runtime.selectedEventIndex = eventIndex;
|
||||
@@ -137,6 +157,13 @@ export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: P
|
||||
|
||||
if (initialData) {
|
||||
patchTimer(initialData);
|
||||
|
||||
const firstStart = initialData?.firstStart;
|
||||
if (firstStart === null || typeof firstStart === 'number') {
|
||||
runtimeState.runtime.actualStart = firstStart;
|
||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||
runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +283,15 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
||||
state.timer.playback = Playback.Play;
|
||||
state.timer.expectedFinish = getExpectedFinish(state);
|
||||
state.timer.elapsed = 0;
|
||||
|
||||
// update runtime delays: over - under
|
||||
if (state.runtime.actualStart === null) {
|
||||
state.runtime.actualStart = state.clock;
|
||||
}
|
||||
|
||||
state.runtime.offset = getRuntimeOffset(state);
|
||||
state.runtime.expectedEnd = state.runtime.plannedEnd + state.runtime.offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -274,6 +310,7 @@ export function stop(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.timer.playback === Playback.Stop) {
|
||||
return false;
|
||||
}
|
||||
runtimeState.runtime.actualStart = null;
|
||||
clear();
|
||||
return true;
|
||||
}
|
||||
@@ -298,6 +335,10 @@ export function addTime(amount: number) {
|
||||
runtimeState.timer.finishedAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
// update runtime delays: over - under
|
||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||
runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -318,6 +359,9 @@ export function update(force: boolean, updateInterval: number) {
|
||||
_force = true;
|
||||
}
|
||||
|
||||
// update offset
|
||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||
|
||||
// we call integrations if we update timers
|
||||
if (runtimeState.timer.playback === Playback.Roll) {
|
||||
const result = roll();
|
||||
|
||||
Reference in New Issue
Block a user