diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index 4f3714cc0..aa939161c 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -5,7 +5,7 @@ import { eventStore } from '../stores/EventStore.js'; import { PlaybackService } from './PlaybackService.js'; import { updateRoll } from './rollUtils.js'; import { integrationService } from './integration-service/IntegrationService.js'; -import { getCurrent, getExpectedFinish } from './timerUtils.js'; +import { getCurrent, getExpectedFinish, skippedOutOfEvent } from './timerUtils.js'; import { clock } from './Clock.js'; import { logger } from '../classes/Logger.js'; import type { RestorePoint } from './RestoreService.js'; @@ -18,10 +18,13 @@ type initialLoadingData = { type RestoreCallback = (newState: RestorePoint) => Promise; +export const timeSkipLimit = 3 * 32; + export class TimerService { private readonly _interval: NodeJS.Timer; private _updateInterval: number; private _lastUpdate: number | null; + private _skipThreshold: number; playback: Playback; timer: TimerState; @@ -40,11 +43,13 @@ export class TimerService { * @param {object} [timerConfig] * @param {number} [timerConfig.refresh] * @param {number} [timerConfig.updateInterval] + * @param {number} [timerConfig.skipThreshold] */ - constructor(timerConfig: { refresh?: number; updateInterval?: number } = {}) { + constructor(timerConfig: { refresh: number; updateInterval: number; skipThreshold: number }) { this._clear(); - this._interval = setInterval(() => this.update(), timerConfig?.refresh ?? 1000); - this._updateInterval = timerConfig?.updateInterval ?? 1000; + this._interval = setInterval(() => this.update(), timerConfig.refresh); + this._updateInterval = timerConfig.updateInterval; + this._skipThreshold = timerConfig.skipThreshold; } /** @@ -339,7 +344,6 @@ export class TimerService { this.timer.expectedFinish >= this.timer.startedAt ? this.timer.expectedFinish : this.timer.expectedFinish + dayInMs, - clock: this.timer.clock, secondaryTimer: this.timer.secondaryTimer, secondaryTarget: this.secondaryTarget, @@ -405,7 +409,19 @@ export class TimerService { let shouldNotify = false; if (this.playback === Playback.Roll) { shouldNotify = true; - this.updateRoll(); + if ( + skippedOutOfEvent( + previousTime, + this.timer.clock, + this.timer.startedAt, + this.timer.expectedFinish, + this._skipThreshold, + ) + ) { + PlaybackService.roll(); + } else { + this.updateRoll(); + } } else if (this.timer.startedAt !== null) { // we only update timer if a timer has been started shouldNotify = true; @@ -505,4 +521,5 @@ export class TimerService { } // calculate at 30fps, refresh at 1fps -export const eventTimer = new TimerService({ refresh: 32, updateInterval: 1000 }); +// we consider a skip at 3 lost updates +export const eventTimer = new TimerService({ refresh: 32, updateInterval: 1000, skipThreshold: 32 * 3 }); diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 2a6e15c67..acb17cc1a 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1,7 +1,7 @@ import { dayInMs } from 'ontime-utils'; import { TimerType } from 'ontime-types'; -import { getCurrent, getExpectedFinish } from '../timerUtils.js'; +import { getCurrent, getExpectedFinish, skippedOutOfEvent } from '../timerUtils.js'; describe('getExpectedFinish()', () => { it('is null if we havent started', () => { @@ -354,3 +354,106 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => { expect(current).toBe(8); }); }); + +describe('skippedOutOfEvent()', () => { + const testSkipLimit = 32; + it('does not consider an event end as a skip', () => { + const startedAt = 1000; + const duration = 1000; + const expectedFinish = startedAt + duration; + const previousTime = expectedFinish - testSkipLimit / 2; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock += testSkipLimit; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + }); + + it('allows rolling backwards in an event', () => { + const startedAt = 1000; + const duration = 1000; + const expectedFinish = startedAt + duration; + const previousTime = startedAt + testSkipLimit / 2; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock -= testSkipLimit; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + }); + + it('accounts for crossing midnight', () => { + const startedAt = dayInMs - testSkipLimit; + const expectedFinish = 10; + const previousTime = dayInMs - 1; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock = testSkipLimit - 2; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + }); + + it('allows rolling backwards in an event across midnight', () => { + const startedAt = dayInMs - testSkipLimit; + const expectedFinish = 10; + const previousTime = startedAt + 1; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock -= testSkipLimit; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + }); + + it('finds skip forwards out of event', () => { + const startedAt = 1000; + const duration = 1000; + const expectedFinish = startedAt + duration; + const previousTime = expectedFinish - testSkipLimit / 2; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock += testSkipLimit + 1; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true); + }); + + it('finds skip backwards out of event', () => { + const startedAt = 1000; + const duration = 1000; + const expectedFinish = startedAt + duration; + const previousTime = startedAt + testSkipLimit / 2; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock -= testSkipLimit + 1; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true); + }); + + it('finds skip forwards out of event across midnight', () => { + const startedAt = dayInMs - testSkipLimit; + const expectedFinish = 10; + const previousTime = dayInMs - 3; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock = testSkipLimit - 2; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true); + }); + + it('finds skip backwards out of event across midnight', () => { + const startedAt = dayInMs - testSkipLimit; + const expectedFinish = 10; + const previousTime = startedAt + 1; + + let clock = previousTime; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(false); + + clock -= testSkipLimit + 1; + expect(skippedOutOfEvent(previousTime, clock, startedAt, expectedFinish, testSkipLimit)).toBe(true); + }); +}); diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 8edad2a7f..a657389f5 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -64,3 +64,20 @@ export function getCurrent( } return startedAt + duration + addedTime + pausedTime - clock; } + +export function skippedOutOfEvent( + previousTime: number, + clock: number, + startedAt: number, + expectedFinish: number, + skipLimit: number, +): boolean { + const hasPassedMidnight = previousTime > dayInMs - skipLimit && clock < skipLimit; + const adjustedClock = hasPassedMidnight ? clock + dayInMs : clock; + + const timeDifference = previousTime - adjustedClock; + const hasSkipped = Math.abs(timeDifference) > skipLimit; + const adjustedExpectedFinish = expectedFinish >= startedAt ? expectedFinish : expectedFinish + dayInMs; + + return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt); +}