mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
Feat: Time until group and flag (#1708)
* refactor: split event data and state data * refactor: calculate the start time insted of until * cleanup * send next flag expected start from server * refactor: render * fix test * use same icon * refactor: group duration * Optimize (#1711) * refactor: consolidate block, flag and end loading and expected times * work on test * cal end value * lint * fix test * remove todo
This commit is contained in:
committed by
GitHub
parent
e37d2ce50f
commit
35347e8d63
@@ -151,7 +151,8 @@ export const useClock = createSelector((state: RuntimeStore) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const useNextFlag = createSelector((state: RuntimeStore) => ({
|
export const useNextFlag = createSelector((state: RuntimeStore) => ({
|
||||||
nextFlag: state.nextFlag,
|
id: state.nextFlag?.id ?? null,
|
||||||
|
expectedStart: state.nextFlag?.expectedStart ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/** Used by the progress bar components */
|
/** Used by the progress bar components */
|
||||||
@@ -177,7 +178,6 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) =
|
|||||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||||
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel,
|
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel,
|
||||||
|
|
||||||
blockStartedAt: state.blockNow?.startedAt ?? null,
|
|
||||||
blockExpectedEnd: state.blockNow?.expectedEnd ?? null,
|
blockExpectedEnd: state.blockNow?.expectedEnd ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -186,13 +186,13 @@ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
|||||||
offset: state.runtime.offsetAbs,
|
offset: state.runtime.offsetAbs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useTimeUntilData = createSelector((state: RuntimeStore) => ({
|
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
|
||||||
clock: state.clock,
|
|
||||||
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel,
|
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel,
|
||||||
offsetMode: state.runtime.offsetMode,
|
offsetMode: state.runtime.offsetMode,
|
||||||
currentDay: state.eventNow?.dayOffset ?? 0,
|
currentDay: state.eventNow?.dayOffset ?? 0,
|
||||||
actualStart: state.runtime.actualStart,
|
actualStart: state.runtime.actualStart,
|
||||||
plannedStart: state.runtime.plannedStart,
|
plannedStart: state.runtime.plannedStart,
|
||||||
|
clock: state.clock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useCurrentDay = createSelector((state: RuntimeStore) => ({
|
export const useCurrentDay = createSelector((state: RuntimeStore) => ({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MaybeNumber, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
import { MaybeNumber, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
calculateTimeUntilStart,
|
|
||||||
formatFromMillis,
|
formatFromMillis,
|
||||||
|
getExpectedStart,
|
||||||
MILLIS_PER_HOUR,
|
MILLIS_PER_HOUR,
|
||||||
MILLIS_PER_MINUTE,
|
MILLIS_PER_MINUTE,
|
||||||
MILLIS_PER_SECOND,
|
MILLIS_PER_SECOND,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||||
import { APP_SETTINGS } from '../api/constants';
|
import { APP_SETTINGS } from '../api/constants';
|
||||||
import { useTimeUntilData } from '../hooks/useSocket';
|
import { useExpectedStartData } from '../hooks/useSocket';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,18 +133,24 @@ export function formatDuration(duration: number, hideSeconds = true): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param totalGap accumulated gap from the current event
|
* @param totalGap accumulated gap from the current event
|
||||||
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
|
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useTimeUntilStart(
|
export function useTimeUntilExpectedStart(
|
||||||
// typed like this to make it very clear what the data is
|
// typed like this to make it very clear what the data is
|
||||||
data: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'> & {
|
event: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'> | null,
|
||||||
|
state: {
|
||||||
totalGap: number;
|
totalGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
},
|
},
|
||||||
): number {
|
): number {
|
||||||
const { offset, clock, currentDay, offsetMode, actualStart, plannedStart } = useTimeUntilData();
|
const { offset, currentDay, offsetMode, actualStart, plannedStart, clock } = useExpectedStartData();
|
||||||
return calculateTimeUntilStart({ ...data, currentDay, clock, offset, offsetMode, actualStart, plannedStart });
|
if (event === null) return 0;
|
||||||
|
|
||||||
|
const expectedStart = getExpectedStart(
|
||||||
|
{ ...event },
|
||||||
|
{ ...state, currentDay, offset, offsetMode, actualStart, plannedStart },
|
||||||
|
);
|
||||||
|
return expectedStart - clock;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString } from 'ontime-uti
|
|||||||
|
|
||||||
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import { formatDuration, useTimeUntilStart } from '../../../common/utils/time';
|
import { formatDuration, useTimeUntilExpectedStart } from '../../../common/utils/time';
|
||||||
import RunningTime from '../../viewers/common/running-time/RunningTime';
|
import RunningTime from '../../viewers/common/running-time/RunningTime';
|
||||||
import type { EditEvent, Subscribed } from '../operator.types';
|
import type { EditEvent, Subscribed } from '../operator.types';
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ interface TimeUntilProps {
|
|||||||
}
|
}
|
||||||
function TimeUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: TimeUntilProps) {
|
function TimeUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: TimeUntilProps) {
|
||||||
// we isolate this to avoid unnecessary re-renders
|
// we isolate this to avoid unnecessary re-renders
|
||||||
const timeUntil = useTimeUntilStart({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded });
|
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
|
||||||
|
|
||||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||||
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { TbCalendar, TbCalendarClock, TbCalendarDown, TbCalendarStar, TbFlagDown, TbFlagStar } from 'react-icons/tb';
|
import {
|
||||||
import { isOntimeBlock, OntimeBlock, OntimeEvent, TimerPhase, TimerType } from 'ontime-types';
|
TbCalendarClock,
|
||||||
|
TbCalendarPin,
|
||||||
|
TbCalendarStar,
|
||||||
|
TbFlagPin,
|
||||||
|
TbFlagStar,
|
||||||
|
TbFolderPin,
|
||||||
|
TbFolderStar,
|
||||||
|
} from 'react-icons/tb';
|
||||||
|
import { OntimeBlock, OntimeEvent, TimerPhase, TimerType } from 'ontime-types';
|
||||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||||
@@ -15,7 +23,7 @@ import {
|
|||||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||||
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
||||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime, useTimeUntilStart } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
|
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
|
||||||
|
|
||||||
import { OverUnder, TimeColumn } from './TimeLayout';
|
import { OverUnder, TimeColumn } from './TimeLayout';
|
||||||
@@ -36,7 +44,7 @@ export function StartTimes() {
|
|||||||
<div className={style.row}>
|
<div className={style.row}>
|
||||||
<span className={style.label}>Start</span>
|
<span className={style.label}>Start</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Planned start time' render={<TbCalendar className={style.icon} />} />
|
<Tooltip text='Planned start time' render={<TbCalendarPin className={style.icon} />} />
|
||||||
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
|
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
@@ -47,7 +55,7 @@ export function StartTimes() {
|
|||||||
<div className={style.row}>
|
<div className={style.row}>
|
||||||
<span className={style.label}>End</span>
|
<span className={style.label}>End</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Planned end time' render={<TbCalendar className={style.icon} />} />
|
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
|
||||||
{maybePlannedDaySpan > 0 ? (
|
{maybePlannedDaySpan > 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
|
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
|
||||||
@@ -88,50 +96,31 @@ export function MetadataTimes() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: there a some things here we still need to think about, mainly what to do whit the planed group duration in relation to the events
|
||||||
function GroupTimes() {
|
function GroupTimes() {
|
||||||
const { blockStartedAt, clock, blockExpectedEnd } = useRuntimePlaybackOverview();
|
const { clock, blockExpectedEnd } = useRuntimePlaybackOverview();
|
||||||
const { currentBlockId } = useCurrentBlockId();
|
const { currentBlockId } = useCurrentBlockId();
|
||||||
const entry = useEntry(currentBlockId);
|
const group = useEntry(currentBlockId) as OntimeBlock | null;
|
||||||
|
|
||||||
if (!currentBlockId) {
|
// the group end time dose not encode any day offsets
|
||||||
return (
|
const plannedGroupEnd = group && group.timeStart !== null ? group.timeStart + group.duration - clock : null;
|
||||||
<div className={style.metadataRow}>
|
const plannedTimeUntilGroupEnd = formattedTime(plannedGroupEnd, 3, TimerType.CountDown);
|
||||||
<span className={style.label}>Group</span>
|
|
||||||
<div className={style.labelledElement}>
|
|
||||||
<Tooltip text='Time to scheduled group end' render={<TbCalendarDown className={style.icon} />} />
|
|
||||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
|
||||||
</div>
|
|
||||||
<div className={style.labelledElement}>
|
|
||||||
<Tooltip text='Time to expected group end' render={<TbCalendarStar className={style.icon} />} />
|
|
||||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const remainingBlockDuration = (() => {
|
const expectedGroupEnd = blockExpectedEnd !== null ? blockExpectedEnd - clock : null;
|
||||||
if (blockStartedAt === null || !entry) return timerPlaceholder;
|
const expectedTimeUntilGroupEnd = formattedTime(expectedGroupEnd, 3, TimerType.CountDown);
|
||||||
if (!isOntimeBlock(entry)) return timerPlaceholder;
|
|
||||||
return formattedTime(blockStartedAt + entry.duration - clock, 3, TimerType.CountDown);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const timeUntilBlockEnd = (() => {
|
const groupTitle = group?.title ?? null;
|
||||||
if (blockExpectedEnd === null) return timerPlaceholder;
|
|
||||||
return formattedTime(blockExpectedEnd - clock, 3, TimerType.CountDown);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const groupTitle = (entry as OntimeBlock | null)?.title || 'Group';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.metadataRow}>
|
<div className={style.metadataRow}>
|
||||||
<span className={style.labelTitle}>{groupTitle}</span>
|
<span className={groupTitle ? style.labelTitle : style.label}>{`${groupTitle ? groupTitle : 'Group'} `}</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to scheduled group end' render={<TbCalendarDown className={style.icon} />} />
|
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
|
||||||
<span className={cx([style.time, blockStartedAt === null && style.muted])}>{remainingBlockDuration}</span>
|
<span className={cx([style.time, !group && style.muted])}>{plannedTimeUntilGroupEnd}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to expected group end' render={<TbCalendarStar className={style.icon} />} />
|
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
|
||||||
<span className={cx([style.time, blockExpectedEnd === null && style.muted])}>{timeUntilBlockEnd}</span>
|
<span className={cx([style.time, blockExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -139,50 +128,27 @@ function GroupTimes() {
|
|||||||
|
|
||||||
function FlagTimes() {
|
function FlagTimes() {
|
||||||
const { clock } = useClock();
|
const { clock } = useClock();
|
||||||
const { nextFlag } = useNextFlag();
|
const { id, expectedStart } = useNextFlag();
|
||||||
const entry = useEntry(nextFlag?.id ?? null);
|
const entry = useEntry(id) as OntimeEvent | null;
|
||||||
|
|
||||||
// TODO(v4): can we make a good approximation of time until next flag?
|
const plannedFlagStart = entry ? entry.timeStart - clock : null;
|
||||||
const timeUntil = useTimeUntilStart({
|
const plannedTimeUntilDisplay = formattedTime(plannedFlagStart, 3, TimerType.CountDown);
|
||||||
timeStart: nextFlag?.start ?? 0,
|
|
||||||
delay: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
isLinkedToLoaded: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!nextFlag) {
|
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
|
||||||
return (
|
const expectedTimeUntilDisplay = formattedTime(expectedTimeUntil, 3, TimerType.CountDown);
|
||||||
<div className={style.metadataRow}>
|
|
||||||
<span className={style.label}>Flag</span>
|
|
||||||
<div className={style.labelledElement}>
|
|
||||||
<Tooltip text='Time to next flag scheduled start' render={<TbFlagDown className={style.icon} />} />
|
|
||||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
|
||||||
</div>
|
|
||||||
<div className={style.labelledElement}>
|
|
||||||
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
|
||||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const muted = nextFlag === null;
|
const title = entry?.title ?? null;
|
||||||
const flagTitle = (entry as OntimeEvent | null)?.title || 'Flag';
|
|
||||||
const timeToNextFlag = nextFlag.start - clock;
|
|
||||||
const display = millisToString(timeToNextFlag, { fallback: timerPlaceholder });
|
|
||||||
const timeUntilDisplay = millisToString(timeUntil, { fallback: timerPlaceholder });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.metadataRow}>
|
<div className={style.metadataRow}>
|
||||||
<span className={cx([style.labelTitle])}>{flagTitle}</span>
|
<span className={title ? style.labelTitle : style.label}>{`${title ? title : 'Flag'} `}</span>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to next flag scheduled start' render={<TbFlagDown className={style.icon} />} />
|
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
|
||||||
<span className={cx([style.time])}>{display}</span>
|
<span className={cx([style.time, !entry && style.muted])}>{plannedTimeUntilDisplay}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.labelledElement}>
|
<div className={style.labelledElement}>
|
||||||
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
||||||
<span className={cx([style.time, muted && style.muted])}>{timeUntilDisplay}</span>
|
<span className={cx([style.time, expectedTimeUntil === null && style.muted])}>{expectedTimeUntilDisplay}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface RundownBlockProps {
|
|||||||
onCollapse: (collapsed: boolean, groupId: EntryId) => void;
|
onCollapse: (collapsed: boolean, groupId: EntryId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: the block should maybe include a multiple day indicator
|
||||||
export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: RundownBlockProps) {
|
export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: RundownBlockProps) {
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
const { clone, ungroup, deleteEntry } = useEntryActions();
|
const { clone, ungroup, deleteEntry } = useEntryActions();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
|||||||
import { usePlayback } from '../../../../common/hooks/useSocket';
|
import { usePlayback } from '../../../../common/hooks/useSocket';
|
||||||
import useReport from '../../../../common/hooks-query/useReport';
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { formatDuration, useTimeUntilStart } from '../../../../common/utils/time';
|
import { formatDuration, useTimeUntilExpectedStart } from '../../../../common/utils/time';
|
||||||
|
|
||||||
import style from './RundownEventChip.module.scss';
|
import style from './RundownEventChip.module.scss';
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ interface EventUntilProps {
|
|||||||
function EventUntil(props: EventUntilProps) {
|
function EventUntil(props: EventUntilProps) {
|
||||||
const { timeStart, delay, dayOffset, totalGap, isLinkedToLoaded } = props;
|
const { timeStart, delay, dayOffset, totalGap, isLinkedToLoaded } = props;
|
||||||
|
|
||||||
const timeUntil = useTimeUntilStart({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded });
|
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
|
||||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||||
|
|
||||||
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
||||||
|
|||||||
@@ -723,11 +723,11 @@ export function processRundown(
|
|||||||
// if the event is a block, we process the nested entries
|
// if the event is a block, we process the nested entries
|
||||||
// the code here is a copy of the processing of top level events
|
// the code here is a copy of the processing of top level events
|
||||||
if (isOntimeBlock(processedEntry)) {
|
if (isOntimeBlock(processedEntry)) {
|
||||||
let totalBlockDuration = 0;
|
|
||||||
let blockStartTime = null;
|
let blockStartTime = null;
|
||||||
let blockEndTime = null;
|
let blockEndTime = null;
|
||||||
let isFirstLinked = false;
|
let isFirstLinked = false;
|
||||||
const blockEvents: EntryId[] = [];
|
const blockEvents: EntryId[] = [];
|
||||||
|
processedEntry.duration = 0;
|
||||||
|
|
||||||
// check if the block contains nested entries
|
// check if the block contains nested entries
|
||||||
for (let j = 0; j < processedEntry.entries.length; j++) {
|
for (let j = 0; j < processedEntry.entries.length; j++) {
|
||||||
@@ -739,10 +739,7 @@ export function processRundown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockEvents.push(nestedEntry.id);
|
blockEvents.push(nestedEntry.id);
|
||||||
const { processedData: processedNestedData, processedEntry: processedNestedEntry } = process(
|
const { processedEntry: processedNestedEntry } = process(nestedEntry, processedEntry.id);
|
||||||
nestedEntry,
|
|
||||||
processedEntry.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// we dont extract metadata of skipped events,
|
// we dont extract metadata of skipped events,
|
||||||
// if this is not a playable event there is nothing else to do
|
// if this is not a playable event there is nothing else to do
|
||||||
@@ -757,12 +754,14 @@ export function processRundown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lastEntry is the event with the latest end time
|
// lastEntry is the event with the latest end time
|
||||||
blockEndTime = processedNestedData.lastEnd;
|
blockEndTime = processedNestedEntry.timeEnd;
|
||||||
totalBlockDuration += processedNestedEntry.duration;
|
if (j > 0) {
|
||||||
|
processedEntry.duration += processedNestedEntry.gap;
|
||||||
|
}
|
||||||
|
processedEntry.duration = processedEntry.duration + processedNestedEntry.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update block metadata
|
// update block metadata
|
||||||
processedEntry.duration = totalBlockDuration;
|
|
||||||
processedEntry.timeStart = blockStartTime;
|
processedEntry.timeStart = blockStartTime;
|
||||||
processedEntry.timeEnd = blockEndTime;
|
processedEntry.timeEnd = blockEndTime;
|
||||||
processedEntry.isFirstLinked = isFirstLinked;
|
processedEntry.isFirstLinked = isFirstLinked;
|
||||||
|
|||||||
@@ -715,14 +715,14 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock)) &&
|
getForceUpdate(RuntimeService.previousRuntimeUpdate, state.clock)) &&
|
||||||
!deepEqual(RuntimeService.previousState?.runtime, state.runtime);
|
!deepEqual(RuntimeService.previousState?.runtime, state.runtime);
|
||||||
|
|
||||||
/**
|
// TODO: the value shows up one tick to late
|
||||||
* the currentBlock object has the potential to tick on expected end
|
|
||||||
* TODO: the value shows up one tick to late
|
|
||||||
*/
|
|
||||||
const shouldBlockUpdate =
|
const shouldBlockUpdate =
|
||||||
!deepEqual(RuntimeService?.previousState.blockNow, state.blockNow) ||
|
!deepEqual(RuntimeService?.previousState.blockNow, state.blockNow) ||
|
||||||
RuntimeService?.previousState.blockNext !== state.blockNext;
|
RuntimeService?.previousState.blockNext !== state.blockNext;
|
||||||
|
|
||||||
|
// TODO: the value shows up one tick to late
|
||||||
|
const shouldNextFlagUpdate = !deepEqual(RuntimeService?.previousState?.nextFlag, state.nextFlag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Many other values are calculated based on the clock
|
* Many other values are calculated based on the clock
|
||||||
* so if any of them are updated we also need to send the clock
|
* so if any of them are updated we also need to send the clock
|
||||||
@@ -755,9 +755,9 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
RuntimeService.previousState.blockNext = state.blockNext;
|
RuntimeService.previousState.blockNext = state.blockNext;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RuntimeService.previousState?.nextFlag !== state.nextFlag) {
|
if (shouldNextFlagUpdate) {
|
||||||
batch.add('nextFlag', state.nextFlag);
|
batch.add('nextFlag', state.nextFlag);
|
||||||
RuntimeService.previousState.nextFlag = state.nextFlag;
|
RuntimeService.previousState.nextFlag = structuredClone(state.nextFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasImmediateChanges) {
|
if (hasImmediateChanges) {
|
||||||
|
|||||||
@@ -1,67 +1,12 @@
|
|||||||
import { isOntimeEvent, MaybeNumber, OffsetMode, OntimeBlock, Rundown, TimerPhase } from 'ontime-types';
|
import { MaybeNumber, TimerPhase } from 'ontime-types';
|
||||||
import { calculateTimeUntilStart, dayInMs, getLastEventNormal, isPlaybackActive } from 'ontime-utils';
|
import { dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||||
import type { RuntimeState } from '../stores/runtimeState.js';
|
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||||
import { shouldCrashDev } from '../utils/development.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle events that span over midnight
|
* handle events that span over midnight
|
||||||
*/
|
*/
|
||||||
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
|
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the expected time of the group to end.
|
|
||||||
* Should only be called if a block is running
|
|
||||||
* TODO: take a look at how it handles relative offset mode
|
|
||||||
*/
|
|
||||||
export function getExpectedBlockFinish(state: RuntimeState, rundown: Rundown): MaybeNumber {
|
|
||||||
const { blockNow, eventNow, timer, clock } = state;
|
|
||||||
|
|
||||||
if (blockNow === null) return null;
|
|
||||||
// if the group doesn't have a start time there is no end time either
|
|
||||||
if (blockNow.startedAt === null) return null;
|
|
||||||
if (eventNow === null) return null;
|
|
||||||
if (timer.current === null) return null;
|
|
||||||
|
|
||||||
const { entries } = rundown;
|
|
||||||
const orderInBlock = (entries[blockNow.id] as OntimeBlock).entries;
|
|
||||||
|
|
||||||
const indexInBlock = orderInBlock.findIndex((id) => eventNow.id === id);
|
|
||||||
shouldCrashDev(indexInBlock < 0, 'Running event is not in current block');
|
|
||||||
|
|
||||||
if (indexInBlock === orderInBlock.length - 1) return timer.expectedFinish;
|
|
||||||
|
|
||||||
let totalGap = 0;
|
|
||||||
let isLinkedToLoaded = true;
|
|
||||||
|
|
||||||
for (let i = indexInBlock + 1; i < orderInBlock.length; i++) {
|
|
||||||
const entry = entries[orderInBlock[i]];
|
|
||||||
if (isOntimeEvent(entry)) {
|
|
||||||
totalGap += entry.gap;
|
|
||||||
isLinkedToLoaded = isLinkedToLoaded && entry.linkStart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { lastEvent } = getLastEventNormal(rundown.entries, orderInBlock);
|
|
||||||
if (!lastEvent) return null;
|
|
||||||
|
|
||||||
const { offsetMode, offsetAbs, offsetRel, plannedStart, actualStart } = state.runtime;
|
|
||||||
|
|
||||||
const timeUntilLastEvent = calculateTimeUntilStart({
|
|
||||||
timeStart: lastEvent.timeStart,
|
|
||||||
dayOffset: lastEvent.dayOffset,
|
|
||||||
delay: lastEvent.delay,
|
|
||||||
currentDay: eventNow.dayOffset,
|
|
||||||
totalGap,
|
|
||||||
isLinkedToLoaded,
|
|
||||||
clock,
|
|
||||||
offsetMode,
|
|
||||||
offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel,
|
|
||||||
plannedStart,
|
|
||||||
actualStart,
|
|
||||||
});
|
|
||||||
|
|
||||||
return clock + timeUntilLastEvent + lastEvent.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates expected finish time of a running timer
|
* Calculates expected finish time of a running timer
|
||||||
* @param {RuntimeState} state runtime state
|
* @param {RuntimeState} state runtime state
|
||||||
@@ -221,17 +166,6 @@ export function getRuntimeOffset(state: RuntimeState): { offsetAbs: number; offs
|
|||||||
return { offsetAbs: offset, offsetRel };
|
return { offsetAbs: offset, offsetRel };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.offsetAbs + state._rundown.totalDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks running timer to see which phase it currently is in
|
* Checks running timer to see which phase it currently is in
|
||||||
* @param state
|
* @param state
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ const baseState: RuntimeState = {
|
|||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: 0,
|
totalDelay: 0,
|
||||||
},
|
},
|
||||||
|
_block: null,
|
||||||
|
_end: null,
|
||||||
|
_flag: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
|
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import {
|
|||||||
clearState,
|
clearState,
|
||||||
getState,
|
getState,
|
||||||
load,
|
load,
|
||||||
loadBlock,
|
loadBlockFlagAndEnd,
|
||||||
pause,
|
pause,
|
||||||
roll,
|
roll,
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
} from '../runtimeState.js';
|
} from '../runtimeState.js';
|
||||||
import { rundownCache } from '../../api-data/rundown/rundown.dao.js';
|
import { rundownCache } from '../../api-data/rundown/rundown.dao.js';
|
||||||
|
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
|
||||||
|
|
||||||
const mockEvent = {
|
const mockEvent = {
|
||||||
type: 'event',
|
type: 'event',
|
||||||
@@ -388,7 +389,9 @@ describe('loadBlock', () => {
|
|||||||
eventNow: rundown.entries[11],
|
eventNow: rundown.entries[11],
|
||||||
} as unknown as RuntimeState;
|
} as unknown as RuntimeState;
|
||||||
|
|
||||||
loadBlock(rundown, state);
|
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
|
loadBlockFlagAndEnd(rundown, metadata, 2, state);
|
||||||
|
|
||||||
expect(state).toMatchObject({
|
expect(state).toMatchObject({
|
||||||
blockNow: { id: rundown.entries[1].id, startedAt: null },
|
blockNow: { id: rundown.entries[1].id, startedAt: null },
|
||||||
@@ -413,7 +416,9 @@ describe('loadBlock', () => {
|
|||||||
eventNow: rundown.entries[22],
|
eventNow: rundown.entries[22],
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
loadBlock(rundown, state);
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
|
loadBlockFlagAndEnd(rundown, metadata, 1, state);
|
||||||
|
|
||||||
expect(state).toMatchObject({
|
expect(state).toMatchObject({
|
||||||
blockNow: { id: rundown.entries[2].id, startedAt: null },
|
blockNow: { id: rundown.entries[2].id, startedAt: null },
|
||||||
@@ -441,7 +446,9 @@ describe('loadBlock', () => {
|
|||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
loadBlock(rundown, state);
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
|
loadBlockFlagAndEnd(rundown, metadata, 1, state);
|
||||||
|
|
||||||
expect(state).toMatchObject({
|
expect(state).toMatchObject({
|
||||||
blockNow: null,
|
blockNow: null,
|
||||||
@@ -464,7 +471,10 @@ describe('loadBlock', () => {
|
|||||||
eventNow: rundown.entries[2],
|
eventNow: rundown.entries[2],
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
loadBlock(rundown, state);
|
const metadata = { playableEventOrder: ['1', '2'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
|
loadBlockFlagAndEnd(rundown, metadata, 0, state);
|
||||||
|
|
||||||
expect(state).toMatchObject({
|
expect(state).toMatchObject({
|
||||||
blockNow: { id: rundown.entries[0].id, startedAt: 123 },
|
blockNow: { id: rundown.entries[0].id, startedAt: 123 },
|
||||||
eventNow: rundown.entries[2],
|
eventNow: rundown.entries[2],
|
||||||
@@ -485,7 +495,9 @@ describe('loadBlock', () => {
|
|||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
loadBlock(rundown, state);
|
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
|
loadBlockFlagAndEnd(rundown, metadata, 0, state);
|
||||||
|
|
||||||
expect(state).toMatchObject({
|
expect(state).toMatchObject({
|
||||||
blockNow: null,
|
blockNow: null,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
CurrentBlockState,
|
CurrentBlockState,
|
||||||
isOntimeBlock,
|
EntryMetaData,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
MaybeNumber,
|
MaybeNumber,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
OffsetMode,
|
OffsetMode,
|
||||||
|
OntimeBlock,
|
||||||
|
OntimeEvent,
|
||||||
PlayableEvent,
|
PlayableEvent,
|
||||||
Playback,
|
Playback,
|
||||||
Rundown,
|
Rundown,
|
||||||
@@ -12,31 +14,31 @@ import {
|
|||||||
runtimeStorePlaceholder,
|
runtimeStorePlaceholder,
|
||||||
TimerPhase,
|
TimerPhase,
|
||||||
TimerState,
|
TimerState,
|
||||||
UpcomingEntry,
|
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { calculateDuration, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
import {
|
||||||
|
calculateDuration,
|
||||||
|
checkIsNow,
|
||||||
|
dayInMs,
|
||||||
|
getExpectedStart,
|
||||||
|
getLastEventNormal,
|
||||||
|
isPlaybackActive,
|
||||||
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { timeNow } from '../utils/time.js';
|
import { timeNow } from '../utils/time.js';
|
||||||
import type { RestorePoint } from '../services/RestoreService.js';
|
import type { RestorePoint } from '../services/RestoreService.js';
|
||||||
import {
|
import { getCurrent, getExpectedFinish, getRuntimeOffset, getTimerPhase } from '../services/timerUtils.js';
|
||||||
getCurrent,
|
|
||||||
getExpectedBlockFinish,
|
|
||||||
getExpectedEnd,
|
|
||||||
getExpectedFinish,
|
|
||||||
getRuntimeOffset,
|
|
||||||
getTimerPhase,
|
|
||||||
} from '../services/timerUtils.js';
|
|
||||||
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
||||||
import { timerConfig } from '../setup/config.js';
|
import { timerConfig } from '../setup/config.js';
|
||||||
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
||||||
import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js';
|
import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js';
|
||||||
import { getCurrentRundown } from '../api-data/rundown/rundown.dao.js';
|
|
||||||
|
type ExpectedMetadata = { event: OntimeEvent; accumulatedGap: number; isLinkedToLoaded: boolean } | null;
|
||||||
|
|
||||||
export type RuntimeState = {
|
export type RuntimeState = {
|
||||||
clock: number; // realtime clock
|
clock: number; // realtime clock
|
||||||
blockNow: CurrentBlockState | null;
|
blockNow: CurrentBlockState | null;
|
||||||
blockNext: MaybeString;
|
blockNext: MaybeString;
|
||||||
nextFlag: UpcomingEntry | null;
|
nextFlag: EntryMetaData | null;
|
||||||
eventNow: PlayableEvent | null;
|
eventNow: PlayableEvent | null;
|
||||||
eventNext: PlayableEvent | null;
|
eventNext: PlayableEvent | null;
|
||||||
runtime: Runtime;
|
runtime: Runtime;
|
||||||
@@ -50,6 +52,9 @@ export type RuntimeState = {
|
|||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: number; // this value comes from rundown service
|
totalDelay: number; // this value comes from rundown service
|
||||||
};
|
};
|
||||||
|
_block: ExpectedMetadata;
|
||||||
|
_flag: ExpectedMetadata;
|
||||||
|
_end: ExpectedMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runtimeState: RuntimeState = {
|
const runtimeState: RuntimeState = {
|
||||||
@@ -69,6 +74,9 @@ const runtimeState: RuntimeState = {
|
|||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: 0,
|
totalDelay: 0,
|
||||||
},
|
},
|
||||||
|
_block: null,
|
||||||
|
_flag: null,
|
||||||
|
_end: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getState(): Readonly<RuntimeState> {
|
export function getState(): Readonly<RuntimeState> {
|
||||||
@@ -96,6 +104,7 @@ export function clearEventData() {
|
|||||||
runtimeState.runtime.expectedEnd = null;
|
runtimeState.runtime.expectedEnd = null;
|
||||||
runtimeState.runtime.selectedEventIndex = null;
|
runtimeState.runtime.selectedEventIndex = null;
|
||||||
|
|
||||||
|
//TODO: is there any ExpectedMetadata stuff we need to clear here
|
||||||
if (runtimeState.blockNow) runtimeState.blockNow.expectedEnd = null;
|
if (runtimeState.blockNow) runtimeState.blockNow.expectedEnd = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
@@ -115,12 +124,16 @@ export function clearState() {
|
|||||||
|
|
||||||
runtimeState.blockNow = null;
|
runtimeState.blockNow = null;
|
||||||
runtimeState.blockNext = null;
|
runtimeState.blockNext = null;
|
||||||
|
runtimeState._block = null;
|
||||||
|
|
||||||
runtimeState.nextFlag = null;
|
runtimeState.nextFlag = null;
|
||||||
|
runtimeState._flag = null;
|
||||||
|
|
||||||
runtimeState.runtime.offsetAbs = 0;
|
runtimeState.runtime.offsetAbs = 0;
|
||||||
runtimeState.runtime.offsetRel = 0;
|
runtimeState.runtime.offsetRel = 0;
|
||||||
runtimeState.runtime.actualStart = null;
|
runtimeState.runtime.actualStart = null;
|
||||||
runtimeState.runtime.expectedEnd = null;
|
runtimeState.runtime.expectedEnd = null;
|
||||||
|
runtimeState._end = null;
|
||||||
runtimeState.runtime.selectedEventIndex = null;
|
runtimeState.runtime.selectedEventIndex = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
@@ -171,7 +184,7 @@ export function updateRundownData(rundownData: RundownData) {
|
|||||||
runtimeState.runtime.plannedStart = rundownData.firstStart;
|
runtimeState.runtime.plannedStart = rundownData.firstStart;
|
||||||
runtimeState.runtime.plannedEnd =
|
runtimeState.runtime.plannedEnd =
|
||||||
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
|
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
getExpectedTimes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,8 +212,7 @@ export function load(
|
|||||||
// load events in memory along with their data
|
// load events in memory along with their data
|
||||||
loadNow(rundown, metadata, eventIndex);
|
loadNow(rundown, metadata, eventIndex);
|
||||||
loadNext(rundown, metadata, eventIndex);
|
loadNext(rundown, metadata, eventIndex);
|
||||||
loadBlock(rundown);
|
loadBlockFlagAndEnd(rundown, metadata, eventIndex);
|
||||||
loadNextFlag(eventIndex, rundown, metadata);
|
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
runtimeState.timer.playback = Playback.Armed;
|
runtimeState.timer.playback = Playback.Armed;
|
||||||
@@ -217,7 +229,7 @@ export function load(
|
|||||||
const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState);
|
const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.offsetAbs = offsetAbs;
|
runtimeState.runtime.offsetAbs = offsetAbs;
|
||||||
runtimeState.runtime.offsetRel = offsetRel;
|
runtimeState.runtime.offsetRel = offsetRel;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
getExpectedTimes();
|
||||||
}
|
}
|
||||||
if (typeof initialData.blockStartAt === 'number' && runtimeState.blockNow) {
|
if (typeof initialData.blockStartAt === 'number' && runtimeState.blockNow) {
|
||||||
runtimeState.blockNow.startedAt = initialData.blockStartAt;
|
runtimeState.blockNow.startedAt = initialData.blockStartAt;
|
||||||
@@ -341,8 +353,7 @@ export function updateAll(rundown: Rundown, metadata: RundownMetadata) {
|
|||||||
loadNow(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined);
|
loadNow(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined);
|
||||||
loadNext(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined);
|
loadNext(rundown, metadata, eventNowIndex >= 0 ? eventNowIndex : undefined);
|
||||||
updateLoaded(runtimeState.eventNow ?? undefined);
|
updateLoaded(runtimeState.eventNow ?? undefined);
|
||||||
loadBlock(rundown);
|
loadBlockFlagAndEnd(rundown, metadata, eventNowIndex);
|
||||||
loadNextFlag(eventNowIndex, rundown, metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(state: RuntimeState = runtimeState): boolean {
|
export function start(state: RuntimeState = runtimeState): boolean {
|
||||||
@@ -397,7 +408,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offsetAbs;
|
getExpectedTimes();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,7 +471,7 @@ export function addTime(amount: number) {
|
|||||||
const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState);
|
const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.offsetAbs = offsetAbs;
|
runtimeState.runtime.offsetAbs = offsetAbs;
|
||||||
runtimeState.runtime.offsetRel = offsetRel;
|
runtimeState.runtime.offsetRel = offsetRel;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
getExpectedTimes();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -507,7 +518,7 @@ export function update(): UpdateResult {
|
|||||||
const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState);
|
const { offsetAbs, offsetRel } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.offsetAbs = offsetAbs;
|
runtimeState.runtime.offsetAbs = offsetAbs;
|
||||||
runtimeState.runtime.offsetRel = offsetRel;
|
runtimeState.runtime.offsetRel = offsetRel;
|
||||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
// runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||||
|
|
||||||
const finishedNow =
|
const finishedNow =
|
||||||
Boolean(runtimeState._timer.forceFinish) ||
|
Boolean(runtimeState._timer.forceFinish) ||
|
||||||
@@ -520,10 +531,7 @@ export function update(): UpdateResult {
|
|||||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtimeState.blockNow) {
|
getExpectedTimes();
|
||||||
const expectedBlockFinish = getExpectedBlockFinish(runtimeState, getCurrentRundown());
|
|
||||||
runtimeState.blockNow.expectedEnd = expectedBlockFinish;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false };
|
return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false };
|
||||||
|
|
||||||
@@ -632,7 +640,7 @@ export function roll(
|
|||||||
// load events in memory along with their data
|
// load events in memory along with their data
|
||||||
loadNow(rundown, metadata, index);
|
loadNow(rundown, metadata, index);
|
||||||
loadNext(rundown, metadata, index);
|
loadNext(rundown, metadata, index);
|
||||||
loadBlock(rundown);
|
loadBlockFlagAndEnd(rundown, metadata, index);
|
||||||
|
|
||||||
// update roll state
|
// update roll state
|
||||||
runtimeState.timer.playback = Playback.Roll;
|
runtimeState.timer.playback = Playback.Roll;
|
||||||
@@ -690,72 +698,152 @@ export function roll(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle block loading, not for use outside of runtimeState
|
* calculates and sets values directly in state
|
||||||
|
* - runtime.expectedEnd
|
||||||
|
* - blockNow.expectedEnd
|
||||||
|
* - nextFlag.expectedStart
|
||||||
*/
|
*/
|
||||||
export function loadBlock(rundown: Rundown, state = runtimeState) {
|
function getExpectedTimes(state = runtimeState) {
|
||||||
// we need a loaded event to have a block
|
const { offsetMode, offsetAbs, offsetRel, plannedStart, actualStart } = state.runtime;
|
||||||
if (state.eventNow === null) {
|
const { eventNow } = state;
|
||||||
state.blockNow = null;
|
|
||||||
state.blockNext = null;
|
if (!eventNow) return;
|
||||||
return;
|
|
||||||
|
state.runtime.expectedEnd = null;
|
||||||
|
if (state.blockNow) {
|
||||||
|
state.blockNow.expectedEnd = null;
|
||||||
|
const { _block } = state;
|
||||||
|
if (state.blockNow.startedAt !== null && _block !== null) {
|
||||||
|
const { event, accumulatedGap, isLinkedToLoaded } = _block;
|
||||||
|
const expectedStart = getExpectedStart(event, {
|
||||||
|
currentDay: eventNow.dayOffset,
|
||||||
|
totalGap: accumulatedGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
offsetMode,
|
||||||
|
offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel,
|
||||||
|
plannedStart,
|
||||||
|
actualStart,
|
||||||
|
});
|
||||||
|
state.blockNow.expectedEnd = expectedStart + event.duration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.nextFlag) {
|
||||||
|
state.nextFlag.expectedStart = null;
|
||||||
|
const { _flag } = state;
|
||||||
|
if (_flag) {
|
||||||
|
const { event, accumulatedGap, isLinkedToLoaded } = _flag;
|
||||||
|
const expectedStart = getExpectedStart(event, {
|
||||||
|
currentDay: eventNow.dayOffset,
|
||||||
|
totalGap: accumulatedGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
offsetMode,
|
||||||
|
offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel,
|
||||||
|
plannedStart,
|
||||||
|
actualStart,
|
||||||
|
});
|
||||||
|
state.nextFlag.expectedStart = expectedStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state._end) {
|
||||||
|
const { event, accumulatedGap, isLinkedToLoaded } = state._end;
|
||||||
|
const expectedStart = getExpectedStart(event, {
|
||||||
|
currentDay: eventNow.dayOffset,
|
||||||
|
totalGap: accumulatedGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
offsetMode,
|
||||||
|
offset: offsetMode === OffsetMode.Absolute ? offsetAbs : offsetRel,
|
||||||
|
plannedStart,
|
||||||
|
actualStart,
|
||||||
|
});
|
||||||
|
state.runtime.expectedEnd = expectedStart + event.duration;
|
||||||
|
} else {
|
||||||
|
state.runtime.expectedEnd = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadBlockFlagAndEnd(
|
||||||
|
rundown: Rundown,
|
||||||
|
metadata: RundownMetadata,
|
||||||
|
currentIndex: MaybeNumber,
|
||||||
|
state = runtimeState,
|
||||||
|
) {
|
||||||
|
if (currentIndex === null) return resetMetaData();
|
||||||
|
if (state.eventNow === null) return resetMetaData();
|
||||||
|
|
||||||
const currentBlockId = state.eventNow.parent;
|
const currentBlockId = state.eventNow.parent;
|
||||||
|
const flagsPresent = metadata.flags.length !== 0;
|
||||||
|
|
||||||
// look for potential next block
|
const { playableEventOrder } = metadata;
|
||||||
let foundEventNow = false;
|
const { entries } = rundown;
|
||||||
for (const id of rundown.order) {
|
|
||||||
if (foundEventNow && isOntimeBlock(rundown.entries[id])) {
|
const orderInBlock = currentBlockId ? (entries[currentBlockId] as OntimeBlock).entries : null;
|
||||||
state.blockNext = id; // the id is set here, the start time is set in other placed that handel starting events
|
const lastEventInGroup = orderInBlock ? getLastEventNormal(rundown.entries, orderInBlock).lastEvent : null;
|
||||||
break;
|
|
||||||
}
|
// if we don't have a any flags in the rundown then no need to look for it
|
||||||
if (id === state.eventNow.id) {
|
let foundFlag = !flagsPresent;
|
||||||
foundEventNow = true;
|
let foundNextGroup = false;
|
||||||
continue;
|
// if we don't have a last event for the group there is no need to find its end time
|
||||||
|
let foundGroupEnd = lastEventInGroup === null;
|
||||||
|
|
||||||
|
let accumulatedGap = 0;
|
||||||
|
let isLinkedToLoaded = true;
|
||||||
|
|
||||||
|
for (let idx = currentIndex; idx < playableEventOrder.length; idx++) {
|
||||||
|
const entry = entries[playableEventOrder[idx]];
|
||||||
|
|
||||||
|
if (isOntimeEvent(entry)) {
|
||||||
|
if (idx !== currentIndex) {
|
||||||
|
// we only accumulate data after the loaded event
|
||||||
|
accumulatedGap += entry.gap;
|
||||||
|
isLinkedToLoaded = isLinkedToLoaded && entry.linkStart;
|
||||||
|
|
||||||
|
// and the loaded event is not allowed to be the next flag
|
||||||
|
if (!foundFlag && metadata.flags.includes(entry.id)) {
|
||||||
|
foundFlag = true;
|
||||||
|
state.nextFlag = { id: entry.id, actualStart: null, expectedStart: null, expectedEnd: null };
|
||||||
|
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundGroupEnd && entry.id === lastEventInGroup?.id) {
|
||||||
|
foundGroupEnd = true;
|
||||||
|
state._block = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundNextGroup && entry.parent !== currentBlockId) {
|
||||||
|
foundNextGroup = true;
|
||||||
|
state.blockNext = entry.parent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// not inside a block
|
const lastID = playableEventOrder.at(-1);
|
||||||
|
const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null;
|
||||||
|
if (lastEvent) {
|
||||||
|
state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundFlag) state.nextFlag = null;
|
||||||
|
|
||||||
if (currentBlockId === null) {
|
if (currentBlockId === null) {
|
||||||
state.blockNow = null;
|
state.blockNow = null;
|
||||||
return;
|
} else if ((state.blockNow != null && state.blockNow.id != currentBlockId) || state.blockNow == null) {
|
||||||
}
|
// we went into a new block - and it is different from the one we might have come from
|
||||||
|
// the id is set here, the start time is set when starting events
|
||||||
//we went into a new block - and it is different from the one we might have come from
|
state.blockNow = { id: currentBlockId, startedAt: null, expectedEnd: null };
|
||||||
if ((state.blockNow != null && state.blockNow.id != currentBlockId) || state.blockNow == null) {
|
|
||||||
state.blockNow = { id: currentBlockId, startedAt: null, expectedEnd: null }; // the id is set here, the start time is set when starting events
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const resetMetaData = (state = runtimeState) => {
|
||||||
* find and load the next flag from the currently loaded event
|
state.blockNow = null;
|
||||||
*/
|
state.blockNext = null;
|
||||||
export function loadNextFlag(currentIndex: number, rundown: Rundown, metadata: RundownMetadata) {
|
state._block = null;
|
||||||
runtimeState.nextFlag = null;
|
state.nextFlag = null;
|
||||||
|
state._flag = null;
|
||||||
if (metadata.flags.length === 0) {
|
state._end = null;
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// we are in the last event
|
|
||||||
if (currentIndex + 1 >= metadata.timedEventOrder.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = currentIndex + 1; i < metadata.timedEventOrder.length; i++) {
|
|
||||||
const entryId = metadata.timedEventOrder[i];
|
|
||||||
if (metadata.flags.includes(entryId)) {
|
|
||||||
const event = rundown.entries[entryId];
|
|
||||||
if (!event || !isOntimeEvent(event)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
runtimeState.nextFlag = { id: event.id, start: event.timeStart };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setOffsetMode(mode: OffsetMode) {
|
export function setOffsetMode(mode: OffsetMode) {
|
||||||
runtimeState.runtime.offsetMode = mode;
|
runtimeState.runtime.offsetMode = mode;
|
||||||
|
|||||||
@@ -11,3 +11,10 @@ export type UpcomingEntry = {
|
|||||||
id: EntryId;
|
id: EntryId;
|
||||||
start: number;
|
start: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EntryMetaData = {
|
||||||
|
id: EntryId;
|
||||||
|
actualStart: MaybeNumber;
|
||||||
|
expectedStart: MaybeNumber;
|
||||||
|
expectedEnd: MaybeNumber;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { MaybeString } from '../../utils/utils.type.js';
|
import type { MaybeString } from '../../utils/utils.type.js';
|
||||||
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
||||||
import type { SimpleTimerState } from './AuxTimer.type.js';
|
import type { SimpleTimerState } from './AuxTimer.type.js';
|
||||||
import type { CurrentBlockState, UpcomingEntry } from './CurrentBlockState.type.js';
|
import type { CurrentBlockState, EntryMetaData } from './CurrentBlockState.type.js';
|
||||||
import type { MessageState } from './MessageControl.type.js';
|
import type { MessageState } from './MessageControl.type.js';
|
||||||
import type { Runtime } from './Runtime.type.js';
|
import type { Runtime } from './Runtime.type.js';
|
||||||
import type { TimerState } from './TimerState.type.js';
|
import type { TimerState } from './TimerState.type.js';
|
||||||
@@ -22,7 +22,7 @@ export type RuntimeStore = {
|
|||||||
|
|
||||||
blockNow: CurrentBlockState | null;
|
blockNow: CurrentBlockState | null;
|
||||||
blockNext: MaybeString;
|
blockNext: MaybeString;
|
||||||
nextFlag: UpcomingEntry | null;
|
nextFlag: EntryMetaData | null;
|
||||||
|
|
||||||
// extra timers
|
// extra timers
|
||||||
auxtimer1: SimpleTimerState;
|
auxtimer1: SimpleTimerState;
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export { OffsetMode } from './definitions/runtime/Runtime.type.js';
|
|||||||
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
||||||
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
|
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
|
||||||
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
|
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
|
||||||
export type { CurrentBlockState, UpcomingEntry } from './definitions/runtime/CurrentBlockState.type.js';
|
export type { CurrentBlockState, UpcomingEntry, EntryMetaData } from './definitions/runtime/CurrentBlockState.type.js';
|
||||||
|
|
||||||
// ---> Extra Timer
|
// ---> Extra Timer
|
||||||
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';
|
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
|
|||||||
|
|
||||||
// feature business logic
|
// feature business logic
|
||||||
|
|
||||||
export { calculateTimeUntilStart } from './src/date-utils/calculateTimeUntilStart.js';
|
export { getExpectedStart } from './src/date-utils/getExpectedStart.js';
|
||||||
|
|
||||||
// feature business logic - rundown
|
// feature business logic - rundown
|
||||||
export { checkIsNow } from './src/date-utils/checkIsNow.js';
|
export { checkIsNow } from './src/date-utils/checkIsNow.js';
|
||||||
|
|||||||
@@ -1,258 +0,0 @@
|
|||||||
import { OffsetMode } from 'ontime-types';
|
|
||||||
|
|
||||||
import { calculateTimeUntilStart } from './calculateTimeUntilStart';
|
|
||||||
import { dayInMs } from './conversionUtils';
|
|
||||||
|
|
||||||
describe('calculateTimeUntilStart()', () => {
|
|
||||||
describe('Absolute offset mode', () => {
|
|
||||||
test('ontime', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 90,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 90,
|
|
||||||
offset: -20,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running ahead', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 80,
|
|
||||||
offset: 10,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind with enough gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 20,
|
|
||||||
clock: 50,
|
|
||||||
offset: -20,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50); // <-- when gap is enough to compensate for the running behind
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind with too little gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 10,
|
|
||||||
clock: 50,
|
|
||||||
offset: -20,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: 0,
|
|
||||||
plannedStart: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Relative offset mode', () => {
|
|
||||||
test('basic function', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeStartEvent2 = 10;
|
|
||||||
const timeStartEvent3 = 20;
|
|
||||||
|
|
||||||
//event 1 is the currently running event
|
|
||||||
|
|
||||||
//event 2
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(10);
|
|
||||||
|
|
||||||
//event 3
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(20);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(20);
|
|
||||||
|
|
||||||
// When clock advances by 5ms, time until start should decrease by 5ms
|
|
||||||
test.clock = 105;
|
|
||||||
|
|
||||||
//event 2
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(5);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(5);
|
|
||||||
|
|
||||||
//event 3
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(15);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 20,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 10,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
|
||||||
|
|
||||||
// When clock advances by 5ms, time until start should decrease by 5ms
|
|
||||||
test.clock = 105;
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('added/remove time', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 20,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
|
||||||
|
|
||||||
test.offset = 5; // remove 5 with addtime - we are ahead of time
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // unlocked evets will stay on schedule
|
|
||||||
|
|
||||||
test.offset = -5; // add 5 with addtime - we are behind
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(25);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(25);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('next day', () => {
|
|
||||||
const test = {
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
// this event will start the current day
|
|
||||||
expect(
|
|
||||||
calculateTimeUntilStart({
|
|
||||||
...test,
|
|
||||||
timeStart: 10,
|
|
||||||
dayOffset: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
}),
|
|
||||||
).toBe(10);
|
|
||||||
|
|
||||||
// this event will start the next day
|
|
||||||
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
|
|
||||||
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
|
|
||||||
expect(
|
|
||||||
calculateTimeUntilStart({
|
|
||||||
...test,
|
|
||||||
timeStart: 0,
|
|
||||||
dayOffset: 1,
|
|
||||||
totalGap: dayInMs - 20,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
}),
|
|
||||||
).toBe(dayInMs);
|
|
||||||
|
|
||||||
// advancing 100ms
|
|
||||||
test.clock = 200;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
calculateTimeUntilStart({
|
|
||||||
...test,
|
|
||||||
timeStart: 0,
|
|
||||||
dayOffset: 1,
|
|
||||||
totalGap: dayInMs - 20,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
}),
|
|
||||||
).toBe(dayInMs - 100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('overlap with negative total gap', () => {
|
|
||||||
const test = {
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// the overlap will be pushed out to the expected available time
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: 5, totalGap: -5 })).toBe(10);
|
|
||||||
|
|
||||||
test.clock = 105;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
import { dayInMs } from './conversionUtils';
|
||||||
|
import { getExpectedStart } from './getExpectedStart';
|
||||||
|
|
||||||
|
describe('getExpectedStart()', () => {
|
||||||
|
describe('Absolute offset mode', () => {
|
||||||
|
test('ontime', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 100,
|
||||||
|
delay: 0,
|
||||||
|
dayOffset: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(testEvent.timeStart);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(testEvent.timeStart);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running ahead', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
offset: 10,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(100); // <-- when running ahead the unlinked timer stays put
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(90);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind with enough gaps', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 20,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(100); // <-- when gap is enough to compensate for the running behind
|
||||||
|
// expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(70); This should not be possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind with too little gaps', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 10,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: 0,
|
||||||
|
plannedStart: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(110); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
|
||||||
|
// expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(70); This should not be possible
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Relative offset mode', () => {
|
||||||
|
test('basic function', () => {
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeStartEvent2 = 10;
|
||||||
|
const timeStartEvent3 = 20;
|
||||||
|
|
||||||
|
//event 1 is the currently running event
|
||||||
|
|
||||||
|
//event 2
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
|
||||||
|
{ ...testState, isLinkedToLoaded: true },
|
||||||
|
),
|
||||||
|
).toBe(110);
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
|
||||||
|
{ ...testState, isLinkedToLoaded: false },
|
||||||
|
),
|
||||||
|
).toBe(110);
|
||||||
|
|
||||||
|
//event 3
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
|
||||||
|
{ ...testState, isLinkedToLoaded: true },
|
||||||
|
),
|
||||||
|
).toBe(120);
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
|
||||||
|
{ ...testState, isLinkedToLoaded: false },
|
||||||
|
),
|
||||||
|
).toBe(120);
|
||||||
|
|
||||||
|
// if we actually started 5ms later
|
||||||
|
testState.actualStart = 105;
|
||||||
|
|
||||||
|
//event 2
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
|
||||||
|
{ ...testState, isLinkedToLoaded: true },
|
||||||
|
),
|
||||||
|
).toBe(115);
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent2 },
|
||||||
|
{ ...testState, isLinkedToLoaded: false },
|
||||||
|
),
|
||||||
|
).toBe(115);
|
||||||
|
|
||||||
|
//event 3
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
|
||||||
|
{ ...testState, isLinkedToLoaded: true },
|
||||||
|
),
|
||||||
|
).toBe(125);
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ dayOffset: 0, delay: 0, timeStart: timeStartEvent3 },
|
||||||
|
{ ...testState, isLinkedToLoaded: false },
|
||||||
|
),
|
||||||
|
).toBe(125);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gaps', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 20,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 10,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120);
|
||||||
|
|
||||||
|
// if we actually started 5ms later
|
||||||
|
testState.actualStart = 105;
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(125);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(125);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added/remove time', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 20,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(120);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120);
|
||||||
|
|
||||||
|
testState.offset = 5; // remove 5 with addtime - we are ahead of time
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(115);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(120); // unlocked evets will stay on schedule
|
||||||
|
|
||||||
|
testState.offset = -5; // add 5 with addtime - we are behind
|
||||||
|
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: true })).toBe(125);
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, isLinkedToLoaded: false })).toBe(125);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next day', () => {
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
// this event will start the current day
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ timeStart: 10, delay: 0, dayOffset: 0 },
|
||||||
|
{ ...testState, totalGap: 0, isLinkedToLoaded: false },
|
||||||
|
),
|
||||||
|
).toBe(110);
|
||||||
|
|
||||||
|
// this event will start the next day
|
||||||
|
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
|
||||||
|
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
|
||||||
|
expect(
|
||||||
|
getExpectedStart(
|
||||||
|
{ timeStart: 0, delay: 0, dayOffset: 1 },
|
||||||
|
{ ...testState, totalGap: dayInMs - 20, isLinkedToLoaded: false },
|
||||||
|
),
|
||||||
|
).toBe(dayInMs + 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('overlap with negative total gap', () => {
|
||||||
|
const testEvent = {
|
||||||
|
timeStart: 5,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
};
|
||||||
|
const testState = {
|
||||||
|
currentDay: 0,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// the overlap will be pushed out to the expected available time
|
||||||
|
expect(getExpectedStart(testEvent, { ...testState, totalGap: -5 })).toBe(110);
|
||||||
|
});
|
||||||
|
});
|
||||||
+12
-22
@@ -4,6 +4,7 @@ import { OffsetMode } from 'ontime-types';
|
|||||||
import { dayInMs } from './conversionUtils.js';
|
import { dayInMs } from './conversionUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param event the event that we are counting to
|
||||||
* @param currentDay the day offset of the currently running event
|
* @param currentDay the day offset of the currently running event
|
||||||
* @param totalGap accumulated gap from the current event
|
* @param totalGap accumulated gap from the current event
|
||||||
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
|
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
|
||||||
@@ -11,31 +12,20 @@ import { dayInMs } from './conversionUtils.js';
|
|||||||
* @param offset
|
* @param offset
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function calculateTimeUntilStart(
|
export function getExpectedStart(
|
||||||
data: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'> & {
|
event: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'>,
|
||||||
|
state: {
|
||||||
currentDay: number;
|
currentDay: number;
|
||||||
totalGap: number;
|
totalGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
clock: number;
|
|
||||||
offset: number;
|
offset: number;
|
||||||
offsetMode: OffsetMode;
|
offsetMode: OffsetMode;
|
||||||
actualStart: MaybeNumber;
|
actualStart: MaybeNumber;
|
||||||
plannedStart: MaybeNumber;
|
plannedStart: MaybeNumber;
|
||||||
},
|
},
|
||||||
): number {
|
): number {
|
||||||
const {
|
const { timeStart, dayOffset, delay } = event;
|
||||||
timeStart,
|
const { currentDay, totalGap, isLinkedToLoaded, offset, offsetMode, actualStart, plannedStart } = state;
|
||||||
dayOffset,
|
|
||||||
currentDay,
|
|
||||||
totalGap,
|
|
||||||
isLinkedToLoaded,
|
|
||||||
clock,
|
|
||||||
offset,
|
|
||||||
delay,
|
|
||||||
offsetMode,
|
|
||||||
actualStart,
|
|
||||||
plannedStart,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
//How many days from the currently running event to this one
|
//How many days from the currently running event to this one
|
||||||
const relativeDayOffset = dayOffset - currentDay;
|
const relativeDayOffset = dayOffset - currentDay;
|
||||||
@@ -51,22 +41,22 @@ export function calculateTimeUntilStart(
|
|||||||
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduledTimeUntil = normalisedTimeStart - clock + relativeStartOffset;
|
const scheduledStartTime = normalisedTimeStart + relativeStartOffset;
|
||||||
|
|
||||||
const offsetTimeUntil = scheduledTimeUntil - offset;
|
const offsetStartTime = scheduledStartTime - offset;
|
||||||
|
|
||||||
if (isLinkedToLoaded) {
|
if (isLinkedToLoaded) {
|
||||||
//if we are directly linked back to the loaded event we just follow the offset
|
//if we are directly linked back to the loaded event we just follow the offset
|
||||||
return offsetTimeUntil;
|
return offsetStartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gapsCanCompensateForOffset = totalGap + offset >= 0;
|
const gapsCanCompensateForOffset = totalGap + offset >= 0;
|
||||||
if (gapsCanCompensateForOffset) {
|
if (gapsCanCompensateForOffset) {
|
||||||
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
||||||
return scheduledTimeUntil;
|
return scheduledStartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise consume as much of the offset as possible with the gap
|
// otherwise consume as much of the offset as possible with the gap
|
||||||
const offsetTimeUntilBufferedByGaps = offsetTimeUntil - totalGap;
|
const offsetStartTimeBufferedByGaps = offsetStartTime - totalGap;
|
||||||
return offsetTimeUntilBufferedByGaps;
|
return offsetStartTimeBufferedByGaps;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user