Files
ontime/apps/server/src/services/timerUtils.ts
T
Carlos Valente a2950442d8 refactor: runtime service (#715)
* refactor: rename PlaybackService > RuntimeService

* refactor: stateful runtime service

* refactor: merge event loading

* refactor: roll is part of state mutation
2024-01-16 19:54:40 +01:00

266 lines
8.8 KiB
TypeScript

import { MaybeNumber, OntimeEvent, TimerType } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { TState } from '../state.js';
import { sortArrayByProperty } from '../utils/arrayUtils.js';
/**
* handle events that span over midnight
*/
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
/**
* Calculates expected finish time of a running timer
* @param {TState} state runtime state
* @returns {number | null} new current time or null if nothing is running
*/
export function getExpectedFinish(state: TState): MaybeNumber {
const { startedAt, clock, finishedAt, duration, addedTime, timerType, pausedAt } = state.timer;
const { timeEnd } = state.eventNow;
if (startedAt === null) {
return null;
}
if (finishedAt !== null) {
return finishedAt;
}
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
if (timerType === TimerType.TimeToEnd) {
return timeEnd + addedTime + pausedTime;
}
// handle events that finish the day after
const expectedFinish = startedAt + duration + addedTime + pausedTime;
if (expectedFinish > dayInMs) {
return expectedFinish - dayInMs;
}
// an event cannot finish before it started (user added too much negative time)
return Math.max(expectedFinish, startedAt);
}
/**
* Calculates running countdown
* @param {TState} state runtime state
* @returns {number} current time for timer
*/
export function getCurrent(state: TState): number {
const { startedAt, duration, addedTime, clock, timerType, pausedAt } = state.timer;
const { timeEnd } = state.eventNow;
if (timerType === TimerType.TimeToEnd) {
const isNextDay = startedAt > timeEnd;
const correctDay = isNextDay ? dayInMs : 0;
return timeEnd + addedTime + correctDay - clock;
}
if (startedAt === null) {
return duration;
}
const hasPassedMidnight = startedAt > clock;
const correctDay = hasPassedMidnight ? dayInMs : 0;
if (pausedAt != null) {
return startedAt + duration + addedTime - pausedAt;
}
return startedAt + duration + addedTime - clock - correctDay;
}
/**
* Checks whether we have skipped out of the event
* @param {TState} state runtime state
* @param {number} previousTime previous clock
* @param {number} skipLimit how much time can we skip
* @returns {boolean}
*/
export function skippedOutOfEvent(state: TState, previousTime: number, skipLimit: number): boolean {
const { clock, startedAt, expectedFinish } = state.timer;
const hasPassedMidnight = previousTime > dayInMs - skipLimit && clock < skipLimit;
const adjustedClock = hasPassedMidnight ? clock + dayInMs : clock;
const timeDifference = previousTime - adjustedClock;
const hasSkipped = Math.abs(timeDifference) > skipLimit;
const adjustedExpectedFinish = expectedFinish >= startedAt ? expectedFinish : expectedFinish + dayInMs;
return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt);
}
/**
* Finds loading information given a current rundown and time
* @param {OntimeEvent[]} rundown - List of playable events
* @param {number} timeNow - time now in ms
* @returns {{}}
*/
export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
let nowIndex: number | null = null; // index of event now
let nowId: string | null = null; // id of event now
let publicIndex: number | null = null; // index of public event now
let nextIndex: number | null = null; // index of next event
let publicNextIndex: number | null = null; // index of next public event
let timeToNext: number | null = null; // counter: time for next event
let publicTimeToNext: number | null = null; // counter: time for next public event
const orderedEvents = sortArrayByProperty(rundown, 'timeStart');
const lastEvent = orderedEvents[orderedEvents.length - 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 = orderedEvents[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 orderedEvents) {
if (event.isPublic) {
nextPublicEvent = event;
// we need the index before this was sorted
publicNextIndex = rundown.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 orderedEvents) {
// 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 = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
} else if (hasNotEnded && hasStarted && !nowFound) {
// event is running
currentEvent = event;
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
nowId = event.id;
nowFound = true;
// it could also be public
if (event.isPublic) {
publicTime = normalEnd;
currentPublicEvent = event;
publicIndex = rundown.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 = rundown.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 = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
}
}
}
}
return {
nowIndex,
nowId,
publicIndex,
nextIndex,
publicNextIndex,
timeToNext,
nextEvent,
nextPublicEvent,
currentEvent,
currentPublicEvent,
};
};
/**
* @description Implements update functions for roll mode
* @param {TState}
* @returns object with selection variables
*/
export const updateRoll = (state: TState) => {
const { selectedEventId } = state.runtime;
const { current, expectedFinish, startedAt, clock, secondaryTimer, secondaryTarget } = state.timer;
// timers
let updatedTimer = current;
let updatedSecondaryTimer = secondaryTimer;
// whether rollLoad should be called: force reload of events
let doRollLoad = false;
// whether finished event should trigger
let isPrimaryFinished = false;
if (selectedEventId && current !== null) {
// if we have something selected and a timer, we are running
const finishAt = expectedFinish >= startedAt ? expectedFinish : expectedFinish + dayInMs;
updatedTimer = finishAt - clock;
if (updatedTimer > dayInMs) {
updatedTimer -= dayInMs;
}
if (updatedTimer < 0) {
isPrimaryFinished = true;
// we need a new event
doRollLoad = true;
}
} else if (secondaryTimer >= 0) {
// if secondaryTimer is running we are in waiting to roll
updatedSecondaryTimer = secondaryTarget - clock;
if (updatedSecondaryTimer <= 0) {
// we need a new event
doRollLoad = true;
}
}
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished };
};