V2 fix roll (#323)

* chore: profile node

* fix: always roll
This commit is contained in:
Carlos Valente
2023-04-04 10:22:28 +02:00
committed by GitHub
parent 390d466673
commit e56460a940
7 changed files with 581 additions and 216 deletions
+25 -6
View File
@@ -1,4 +1,4 @@
import { EndAction, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
import { eventStore } from '../stores/EventStore.js';
import { PlaybackService } from './PlaybackService.js';
@@ -8,13 +8,23 @@ import { integrationService } from './integration-service/IntegrationService.js'
import { getCurrent, getElapsed, getExpectedFinish } from './timerUtils.js';
import { clock } from './Clock.js';
/**
* Aux type
*/
type Timer = {
_startedAt: number;
_finishAt: number;
duration: number;
current: number;
};
export class TimerService {
private readonly _interval: NodeJS.Timer;
playback: Playback;
timer: TimerState;
loadedTimerId: null;
loadedTimerId: string | null;
private pausedTime: number;
private pausedAt: number | null;
private secondaryTarget: number | null;
@@ -250,7 +260,7 @@ export class TimerService {
clock: this.timer.clock,
secondaryTimer: this.timer.secondaryTimer,
_secondaryTarget: this.secondaryTarget,
secondaryTarget: this.secondaryTarget,
};
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer);
@@ -263,6 +273,7 @@ export class TimerService {
this._onFinish();
}
// to load the next event we have to escalate to parent service
if (doRollLoad) {
PlaybackService.roll();
}
@@ -321,7 +332,13 @@ export class TimerService {
}
}
roll(currentEvent, nextEvent, timers) {
/**
* Loads roll information into timer service
* @param {OntimeEvent | null} currentEvent -- both current event and next event cant be null
* @param {OntimeEvent | null} nextEvent -- both current event and next event cant be null
* @param {Timer} timers
*/
roll(currentEvent: OntimeEvent | null, nextEvent: OntimeEvent | null, timers: Timer) {
this._clear();
this.timer.clock = clock.timeNow();
@@ -336,9 +353,11 @@ export class TimerService {
this.timer.duration = timers.duration;
this.timer.current = timers.current;
} else if (nextEvent) {
// account for day after
const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + DAY_TO_MS : nextEvent.timeStart;
// nothing now, but something coming up
this.timer.secondaryTimer = nextEvent.timeStart - this.timer.clock;
this.secondaryTarget = nextEvent.timeStart;
this.timer.secondaryTimer = nextStart - this.timer.clock;
this.secondaryTarget = nextStart;
}
this.playback = Playback.Roll;
@@ -1,11 +1,4 @@
import {
DAY_TO_MS,
getRollTimers,
normaliseEndTime,
replacePlaceholder,
sortArrayByProperty,
updateRoll,
} from '../rollUtils.ts';
import { DAY_TO_MS, getRollTimers, normaliseEndTime, sortArrayByProperty, updateRoll } from '../rollUtils.ts';
// test sortArrayByProperty()
describe('sort simple arrays of objects', () => {
@@ -265,8 +258,7 @@ describe('test that roll loads selection in right order', () => {
expect(state).toStrictEqual(expected);
});
// TODO: always roll
it.skip('if timer is at 100', () => {
it('if timer is at 100', () => {
const now = 100;
const expected = {
nowIndex: null,
@@ -279,15 +271,14 @@ describe('test that roll loads selection in right order', () => {
nextEvent: eventlist[0],
nextPublicEvent: eventlist[4],
currentEvent: null,
currentPublicEvent: eventlist[6],
currentPublicEvent: null,
};
const state = getRollTimers(eventlist, now);
expect(state).toStrictEqual(expected);
});
// TODO: always roll
it.skip('handles rolls to next day with real values', () => {
it('handles rolls to next day with real values', () => {
const singleEventList = [
{
id: 1,
@@ -459,68 +450,6 @@ describe('test that roll behaviour with overlapping times', () => {
});
});
// test replacePlaceholder()
describe('test that it replaces data correctly', () => {
const values = {
$timer: 'timer',
$title: 'title',
$presenter: 'presenter',
$subtitle: 'subtitle',
'$next-title': 'next title',
'$next-presenter': 'next presenter',
'$next-subtitle': 'next subtitle',
};
it('replaces timer', () => {
const test = '___1232132 $timer';
const expected = '___1232132 timer';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
it('replaces title', () => {
const test = '___1232132 $title';
const expected = '___1232132 title';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
it('replaces presenter', () => {
const test = '___1232132 $presenter';
const expected = '___1232132 presenter';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
it('replaces subtitle', () => {
const test = '___1232132 $subtitle';
const expected = '___1232132 subtitle';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
it('replaces next next title', () => {
const test = '___1232132 $next-title';
const expected = '___1232132 next title';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
it('replaces next presenter', () => {
const test = '___1232132 $next-presenter';
const expected = '___1232132 next presenter';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
it('replaces next subtitle', () => {
const test = '___1232132 $next-subtitle';
const expected = '___1232132 next subtitle';
const s = replacePlaceholder(test, values);
expect(s).toBe(expected);
});
});
// test getRollTimers() on issue #58
describe('test that roll behaviour multi day event edge cases', () => {
it('if the start time is the day after end time, and start time is earlier than now', () => {
@@ -621,7 +550,7 @@ describe('typical scenarios', () => {
_finishAt: 15,
clock: 11,
secondaryTimer: null,
_secondaryTarget: null,
secondaryTarget: null,
};
const expected = {
@@ -648,12 +577,12 @@ describe('typical scenarios', () => {
_finishAt: null,
clock: 11,
secondaryTimer: 1,
_secondaryTarget: 15,
secondaryTarget: 15,
};
const expected = {
updatedTimer: null,
updatedSecondaryTimer: timers._secondaryTarget - timers.clock,
updatedSecondaryTimer: timers.secondaryTarget - timers.clock,
doRollLoad: false,
isFinished: false,
};
@@ -668,11 +597,11 @@ describe('typical scenarios', () => {
_finishAt: 11,
clock: 12,
secondaryTimer: null,
_secondaryTarget: null,
secondaryTarget: null,
};
const expected = {
updatedTimer: null,
updatedTimer: -1,
updatedSecondaryTimer: null,
doRollLoad: true,
isFinished: true,
@@ -688,12 +617,12 @@ describe('typical scenarios', () => {
_finishAt: null,
clock: 16,
secondaryTimer: 1,
_secondaryTarget: 15,
secondaryTarget: 15,
};
const expected = {
updatedTimer: null,
updatedSecondaryTimer: timers._secondaryTarget - timers.clock,
updatedSecondaryTimer: timers.secondaryTarget - timers.clock,
doRollLoad: true,
isFinished: false,
};
@@ -708,12 +637,12 @@ describe('typical scenarios', () => {
_finishAt: null,
clock: 15,
secondaryTimer: 0,
_secondaryTarget: 15,
secondaryTarget: 15,
};
const expected = {
updatedTimer: null,
updatedSecondaryTimer: timers._secondaryTarget - timers.clock,
updatedSecondaryTimer: timers.secondaryTarget - timers.clock,
doRollLoad: true,
isFinished: false,
};
+85 -82
View File
@@ -17,72 +17,75 @@ export const normaliseEndTime = (start: number, end: number) => (end < start ? e
* @returns {array} copy of array sorted in ascending order
*/
export const sortArrayByProperty = (arr, property) => {
export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
return [...arr].sort((a, b) => {
return a[property] - b[property];
});
};
/**
* @description Replaces placeholder variables in string with given data
* @param {string} str - string to analyse
* @param {object} values - map of variables: values to use
* @returns {string} finished string
*/
export const replacePlaceholder = (str, values) => {
for (const [k, v] of Object.entries(values)) {
str = str.replace(k, v);
}
return str;
type Timer = {
_startedAt: number;
_finishAt: number;
duration: number;
current: number;
};
/**
* Finds loading information given a current rundown and time
* @param rundown
* @param timeNow
* @param {OntimeEvent[]} rundown - List of playable events
* @param {number} timeNow - time now in ms
* @returns {{}}
*/
export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
let nowIndex: number | null = null; // index of event now
let nowId: string | null = null; // id of event now
let publicIndex: number | null = null; // index of public event now
let publicTime = -1;
let nextIndex: number | null = null; // index of next event
let publicNextIndex: number | null = null; // index of next public event
let timeToNext: number | null = null; // counter: time for next event
let publicTimeToNext: number | null = null; // counter: time for next public event
let timers = null;
let timers: Timer | null = null;
// Order events by startTime
const orderedEvents = sortArrayByProperty(rundown, 'timeStart');
// preload first if we are past events
const lastEvent = orderedEvents[orderedEvents.length - 1];
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
let nextEvent = null;
let nextPublicEvent = null;
let currentEvent = null;
let currentPublicEvent = null;
let nextEvent: OntimeEvent | null = null;
let nextPublicEvent: OntimeEvent | null = null;
let currentEvent: OntimeEvent | null = null;
let currentPublicEvent: OntimeEvent | null = null;
if (timeNow > lastNormalEnd) {
nextIndex = 0;
timeToNext = orderedEvents[0].timeStart + DAY_TO_MS - timeNow;
// we are past last end
// preload first and find next
// look for next public
for (const event of orderedEvents) {
if (event.isPublic) {
nextPublicEvent = event;
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
break;
const firstEvent = orderedEvents[0];
nextIndex = 0;
nextEvent = firstEvent;
timeToNext = firstEvent.timeStart + DAY_TO_MS - timeNow;
if (firstEvent.isPublic) {
nextPublicEvent = firstEvent;
publicNextIndex = 0;
} else {
// look for next public
// dev note: we feel that this is more efficient than filtering
// since the next event will likely be close to the one playing
for (const event of orderedEvents) {
if (event.isPublic) {
nextPublicEvent = event;
// we need the index before this was sorted
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
break;
}
}
}
} else {
// flags: select first event if several overlapping
let nowFound = false;
// keep track of the end times when looking for public
let publicTime = -1;
// loop through events, look for where we should be
for (const event of orderedEvents) {
// When does the event end (handle midnight)
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
@@ -90,27 +93,18 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
if (normalEnd <= timeNow) {
// event ran already
// public event might not be the one running
if (event.isPublic && normalEnd > publicTime) {
// public event might not be the one running
publicTime = normalEnd;
currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
} else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) {
// event is running
// it could also be public
if (event.isPublic) {
publicTime = normalEnd;
currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
currentEvent = event;
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
nowId = event.id;
// set timers
timers = {
_startedAt: event.timeStart,
_finishAt: event.timeEnd,
@@ -118,25 +112,39 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
current: normalEnd - timeNow,
};
nowFound = true;
// it could also be public
if (event.isPublic) {
publicTime = normalEnd;
currentPublicEvent = event;
publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
} else if (normalEnd > timeNow) {
// event will run
// no need to look after found first
if (nextIndex !== null && publicNextIndex !== null) continue;
// we already know whats next and next-public
if (nextIndex !== null && publicNextIndex !== null) {
continue;
}
// look for next events
// check how far the start is from now
const wait = event.timeStart - timeNow;
const timeToEventStart = event.timeStart - timeNow;
if (nextIndex === null || wait < timeToNext) {
timeToNext = wait;
// we don't have a next or this one starts sooner than current next
if (nextIndex === null || timeToEventStart < timeToNext) {
timeToNext = timeToEventStart;
nextEvent = event;
nextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
if ((publicNextIndex === null || wait < publicTimeToNext) && event.isPublic) {
publicTimeToNext = wait;
nextPublicEvent = event;
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
if (event.isPublic) {
// if we don't have a public next or this one start sooner than assigned next
if (publicNextIndex === null || timeToEventStart < publicTimeToNext) {
publicTimeToNext = timeToEventStart;
nextPublicEvent = event;
publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
}
}
}
}
@@ -157,55 +165,50 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
};
};
type CurrentTimers = {
selectedEventId: string | null;
current: number | null;
_finishAt: number | null;
clock: number | null;
secondaryTimer: number | null;
secondaryTarget: number | null;
};
/**
* @description Implements update functions for roll mode
* @param {object} currentTimers
* @param {object} currentTimers.selectedEventId - Id of currently selected event
* @param {object} currentTimers.current - Running timer
* @param {object} currentTimers._finishAt - Expected finish time
* @param {object} currentTimers.clock - time now
* @param {object} currentTimers.secondaryTimer - secondary timer
* @param {object} currentTimers._secondaryTarget - finish time of secondary timer
* @param {CurrentTimers} currentTimers
* @returns {object} object with selection variables
*/
export const updateRoll = (currentTimers) => {
const { selectedEventId, current, _finishAt, clock, secondaryTimer, _secondaryTarget } = currentTimers;
export const updateRoll = (currentTimers: CurrentTimers) => {
const { selectedEventId, current, _finishAt, clock, secondaryTimer, secondaryTarget } = currentTimers;
// timers
let updatedTimer = current;
let updatedSecondaryTimer = secondaryTimer;
// whether rollLoad should be called
// whether rollLoad should be called: force reload of events
let doRollLoad = false;
// whether finished event should trigger
let isFinished = false;
let isPrimaryFinished = false;
if (selectedEventId && current >= 0) {
if (selectedEventId && current !== null) {
// if we have something selected and a timer, we are running
// this is true because roll never goes into negative times
// update timer
updatedTimer = _finishAt - clock;
if (updatedTimer < 0) {
isFinished = true;
updatedTimer = null;
isPrimaryFinished = true;
// we need a new event
doRollLoad = true;
}
} else if (secondaryTimer >= 0) {
// if secondaryTimer is running we are in waiting to roll
// update secondary
updatedSecondaryTimer = _secondaryTarget - clock;
updatedSecondaryTimer = secondaryTarget - clock;
if (updatedSecondaryTimer <= 0) {
// we need a new event
doRollLoad = true;
}
}
// if nothing is running, we need to find out if
// a) we just finished an event (finished was set to true)
// b) we need to look for events
// this could be caused by a secondary timer or event finished
const secondaryRunning = updatedSecondaryTimer <= 0 && updatedSecondaryTimer != null;
if (isFinished || secondaryRunning) {
// look for events
doRollLoad = true;
}
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished };
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished };
};