Keep offset when taking over playback with roll v2 (#1184)

* pass on offset

* account for offset in roll

* prevent roll from overtime

* add test
This commit is contained in:
Alex Christoffer Rasmussen
2024-08-26 18:09:03 +02:00
committed by GitHub
parent 1b1823e0fe
commit ee1b5b7fdd
8 changed files with 160 additions and 51 deletions
@@ -1,7 +1,7 @@
import { PlayableEvent, Playback } from 'ontime-types';
import { PlayableEvent, Playback, TimerPhase } from 'ontime-types';
import { deepmerge } from 'ontime-utils';
import { RuntimeState, addTime, clear, getState, load, pause, start, stop } from '../runtimeState.js';
import { RuntimeState, addTime, clear, getState, load, pause, roll, start, stop } from '../runtimeState.js';
import { initRundown } from '../../services/rundown-service/RundownService.js';
const mockEvent = {
@@ -227,7 +227,103 @@ describe('mutation on runtimeState', () => {
});
test.todo('runtime offset on timers in overtime', () => {});
test.todo('roll mode', () => {});
});
});
describe('roll mode', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime('jan 1 00:00');
clear();
});
afterEach(() => {
vi.useRealTimers();
});
describe('normal roll', () => {
const rundown = [
{ ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 },
{ ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 },
{ ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 },
] as PlayableEvent[];
test('pending event', () => {
const { eventId, didStart } = roll(rundown);
const state = getState();
expect(eventId).toBe('1');
expect(didStart).toBe(false);
expect(state.timer.phase).toBe(TimerPhase.Pending);
expect(state.timer.secondaryTimer).toBe(1000);
});
test('roll events', () => {
vi.setSystemTime('jan 1 00:00:01');
let result = roll(rundown);
expect(result).toStrictEqual({ eventId: '1', didStart: true });
vi.setSystemTime('jan 1 00:00:02');
result = roll(rundown);
expect(result).toStrictEqual({ eventId: '2', didStart: true });
vi.setSystemTime('jan 1 00:00:03:500');
result = roll(rundown);
expect(result).toStrictEqual({ eventId: '3', didStart: true });
});
});
describe('roll takover', () => {
const rundown = [
{ ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 },
{ ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 },
{ ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 },
] as PlayableEvent[];
test('from load', () => {
load(rundown[2], rundown);
const result = roll(rundown);
expect(result).toStrictEqual({ eventId: '3', didStart: false });
const state = getState();
expect(state.timer.phase).toBe(TimerPhase.Pending);
expect(state.timer.secondaryTimer).toBe(3000);
});
test('from play', () => {
load(rundown[0], rundown);
start();
const result = roll(rundown);
expect(result).toStrictEqual({ eventId: '1', didStart: false });
expect(getState().runtime.offset).toBe(1000);
});
});
describe('roll continue with offset', () => {
test('no gaps', () => {
const rundown = [
{ ...mockEvent, id: '1', timeStart: 1000, duration: 1000, timeEnd: 2000 },
{ ...mockEvent, id: '2', timeStart: 2000, duration: 1000, timeEnd: 3000 },
{ ...mockEvent, id: '3', timeStart: 3000, duration: 1000, timeEnd: 4000 },
] as PlayableEvent[];
load(rundown[0], rundown);
start();
let result = roll(rundown, getState().runtime.offset);
expect(result).toStrictEqual({ eventId: '1', didStart: false });
expect(getState().runtime.offset).toBe(1000);
vi.setSystemTime('jan 1 00:00:01');
result = roll(rundown, getState().runtime.offset);
expect(result).toStrictEqual({ eventId: '2', didStart: true });
expect(getState().runtime.offset).toBe(1000);
vi.setSystemTime('jan 1 00:00:02');
result = roll(rundown, getState().runtime.offset);
expect(result).toStrictEqual({ eventId: '3', didStart: true });
expect(getState().runtime.offset).toBe(1000);
});
test.todo('with gaps', () => {
//this is a bit involved as it also depends somewhat on the RintimeService
});
});
});
+33 -20
View File
@@ -314,17 +314,15 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
// handle edge cases with roll
if (runtimeState.timer.playback === Playback.Roll) {
const offsetClock = runtimeState.clock + runtimeState.runtime.offset;
// if waiting to roll, we update the targets and potentially start the timer
if (runtimeState._timer.secondaryTarget !== null) {
if (
runtimeState.eventNow.timeStart < runtimeState.clock &&
runtimeState.clock < runtimeState.eventNow.timeEnd
) {
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
// if the event is now, we queue a start
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart;
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
} else {
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, runtimeState.clock);
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
}
}
}
@@ -516,7 +514,6 @@ export function update(): UpdateResult {
if (finishedNow) {
// reset state
runtimeState._timer.forceFinish;
runtimeState.timer.finishedAt = runtimeState._timer.forceFinish ?? runtimeState.clock;
} else {
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
@@ -536,14 +533,15 @@ export function update(): UpdateResult {
throw new Error('runtimeState.updateIfWaitingToRoll: invalid state received');
}
}
//account for offset
const offsetClock = runtimeState.clock + runtimeState.runtime.offset;
runtimeState.timer.phase = TimerPhase.Pending;
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer < 0 };
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0 };
}
}
export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart: boolean } {
export function roll(rundown: OntimeRundown, offset = 0): { eventId: MaybeString; didStart: boolean } {
// 1. if an event is running, we simply take over the playback
if (runtimeState.timer.playback === Playback.Play && runtimeState.runtime.selectedEventIndex !== null) {
runtimeState.timer.playback = Playback.Roll;
@@ -551,7 +549,15 @@ export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart:
}
// 2. if there is an event armed, we use it
if (runtimeState.timer.playback === Playback.Armed && runtimeState.eventNow !== null) {
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
// eslint-disable-next-line no-unused-labels -- dev code path
DEV: {
if (runtimeState.eventNow === null) {
throw new Error('runtimeState.roll: invalid state received');
}
}
runtimeState.runtime.offset = offset;
runtimeState.timer.playback = Playback.Roll;
// account for event that finishes the day after
@@ -561,14 +567,16 @@ export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart:
: runtimeState.eventNow.timeEnd;
runtimeState.timer.expectedFinish = normalisedEndTime;
//account for offset
const offsetClock = runtimeState.clock + runtimeState.runtime.offset;
// state catch up
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
runtimeState.timer.current = runtimeState.timer.duration;
runtimeState.timer.elapsed = 0;
// check if the event is ready to start or if needs to be waited
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, runtimeState.clock);
// check if the event is ready to start or if needs to be pending
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
if (isNow) {
runtimeState.timer.startedAt = runtimeState.clock;
@@ -579,9 +587,10 @@ export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart:
if (!runtimeState.runtime.actualStart) {
runtimeState.runtime.actualStart = runtimeState.clock;
}
runtimeState.timer.secondaryTimer = null;
} else {
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, runtimeState.clock);
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
runtimeState.timer.phase = TimerPhase.Pending;
}
@@ -599,7 +608,11 @@ export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart:
clear();
runtimeState.currentBlock = prevCurrentBlock;
const { index, isPending } = loadRoll(timedEvents, runtimeState.clock);
//account for offset but we only keep it if passed to us
runtimeState.runtime.offset = offset;
const offsetClock = runtimeState.clock + runtimeState.runtime.offset;
const { index, isPending } = loadRoll(timedEvents, offsetClock);
// load events in memory along with their data
loadNow(timedEvents, index);
@@ -623,8 +636,8 @@ export function roll(rundown: OntimeRundown): { eventId: MaybeString; didStart:
// there is nothing now, but something coming up
runtimeState.timer.phase = TimerPhase.Pending;
// we need to normalise start time in case it is the day after
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, runtimeState.clock);
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - runtimeState.clock;
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
// preload timer properties
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);