mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2d6847ea6 | |||
| 25466c8376 | |||
| 85e46c961b |
@@ -1,6 +1,7 @@
|
|||||||
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
import { EndAction, Instant, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import * as timeCore from '../../lib/time-core/timeCore.js';
|
||||||
import type { RuntimeState } from '../../stores/runtimeState.js';
|
import type { RuntimeState } from '../../stores/runtimeState.js';
|
||||||
import {
|
import {
|
||||||
findDayOffset,
|
findDayOffset,
|
||||||
@@ -53,11 +54,12 @@ describe('getElapsed()', () => {
|
|||||||
it('uses the current pause start while paused', () => {
|
it('uses the current pause start while paused', () => {
|
||||||
const state = {
|
const state = {
|
||||||
clock: 10 * MILLIS_PER_MINUTE,
|
clock: 10 * MILLIS_PER_MINUTE,
|
||||||
|
_now: timeCore.toInstant((10 * MILLIS_PER_MINUTE) as TimeOfDay, timeCore.now()),
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: 2 * MILLIS_PER_MINUTE,
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: 7 * MILLIS_PER_MINUTE,
|
pausedAt: timeCore.toInstant((7 * MILLIS_PER_MINUTE) as TimeOfDay, timeCore.now()),
|
||||||
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
@@ -975,6 +977,40 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expect(absolute).toBe(25);
|
expect(absolute).toBe(25);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('paused time is delayed time when the pause spans midnight', () => {
|
||||||
|
const state = {
|
||||||
|
eventNow: {
|
||||||
|
id: '1',
|
||||||
|
timeStart: 23 * MILLIS_PER_HOUR, // 23:00
|
||||||
|
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
|
||||||
|
dayOffset: 0,
|
||||||
|
},
|
||||||
|
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
|
||||||
|
_now: timeCore.toInstant((3 * MILLIS_PER_MINUTE) as TimeOfDay, timeCore.now()),
|
||||||
|
timer: {
|
||||||
|
startedAt: 23 * MILLIS_PER_HOUR, // started on time at 23:00
|
||||||
|
current: 25, // still counting down
|
||||||
|
addedTime: 0,
|
||||||
|
},
|
||||||
|
_timer: {
|
||||||
|
pausedAt: timeCore.toInstant(
|
||||||
|
(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE) as TimeOfDay,
|
||||||
|
(timeCore.now() - dayInMs) as Instant,
|
||||||
|
), // 23:58, before midnight
|
||||||
|
pausedDuration: 0,
|
||||||
|
},
|
||||||
|
rundown: {
|
||||||
|
actualStart: 23 * MILLIS_PER_HOUR,
|
||||||
|
plannedStart: 23 * MILLIS_PER_HOUR,
|
||||||
|
currentDay: 0,
|
||||||
|
},
|
||||||
|
_startDayOffset: 0,
|
||||||
|
} as RuntimeState;
|
||||||
|
|
||||||
|
// paused from 23:58 to 00:03 -> so elapsed should still be 58 minutes
|
||||||
|
expect(getElapsed(state)).toBe(58 * MILLIS_PER_MINUTE);
|
||||||
|
});
|
||||||
|
|
||||||
it('offset doesnt exist if we havent started', () => {
|
it('offset doesnt exist if we havent started', () => {
|
||||||
const state = {
|
const state = {
|
||||||
clock: 78480789,
|
clock: 78480789,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
|
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||||
|
|
||||||
|
import * as timeCore from '../lib/time-core/timeCore.js';
|
||||||
import type { RuntimeState } from '../stores/runtimeState.js';
|
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,17 +97,18 @@ export function getCurrent(state: RuntimeState): number {
|
|||||||
* Calculates active time elapsed since the timer started.
|
* Calculates active time elapsed since the timer started.
|
||||||
*/
|
*/
|
||||||
export function getElapsed(state: RuntimeState): MaybeNumber {
|
export function getElapsed(state: RuntimeState): MaybeNumber {
|
||||||
const { clock } = state;
|
const { clock, _now } = state;
|
||||||
const { startedAt } = state.timer;
|
const { startedAt } = state.timer;
|
||||||
const { pausedAt, pausedDuration } = state._timer;
|
const { pausedDuration, pausedAt } = state._timer;
|
||||||
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceClock = pausedAt ?? clock;
|
const currentPauseDuration = pausedAt !== null ? timeCore.timeSince(_now, pausedAt) : 0;
|
||||||
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt);
|
|
||||||
const activeElapsed = elapsedSinceStart - pausedDuration;
|
const elapsedSinceStart = getTimeSinceStart(clock, startedAt);
|
||||||
|
const activeElapsed = elapsedSinceStart - pausedDuration - currentPauseDuration;
|
||||||
|
|
||||||
return Math.max(0, activeElapsed);
|
return Math.max(0, activeElapsed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
|
import { Instant, OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
|
||||||
import { deepmerge } from 'ontime-utils';
|
import { deepmerge } from 'ontime-utils';
|
||||||
|
|
||||||
import type { RuntimeState } from '../runtimeState.js';
|
import type { RuntimeState } from '../runtimeState.js';
|
||||||
|
|
||||||
const baseState: RuntimeState = {
|
const baseState: RuntimeState = {
|
||||||
clock: 0 as TimeOfDay,
|
clock: 0 as TimeOfDay,
|
||||||
|
_now: 0 as Instant,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
eventFlag: null,
|
eventFlag: null,
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ describe('mutation on runtimeState', () => {
|
|||||||
playback: Playback.Pause,
|
playback: Playback.Pause,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
});
|
});
|
||||||
expect(newState._timer.pausedAt).toEqual(newState.clock);
|
expect(newState._timer.pausedAt).toEqual(newState._now);
|
||||||
|
|
||||||
success = pause();
|
success = pause();
|
||||||
expect(success).toBe(false);
|
expect(success).toBe(false);
|
||||||
@@ -248,6 +248,59 @@ describe('mutation on runtimeState', () => {
|
|||||||
state = getState();
|
state = getState();
|
||||||
expect(state.timer.elapsed).toBe(3 * MILLIS_PER_MINUTE);
|
expect(state.timer.elapsed).toBe(3 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('elapsed excludes a pause that spans midnight', async () => {
|
||||||
|
clearState();
|
||||||
|
// an event that runs over midnight (23:00 -> 01:00)
|
||||||
|
const event = {
|
||||||
|
...mockEvent,
|
||||||
|
id: 'elapsed-pause-midnight',
|
||||||
|
timeStart: 23 * MILLIS_PER_HOUR,
|
||||||
|
timeEnd: 1 * MILLIS_PER_HOUR,
|
||||||
|
duration: 2 * MILLIS_PER_HOUR,
|
||||||
|
};
|
||||||
|
const mockRundown = makeRundown({
|
||||||
|
entries: { [event.id]: event },
|
||||||
|
order: [event.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
await initRundown(mockRundown, {});
|
||||||
|
vi.runAllTimers();
|
||||||
|
|
||||||
|
const { metadata, rundown } = rundownCache.get();
|
||||||
|
|
||||||
|
// start before midnight
|
||||||
|
vi.setSystemTime('jan 1 23:50');
|
||||||
|
load(event, rundown, metadata);
|
||||||
|
start();
|
||||||
|
|
||||||
|
// 8 minutes of active running before we pause
|
||||||
|
vi.setSystemTime('jan 1 23:58');
|
||||||
|
update();
|
||||||
|
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
|
||||||
|
pause();
|
||||||
|
|
||||||
|
// elapsed is active time since start, so it must not advance while paused,
|
||||||
|
// not even when the pause itself crosses midnight
|
||||||
|
vi.setSystemTime('jan 2 00:01');
|
||||||
|
update();
|
||||||
|
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
|
||||||
|
|
||||||
|
// resume 5 minutes after pausing, having crossed midnight (23:58 -> 00:03)
|
||||||
|
vi.setSystemTime('jan 2 00:03');
|
||||||
|
start();
|
||||||
|
let state = getState();
|
||||||
|
// the accumulated pause count is 5 minutes, regardless of the midnight wrap
|
||||||
|
expect(state._timer.pausedDuration).toBe(5 * MILLIS_PER_MINUTE);
|
||||||
|
// and elapsed still reflects only the 8 active minutes
|
||||||
|
expect(state.timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
|
||||||
|
|
||||||
|
// 2 more active minutes after resume -> 10 minutes elapsed
|
||||||
|
vi.setSystemTime('jan 2 00:05');
|
||||||
|
update();
|
||||||
|
state = getState();
|
||||||
|
expect(state.timer.elapsed).toBe(10 * MILLIS_PER_MINUTE);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runtime offset', async () => {
|
test('runtime offset', async () => {
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export type RuntimeState = {
|
|||||||
// private properties of the timer calculations
|
// private properties of the timer calculations
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
|
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
|
||||||
pausedAt: Maybe<TimeOfDay>;
|
pausedAt: Maybe<Instant>;
|
||||||
|
|
||||||
|
/** Accumulate pause duration but dose not include the current pause */
|
||||||
pausedDuration: number;
|
pausedDuration: number;
|
||||||
secondaryTarget: Maybe<TimeOfDay>;
|
secondaryTarget: Maybe<TimeOfDay>;
|
||||||
hasFinished: boolean;
|
hasFinished: boolean;
|
||||||
@@ -76,10 +78,12 @@ export type RuntimeState = {
|
|||||||
_end: ExpectedMetadata;
|
_end: ExpectedMetadata;
|
||||||
_startEpoch: Maybe<Instant>;
|
_startEpoch: Maybe<Instant>;
|
||||||
_startDayOffset: Maybe<Day>;
|
_startDayOffset: Maybe<Day>;
|
||||||
|
_now: Instant;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runtimeState: RuntimeState = {
|
const runtimeState: RuntimeState = {
|
||||||
clock: timeCore.timeOfDayNow(),
|
clock: timeCore.timeOfDayNow(),
|
||||||
|
_now: timeCore.now(),
|
||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
@@ -104,6 +108,12 @@ const runtimeState: RuntimeState = {
|
|||||||
_startDayOffset: null,
|
_startDayOffset: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** set the current clock to ensure parity between _now and clock */
|
||||||
|
function setClock(state: RuntimeState) {
|
||||||
|
state._now = timeCore.now();
|
||||||
|
state.clock = timeCore.toTimeOfDay(state._now);
|
||||||
|
}
|
||||||
|
|
||||||
export function getState(): Readonly<RuntimeState> {
|
export function getState(): Readonly<RuntimeState> {
|
||||||
// create a shallow copy of the state
|
// create a shallow copy of the state
|
||||||
return {
|
return {
|
||||||
@@ -136,7 +146,7 @@ export function clearEventData() {
|
|||||||
runtimeState.rundown.selectedEventIndex = null;
|
runtimeState.rundown.selectedEventIndex = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState.clock = timeCore.timeOfDayNow();
|
setClock(runtimeState);
|
||||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
// when clearing, we maintain the total delay from the rundown
|
// when clearing, we maintain the total delay from the rundown
|
||||||
@@ -169,7 +179,7 @@ export function clearState() {
|
|||||||
runtimeState._end = null;
|
runtimeState._end = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState.clock = timeCore.timeOfDayNow();
|
setClock(runtimeState);
|
||||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
// when clearing, we maintain the total delay from the rundown
|
// when clearing, we maintain the total delay from the rundown
|
||||||
@@ -422,15 +432,12 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const epoch = timeCore.now();
|
setClock(state);
|
||||||
const now = timeCore.toTimeOfDay(epoch);
|
|
||||||
|
|
||||||
state.clock = now;
|
|
||||||
state.timer.secondaryTimer = null;
|
state.timer.secondaryTimer = null;
|
||||||
|
|
||||||
// add paused time if it exists
|
// add paused time if it exists
|
||||||
if (state._timer.pausedAt) {
|
if (state._timer.pausedAt) {
|
||||||
const timeToAdd = state.clock - state._timer.pausedAt;
|
const timeToAdd = state._now - state._timer.pausedAt;
|
||||||
state.timer.addedTime += timeToAdd;
|
state.timer.addedTime += timeToAdd;
|
||||||
state._timer.pausedDuration += timeToAdd;
|
state._timer.pausedDuration += timeToAdd;
|
||||||
state._timer.pausedAt = null;
|
state._timer.pausedAt = null;
|
||||||
@@ -447,7 +454,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
if (state.rundown.actualStart === null) {
|
if (state.rundown.actualStart === null) {
|
||||||
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
|
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
|
||||||
state.rundown.currentDay = state._startDayOffset;
|
state.rundown.currentDay = state._startDayOffset;
|
||||||
state._startEpoch = epoch;
|
state._startEpoch = state._now;
|
||||||
state.rundown.actualStart = state.clock;
|
state.rundown.actualStart = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,8 +488,8 @@ export function pause(state: RuntimeState = runtimeState): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.timer.playback = Playback.Pause;
|
state.timer.playback = Playback.Pause;
|
||||||
state.clock = timeCore.timeOfDayNow();
|
setClock(state);
|
||||||
state._timer.pausedAt = state.clock;
|
state._timer.pausedAt = state._now;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,9 +554,7 @@ export type UpdateResult = {
|
|||||||
export function update(): UpdateResult {
|
export function update(): UpdateResult {
|
||||||
// 0. there are some things we always do
|
// 0. there are some things we always do
|
||||||
const previousClock = runtimeState.clock;
|
const previousClock = runtimeState.clock;
|
||||||
const epoch = timeCore.now();
|
setClock(runtimeState); // we update the clock on every update call
|
||||||
const now = timeCore.toTimeOfDay(epoch);
|
|
||||||
runtimeState.clock = now; // we update the clock on every update call
|
|
||||||
|
|
||||||
// 1. is playback idle?
|
// 1. is playback idle?
|
||||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||||
@@ -558,13 +563,13 @@ export function update(): UpdateResult {
|
|||||||
|
|
||||||
// calculate currentDay from epoch (days elapsed since playback was started)
|
// calculate currentDay from epoch (days elapsed since playback was started)
|
||||||
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
||||||
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
|
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
|
||||||
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart;
|
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. are we waiting to roll?
|
// 2. are we waiting to roll?
|
||||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||||
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
|
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, runtimeState.clock);
|
||||||
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user