import { deepEqual } from 'fast-equals'; import { EntryId, MaybeNumber, Offset, OntimeEvent, Rundown, TimerState, TimerType, isOntimeEvent, isPlayableEvent, } from 'ontime-types'; import { millisToSeconds } from 'ontime-utils'; export function isNewSecond( previousValue: MaybeNumber | undefined, currentValue: MaybeNumber | undefined, direction: TimerType.CountDown | TimerType.CountUp = TimerType.CountDown, ) { return millisToSeconds(currentValue ?? null, direction) !== millisToSeconds(previousValue ?? null, direction); } /** * Checks whether we should update the clock value * - we have rolled into a new seconds unit */ export function getShouldClockUpdate(previousUpdate: number, now: number): boolean { const newSeconds = millisToSeconds(now, TimerType.CountUp) !== millisToSeconds(previousUpdate, TimerType.CountUp); return newSeconds; } /** * Checks whether we should update the timer values * - `current` and `secondaryTimer` trigger on seconds roll over * - the rest trigger on any change * - `elapsed` and `expectedFinish` is not checked */ export function getShouldTimerUpdate(previousValue: TimerState | undefined, currentValue: TimerState): boolean { if (previousValue === undefined) return true; return ( // current timer value isNewSecond(previousValue.current, currentValue.current) || //secondary timer value, when in pre-roll isNewSecond(previousValue.secondaryTimer, currentValue.secondaryTimer) || // other timer values that could have changed previousValue.addedTime !== currentValue.addedTime || previousValue.duration !== currentValue.duration || previousValue.phase !== currentValue.phase || previousValue.playback !== currentValue.playback || previousValue.startedAt !== currentValue.startedAt // elapsed - this would be the direct invert of current value so no need to check // expectedFinish - this will be moved out by the current value going into over time, no need to check ); } /** * Checks whether we should update the offset values * - `mode` triggers update * - `absolute`, `relative`, `expected**End` are ticked with `didDependencyUpdate` */ export function getShouldOffsetUpdate( previousValue: Offset | undefined, currentValue: Offset, didDependencyUpdate: boolean, ): boolean { if (previousValue === undefined) return true; if (previousValue.mode !== currentValue.mode) return true; return didDependencyUpdate && !deepEqual(previousValue, currentValue); } /** * finds the previous playable event, if it exists */ export function findPreviousPlayableId(playableEventsOrder: EntryId[], currentEventId?: string): EntryId | undefined { if (!playableEventsOrder.length) { return; } // if there is no event running, go to first if (!currentEventId) { return getFirstPlayableId(playableEventsOrder); } const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId); if (currentIndex < 1) { return getFirstPlayableId(playableEventsOrder); } return playableEventsOrder.at(currentIndex - 1); } /** * finds the next event playable event, if it exists */ export function findNextPlayableId(playableEventsOrder: EntryId[], currentEventId?: string): EntryId | undefined { if (!playableEventsOrder.length) { return; } // if there is no event running, go to first if (!currentEventId) { return getFirstPlayableId(playableEventsOrder); } const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId); if (currentIndex === -1 || currentIndex >= playableEventsOrder.length - 1) { return getFirstPlayableId(playableEventsOrder); } return playableEventsOrder.at(currentIndex + 1); } /** * returns next event that matches a given cue * then loops from the start */ export function findNextPlayableWithCue( rundown: Rundown, timedEventsOrder: EntryId[], targetCue: string, currentEventIndex = 0, allowCurrent = false, ): OntimeEvent | undefined { const startFromIndex = allowCurrent ? currentEventIndex : currentEventIndex + 1; for (let i = startFromIndex; i < timedEventsOrder.length; i++) { const eventId = timedEventsOrder[i]; const event = rundown.entries[eventId]; if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) { return event; } } for (let i = 0; i < startFromIndex; i++) { const eventId = timedEventsOrder[i]; const event = rundown.entries[eventId]; if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) { return event; } } } /** * Utility returns the first playable event in rundown, if it exists */ export function getFirstPlayableId(playableOrder: EntryId[]): EntryId | undefined { return playableOrder.at(0); } /** * This is a utility function to return an event at a given index * It uses the timedEventOrder so that the index is the same as the one in the UI */ export function getEventAtIndex( rundown: Rundown, timedEventOrder: EntryId[], eventIndex: number, ): OntimeEvent | undefined { const eventId = timedEventOrder[eventIndex]; if (!eventId) { return undefined; } return rundown.entries[eventId] as OntimeEvent | undefined; }