mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 15:33:59 +00:00
211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
import { MaybeNumber, TimerPhase } from 'ontime-types';
|
|
import { dayInMs, isPlaybackActive } from 'ontime-utils';
|
|
import type { RuntimeState } from '../stores/runtimeState.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 {RuntimeState} state runtime state
|
|
* @returns {number | null} new current time or null if nothing is running
|
|
*/
|
|
export function getExpectedFinish(state: RuntimeState): MaybeNumber {
|
|
const { startedAt, finishedAt, duration, addedTime } = state.timer;
|
|
|
|
if (state.eventNow === null) {
|
|
return null;
|
|
}
|
|
|
|
const { countToEnd, timeEnd } = state.eventNow;
|
|
const { pausedAt } = state._timer;
|
|
const { clock } = state;
|
|
|
|
if (startedAt === null) {
|
|
return null;
|
|
}
|
|
|
|
if (finishedAt !== null) {
|
|
return finishedAt;
|
|
}
|
|
|
|
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
|
|
|
|
if (countToEnd) {
|
|
return timeEnd + addedTime + pausedTime;
|
|
}
|
|
|
|
// handle events that finish the day after
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer
|
|
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 {RuntimeState} state runtime state
|
|
* @returns {number} current time for timer
|
|
*/
|
|
|
|
export function getCurrent(state: RuntimeState): number {
|
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
|
DEV: {
|
|
if (state.eventNow === null || state.timer.duration === null) {
|
|
throw new Error('timerUtils.getCurrent: invalid state received');
|
|
}
|
|
}
|
|
const { startedAt, duration, addedTime } = state.timer;
|
|
const { countToEnd, timeStart, timeEnd } = state.eventNow;
|
|
const { pausedAt } = state._timer;
|
|
const { clock } = state;
|
|
|
|
if (countToEnd) {
|
|
const isEventOverMidnight = timeStart > timeEnd;
|
|
const correctDay = isEventOverMidnight ? dayInMs : 0;
|
|
return correctDay - clock + timeEnd + addedTime;
|
|
}
|
|
|
|
if (startedAt === null) {
|
|
return duration;
|
|
}
|
|
|
|
if (pausedAt != null) {
|
|
return startedAt + duration + addedTime - pausedAt;
|
|
}
|
|
|
|
const hasPassedMidnight = startedAt > clock;
|
|
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
|
return startedAt + duration + addedTime - clock - correctDay;
|
|
}
|
|
|
|
/**
|
|
* Checks whether we have skipped out of the event
|
|
* @param {RuntimeState} state runtime state
|
|
* @param {number} previousTime previous clock
|
|
* @param {number} skipLimit how much time can we skip
|
|
* @returns {boolean}
|
|
*/
|
|
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
|
// 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;
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Calculates difference between the runtime and the schedule of an event
|
|
* Positive offset is time ahead
|
|
* Negative offset is time delayed
|
|
*/
|
|
export function getRuntimeOffset(state: RuntimeState): { absoluteOffset: number; relativeOffset: number } {
|
|
// nothing to calculate if there are no loaded events or if we havent started
|
|
if (state.eventNow === null || state.runtime.actualStart === null) {
|
|
return { absoluteOffset: 0, relativeOffset: 0 };
|
|
}
|
|
|
|
const { clock } = state;
|
|
const { countToEnd, timeStart } = state.eventNow;
|
|
const { addedTime, current, startedAt } = state.timer;
|
|
const { actualStart, plannedStart } = state.runtime;
|
|
|
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
|
DEV: {
|
|
// we know current exists as long as eventNow exists
|
|
if (current === null) throw new Error('timerUtils.getRuntimeOffset: state.timer.current must be set');
|
|
if (plannedStart === null) throw new Error('timerUtils.getRuntimeOffset: state.runtime.plannedStart must be set');
|
|
}
|
|
|
|
// if we havent started, but the timer is armed
|
|
// the offset is the difference to the schedule
|
|
if (startedAt === null) {
|
|
return { absoluteOffset: timeStart - clock, relativeOffset: 0 };
|
|
}
|
|
|
|
const overtime = Math.min(current, 0);
|
|
// in time-to-end, offset is overtime
|
|
|
|
const startOffset = timeStart - startedAt;
|
|
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
|
|
|
// startOffset - difference between scheduled start and actual start
|
|
// addedTime - time added by user (negative offset)
|
|
// pausedTime - time the playback was paused (negative offset)
|
|
// overtime - how long the timer has been over-running (negative offset)
|
|
const offset = startOffset - addedTime - pausedTime + overtime;
|
|
|
|
// offset between planned rundown start and actual rundown start
|
|
const rundownStartOffset = actualStart - plannedStart;
|
|
|
|
// offset offset relative to the actual rundown start
|
|
const relativeOffset = offset + rundownStartOffset;
|
|
|
|
// in time-to-end, offset is overtime
|
|
if (countToEnd) {
|
|
return { absoluteOffset: overtime, relativeOffset };
|
|
}
|
|
|
|
return { absoluteOffset: offset, relativeOffset };
|
|
}
|
|
|
|
/**
|
|
* Calculates the expected end of the rundown
|
|
*/
|
|
export function getExpectedEnd(state: RuntimeState): MaybeNumber {
|
|
// there is no expected end if we havent started
|
|
if (state.runtime.actualStart === null || state.runtime.plannedEnd === null) {
|
|
return null;
|
|
}
|
|
return state.runtime.plannedEnd - state.runtime.offset + state._rundown.totalDelay;
|
|
}
|
|
|
|
/**
|
|
* Checks running timer to see which phase it currently is in
|
|
* @param state
|
|
*/
|
|
export function getTimerPhase(state: RuntimeState): TimerPhase {
|
|
if (!isPlaybackActive(state.timer.playback)) {
|
|
return TimerPhase.None;
|
|
}
|
|
|
|
const current = state.timer.current;
|
|
|
|
if (current === null || state.eventNow === null || state.timer.secondaryTimer != null) {
|
|
return TimerPhase.Pending;
|
|
}
|
|
|
|
if (current < 0) {
|
|
return TimerPhase.Overtime;
|
|
}
|
|
|
|
const danger = state.eventNow.timeDanger;
|
|
if (current <= danger) {
|
|
return TimerPhase.Danger;
|
|
}
|
|
|
|
const warning = state.eventNow.timeWarning;
|
|
if (current <= warning) {
|
|
return TimerPhase.Warning;
|
|
}
|
|
|
|
return TimerPhase.Default;
|
|
}
|