V2 fix timer (#304)

This commit is contained in:
Carlos Valente
2023-02-28 19:50:37 +01:00
committed by GitHub
parent 99f4738f4a
commit 3e2626d4f9
9 changed files with 320 additions and 87 deletions
@@ -20,8 +20,7 @@ export function getTimerByType(timerObject?: TimerTypeParams): string | number |
if (timerObject.timerType === TimerType.CountDown) { if (timerObject.timerType === TimerType.CountDown) {
timer = timerObject.current; timer = timerObject.current;
} else if (timerObject.timerType === TimerType.CountUp) { } else if (timerObject.timerType === TimerType.CountUp) {
//todo: fix counting down issue in backend later timer = timerObject.elapsed;
timer = (timerObject.elapsed ?? 0) < 0 ? 0 : timerObject.elapsed;
} else if (timerObject.timerType === TimerType.Clock) { } else if (timerObject.timerType === TimerType.Clock) {
timer = formatTime(timerObject.clock, formatOptions); timer = formatTime(timerObject.clock, formatOptions);
} }
@@ -142,8 +142,8 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const stageTimerCharacters = display.replace('/:/g', '').length; const stageTimerCharacters = display.replace('/:/g', '').length;
const timerFontSize = (89 / (stageTimerCharacters - 1)) * (userOptions.size || 1); const timerFontSize = (89 / (stageTimerCharacters - 1)) * (userOptions.size || 1);
const timerClasseNames = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`; const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`;
return ( return (
@@ -166,7 +166,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
<div className='end-message'>{general.endMessage}</div> <div className='end-message'>{general.endMessage}</div>
) : ( ) : (
<div <div
className={timerClasseNames} className={timerClasses}
style={{ style={{
color: userOptions.textColour, color: userOptions.textColour,
fontSize: `${timerFontSize}vw`, fontSize: `${timerFontSize}vw`,
@@ -78,8 +78,8 @@ export default function Timer(props) {
display = `-${display}`; display = `-${display}`;
} }
const timerFontSize = 100 / stageTimerCharacters; const timerFontSize = 89 / (stageTimerCharacters - 1);
const timerClasseNames = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
return ( return (
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'> <div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
@@ -98,7 +98,7 @@ export default function Timer(props) {
<div className='end-message'>{general.endMessage}</div> <div className='end-message'>{general.endMessage}</div>
) : ( ) : (
<div <div
className={timerClasseNames} className={timerClasses}
style={{ style={{
fontSize: `${timerFontSize}vw`, fontSize: `${timerFontSize}vw`,
}} }}
@@ -6,8 +6,9 @@ import { stringFromMillis } from '../../utils/time.js';
import { messageManager } from '../message-manager/MessageManager.js'; import { messageManager } from '../message-manager/MessageManager.js';
import { PlaybackService } from '../../services/PlaybackService.js'; import { PlaybackService } from '../../services/PlaybackService.js';
import { eventTimer, TimerService } from '../../services/TimerService.js'; import { eventTimer } from '../../services/TimerService.js';
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js'; import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
import { clock } from '../../services/Clock.js';
class SocketController { class SocketController {
constructor() { constructor() {
@@ -329,7 +330,7 @@ class SocketController {
level, level,
origin, origin,
text, text,
time: stringFromMillis(TimerService.getCurrentTime() || 0), time: stringFromMillis(clock.getSystemTime() || 0),
}; };
this.messageStack.unshift(logMessage); this.messageStack.unshift(logMessage);
+57
View File
@@ -0,0 +1,57 @@
enum Source {
System = 'system',
MIDI = 'MIDI',
}
class Clock {
private static instance: Clock;
private readonly source: Source;
constructor(source?: Source) {
if (Clock.instance) {
return Clock.instance;
}
Clock.instance = this;
this.source = source || Source.System;
}
/**
* Get current time from source
*/
timeNow(): number {
switch (this.source) {
case Source.System:
return this.getSystemTime();
case Source.MIDI:
// @ts-expect-error -- not implemented
return this.getMidiTime();
default:
throw new Error('Invalid time source');
}
}
/**
* Get current time from system
*/
getSystemTime() {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
}
/**
* Get current time from MIDI
*/
getMidiTime() {
throw new Error('Not implemented');
}
}
export const clock = new Clock();
+3 -2
View File
@@ -3,8 +3,9 @@
*/ */
import { socketProvider } from '../classes/socket/SocketController.js'; import { socketProvider } from '../classes/socket/SocketController.js';
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js'; import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
import { eventTimer, TimerService } from './TimerService.js';
import { eventStore } from '../stores/EventStore.js'; import { eventStore } from '../stores/EventStore.js';
import { eventTimer } from './TimerService.js';
import { clock } from './Clock.js';
/** /**
* Service manages playback status of app * Service manages playback status of app
@@ -163,7 +164,7 @@ export class PlaybackService {
*/ */
static roll() { static roll() {
if (EventLoader.getPlayableEvents()) { if (EventLoader.getPlayableEvents()) {
const rollTimers = eventLoader.findRoll(TimerService.getCurrentTime()); const rollTimers = eventLoader.findRoll(clock.timeNow());
// nothing to play // nothing to play
if (rollTimers === null) { if (rollTimers === null) {
+60 -76
View File
@@ -5,6 +5,8 @@ import { PlaybackService } from './PlaybackService.js';
import { updateRoll } from './rollUtils.js'; import { updateRoll } from './rollUtils.js';
import { DAY_TO_MS } from '../utils/time.js'; import { DAY_TO_MS } from '../utils/time.js';
import { integrationService } from './integration-service/IntegrationService.js'; import { integrationService } from './integration-service/IntegrationService.js';
import { getCurrent, getElapsed, getExpectedFinish } from './timerUtils.js';
import { clock } from './Clock.js';
export class TimerService { export class TimerService {
private readonly _interval: NodeJS.Timer; private readonly _interval: NodeJS.Timer;
@@ -12,19 +14,19 @@ export class TimerService {
playback: string; playback: string;
loadedTimerId: null; loadedTimerId: null;
private _pausedInterval: number; private pausedTime: number;
private _pausedAt: number | null; private pausedAt: number | null;
private _secondaryTarget: number | null; private secondaryTarget: number | null;
timer: { timer: {
clock: number; clock: number; // realtime clock
current: number | null; current: number | null; // running countdown
elapsed: number | null; elapsed: number | null; // elapsed time in current timer
expectedFinish: number | null; expectedFinish: number | null;
addedTime: number; addedTime: number; // time added by user, can be negative
startedAt: number | null; startedAt: number | null;
finishedAt: number | null; finishedAt: number | null; // only if timer has already finished
secondaryTimer: number | null; secondaryTimer: number | null; // used for roll mode
selectedEventId: string | null; selectedEventId: string | null;
duration: number | null; duration: number | null;
timerType: TimerType | null; timerType: TimerType | null;
@@ -40,41 +42,6 @@ export class TimerService {
this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000); this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000);
} }
/**
* Get current time in ms from midnight
* @static
* @return {number}
*/
static getCurrentTime() {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
}
/**
* Returns expected time finish
* @private
*/
_getExpectedFinish() {
if (this.timer.startedAt === null) {
return null;
}
if (this.timer.finishedAt) {
return this.timer.finishedAt;
}
return Math.max(
this.timer.startedAt + this.timer.duration + this._pausedInterval + this.timer.addedTime,
this.timer.startedAt,
);
}
/** /**
* Clears internal state * Clears internal state
* @private * @private
@@ -82,7 +49,7 @@ export class TimerService {
_clear() { _clear() {
this.playback = 'stop'; this.playback = 'stop';
this.timer = { this.timer = {
clock: TimerService.getCurrentTime(), clock: clock.timeNow(),
current: null, current: null,
elapsed: null, elapsed: null,
expectedFinish: null, expectedFinish: null,
@@ -95,9 +62,9 @@ export class TimerService {
timerType: null, timerType: null,
}; };
this.loadedTimerId = null; this.loadedTimerId = null;
this._pausedInterval = 0; this.pausedTime = 0;
this._pausedAt = null; this.pausedAt = null;
this._secondaryTarget = null; this.secondaryTarget = null;
} }
/** /**
@@ -127,7 +94,13 @@ export class TimerService {
// this might not be ideal // this might not be ideal
this.timer.finishedAt = null; this.timer.finishedAt = null;
this.timer.expectedFinish = this._getExpectedFinish(); this.timer.expectedFinish = getExpectedFinish(
this.timer.startedAt,
this.timer.finishedAt,
this.timer.duration,
this.pausedTime,
this.timer.addedTime,
);
if (this.timer.startedAt === null) { if (this.timer.startedAt === null) {
this.timer.current = timer.duration; this.timer.current = timer.duration;
} }
@@ -157,8 +130,8 @@ export class TimerService {
this.timer.current = timer.duration; this.timer.current = timer.duration;
this.playback = 'armed'; this.playback = 'armed';
this.timer.timerType = timer.timerType; this.timer.timerType = timer.timerType;
this._pausedInterval = 0; this.pausedTime = 0;
this._pausedAt = 0; this.pausedAt = 0;
this._onLoad(); this._onLoad();
} }
@@ -182,19 +155,25 @@ export class TimerService {
return; return;
} }
this.timer.clock = TimerService.getCurrentTime(); this.timer.clock = clock.timeNow();
// add paused time // add paused time
if (this._pausedInterval) { if (this.pausedTime) {
this.timer.addedTime += this._pausedInterval; this.timer.addedTime += this.pausedTime;
this._pausedAt = null; this.pausedAt = null;
this._pausedInterval = 0; this.pausedTime = 0;
} else { } else {
this.timer.startedAt = this.timer.clock; this.timer.startedAt = this.timer.clock;
} }
this.playback = 'play'; this.playback = 'play';
this.timer.expectedFinish = this._getExpectedFinish(); this.timer.expectedFinish = getExpectedFinish(
this.timer.startedAt,
this.timer.finishedAt,
this.timer.duration,
this.pausedTime,
this.timer.addedTime,
);
this._onStart(); this._onStart();
} }
@@ -214,8 +193,8 @@ export class TimerService {
} }
this.playback = 'pause'; this.playback = 'pause';
this.timer.clock = TimerService.getCurrentTime(); this.timer.clock = clock.timeNow();
this._pausedAt = this.timer.clock; this.pausedAt = this.timer.clock;
this._onPause(); this._onPause();
} }
@@ -250,14 +229,12 @@ export class TimerService {
} }
this.timer.addedTime += amount; this.timer.addedTime += amount;
this.timer.current += amount;
this.timer.elapsed += amount;
// handle edge cases // handle edge cases
if (amount < 0 && Math.abs(amount) > this.timer.current) { if (amount < 0 && Math.abs(amount) > this.timer.current) {
if (this.timer.finishedAt === null) { if (this.timer.finishedAt === null) {
// if we will make the clock negative // if we will make the clock negative
this.timer.finishedAt = TimerService.getCurrentTime(); this.timer.finishedAt = clock.timeNow();
} }
} else if (this.timer.current < 0 && this.timer.current + amount > 0) { } else if (this.timer.current < 0 && this.timer.current + amount > 0) {
// clock will go from negative to positive // clock will go from negative to positive
@@ -269,7 +246,7 @@ export class TimerService {
} }
update() { update() {
this.timer.clock = TimerService.getCurrentTime(); this.timer.clock = clock.timeNow();
if (this.playback === 'roll') { if (this.playback === 'roll') {
const tempCurrentTimer = { const tempCurrentTimer = {
@@ -283,7 +260,7 @@ export class TimerService {
clock: this.timer.clock, clock: this.timer.clock,
secondaryTimer: this.timer.secondaryTimer, secondaryTimer: this.timer.secondaryTimer,
_secondaryTarget: this._secondaryTarget, _secondaryTarget: this.secondaryTarget,
}; };
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer); const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer);
@@ -303,45 +280,52 @@ export class TimerService {
// we only update timer if a timer has been started // we only update timer if a timer has been started
if (this.timer.startedAt !== null) { if (this.timer.startedAt !== null) {
if (this.playback === 'pause') { if (this.playback === 'pause') {
this._pausedInterval = this.timer.clock - this._pausedAt; this.pausedTime = this.timer.clock - this.pausedAt;
} }
this.timer.current =
this.timer.startedAt + this.timer.duration + this.timer.addedTime + this._pausedInterval - this.timer.clock;
this.timer.elapsed = this.timer.duration - this.timer.current;
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) { if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
this.timer.finishedAt = this.timer.clock; this.timer.finishedAt = this.timer.clock;
this._onFinish(); this._onFinish();
} else { } else {
this.timer.finishedAt = null; this.timer.expectedFinish = getExpectedFinish(
this.timer.startedAt,
this.timer.finishedAt,
this.timer.duration,
this.pausedTime,
this.timer.addedTime,
);
} }
this.timer.expectedFinish = this._getExpectedFinish(); this.timer.current = getCurrent(
this.timer.startedAt,
this.timer.duration,
this.timer.addedTime,
this.pausedTime,
this.timer.clock,
);
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock);
} }
} }
this._onUpdate(); this._onUpdate();
} }
_onUpdate() { _onUpdate() {
eventStore.set('playback', this.playback);
eventStore.set('timer', this.timer); eventStore.set('timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onUpdate); integrationService.dispatch(TimerLifeCycle.onUpdate);
} }
_onFinish() { _onFinish() {
eventStore.set('playback', this.playback);
eventStore.set('timer', this.timer); eventStore.set('timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onFinish); integrationService.dispatch(TimerLifeCycle.onFinish);
} }
roll(currentEvent, nextEvent, timers) { roll(currentEvent, nextEvent, timers) {
this._clear(); this._clear();
this.timer.clock = TimerService.getCurrentTime(); this.timer.clock = clock.timeNow();
if (currentEvent) { if (currentEvent) {
// there is something running, load // there is something running, load
this.timer.secondaryTimer = null; this.timer.secondaryTimer = null;
this._secondaryTarget = null; this.secondaryTarget = null;
this.loadedTimerId = currentEvent.id; this.loadedTimerId = currentEvent.id;
this.timer.startedAt = currentEvent.timeStart; this.timer.startedAt = currentEvent.timeStart;
@@ -351,7 +335,7 @@ export class TimerService {
} else if (nextEvent) { } else if (nextEvent) {
// nothing now, but something coming up // nothing now, but something coming up
this.timer.secondaryTimer = nextEvent.timeStart - this.timer.clock; this.timer.secondaryTimer = nextEvent.timeStart - this.timer.clock;
this._secondaryTarget = nextEvent.timeStart; this.secondaryTarget = nextEvent.timeStart;
} }
this.playback = 'roll'; this.playback = 'roll';
@@ -0,0 +1,143 @@
import { getCurrent, getElapsed, getExpectedFinish } from '../timerUtils.js';
describe('getExpectedFinish()', () => {
it('is null if we havent started', () => {
const startedAt = null;
const finishedAt = null;
const duration = 10;
const pausedTime = 0;
const addedTime = 0;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(null);
});
it('is finishedAt if defined', () => {
const startedAt = 10;
const finishedAt = 20;
const duration = 10;
const pausedTime = 0;
const addedTime = 0;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(finishedAt);
});
it('calculates the finish time', () => {
const startedAt = 1;
const finishedAt = null;
const duration = 10;
const pausedTime = 0;
const addedTime = 0;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(11);
});
it('adds paused and added times', () => {
const startedAt = 1;
const finishedAt = null;
const duration = 10;
const pausedTime = 10;
const addedTime = 10;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(31);
});
it('added time could be negative', () => {
const startedAt = 1;
const finishedAt = null;
const duration = 10;
const pausedTime = 10;
const addedTime = -10;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(11);
});
it('user could add enough time for it to be negative', () => {
const startedAt = 1;
const finishedAt = null;
const duration = 10;
const pausedTime = 0;
const addedTime = -100;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(1);
});
it('timer can have no duration', () => {
const startedAt = 1;
const finishedAt = null;
const duration = 0;
const pausedTime = 0;
const addedTime = 0;
const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
expect(calculatedFinish).toBe(1);
});
});
describe('getCurrent()', () => {
it('is null if it hasnt started', () => {
const startedAt = null;
const duration = 0;
const pausedTime = 0;
const addedTime = 0;
const clock = 0;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(null);
});
it('is the remaining time in clock', () => {
const startedAt = 0;
const duration = 10;
const pausedTime = 0;
const addedTime = 0;
const clock = 1;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(9);
});
it('accounts for added times', () => {
const startedAt = 0;
const duration = 10;
const pausedTime = 5;
const addedTime = 5;
const clock = 1;
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(current).toBe(19);
});
});
describe('getElapsedTime()', () => {
it('time since we started', () => {
const startedAt = 0;
const clock = 5;
const elapsed = getElapsed(startedAt, clock);
expect(elapsed).toBe(5);
});
it('clock cannot be lower than started time', () => {
const startedAt = 10;
const clock = 5;
expect(() => getElapsed(startedAt, clock)).toThrow();
});
});
describe('getExpectedFinish() getElapsedTime() and getCurrentTime() combined', () => {
it('without added times, they combine to be duration', () => {
const startedAt = 0;
const duration = 10;
const finishedAt = null;
const pausedTime = 0;
const addedTime = 0;
const clock = 0;
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
const elapsed = getElapsed(startedAt, clock);
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(expectedFinish).toBe(10);
expect(elapsed).toBe(0);
expect(current).toBe(10);
expect(elapsed + current).toBe(10);
});
it('added times influence expected finish', () => {
const startedAt = 0;
const duration = 10;
const finishedAt = null;
const pausedTime = 1;
const addedTime = 2;
const clock = 5;
const expectedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime);
const elapsed = getElapsed(startedAt, clock);
const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock);
expect(expectedFinish).toBe(13);
expect(elapsed).toBe(5);
expect(current).toBe(8);
});
});
+48
View File
@@ -0,0 +1,48 @@
type MaybeNumber = number | null;
/**
* Calculates expected finish time of a running timer
*/
export function getExpectedFinish(
startedAt: MaybeNumber,
finishedAt: MaybeNumber,
duration: number,
pausedTime: number,
addedTime: number,
) {
if (startedAt === null) {
return null;
}
if (finishedAt !== null) {
return finishedAt;
}
return Math.max(startedAt + duration + pausedTime + addedTime, startedAt);
}
/**
* Calculates running countdown
*/
export function getCurrent(
startedAt: MaybeNumber,
duration: number,
addedTime: number,
pausedTime: number,
clock: number,
) {
if (startedAt === null) {
return null;
}
return startedAt + duration + addedTime + pausedTime - clock;
}
/**
* Calculates elapsed time
*/
export function getElapsed(startedAt: number, clock: number) {
if (startedAt > clock) {
throw new Error('clock cannot be higher than startedAt');
}
return clock - startedAt;
}