hotfix/92-osc timer is absolute (#92)

Fixes issues with approximating timers in the app views
This commit is contained in:
Carlos Valente
2022-01-14 20:47:39 +01:00
committed by GitHub
parent 48093b8651
commit ba782d5d22
13 changed files with 90 additions and 99 deletions
@@ -1,15 +1,16 @@
import { memo } from 'react'; import { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig'; import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import styles from './Countdown.module.css'; import styles from './Countdown.module.css';
const Countdown = ({ time, small, negative, hideZeroHours }) => { const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
// prepare display string // prepare display string
const display = const display =
time != null && !isNaN(time) time != null && !isNaN(time)
? formatDisplay(time, hideZeroHours) ? formatDisplay(time, hideZeroHours)
: '-- : -- : --'; : '-- : -- : --';
const colour = negative ? '#ff7597' : '#fffffa'; const colour = isNegative ? '#ff7597' : '#fffffa';
return ( return (
<div <div
@@ -22,3 +23,10 @@ const Countdown = ({ time, small, negative, hideZeroHours }) => {
}; };
export default memo(Countdown); export default memo(Countdown);
Countdown.propTypes = {
time: PropTypes.number.isRequired,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHour: PropTypes.bool,
};
@@ -6,6 +6,7 @@ import {
forgivingStringToMillis, forgivingStringToMillis,
timeStringToMillis, timeStringToMillis,
} from '../dateConfig'; } from '../dateConfig';
import { stringFromMillis } from 'ontime-utils/time';
describe('test string from formatDisplay function', () => { describe('test string from formatDisplay function', () => {
it('test with null values', () => { it('test with null values', () => {
@@ -49,6 +50,13 @@ describe('test string from formatDisplay function', () => {
}); });
}); });
describe('test formatDisplay handles partial secs', () => {
it('test with 1795829', () => {
const t = { val: 1795829, result: '00:29:55' };
expect(stringFromMillis(t.val)).toBe(t.result);
});
});
describe('test string from formatDisplay function with hidezero', () => { describe('test string from formatDisplay function with hidezero', () => {
it('test with null values', () => { it('test with null values', () => {
const t = { val: null, result: '00:00' }; const t = { val: null, result: '00:00' };
@@ -94,22 +102,22 @@ describe('test string from formatDisplay function with hidezero', () => {
describe('test millisToSeconds function', () => { describe('test millisToSeconds function', () => {
it('test with null values', () => { it('test with null values', () => {
const t = { val: null, result: 0 }; const t = { val: null, result: 0 };
expect(millisToSeconds(t.val, false)).toBe(t.result); expect(millisToSeconds(t.val)).toBe(t.result);
}); });
it('test with valid millis', () => { it('test with valid millis', () => {
const t = { val: 3600000, result: 3600 }; const t = { val: 3600000, result: 3600 };
expect(millisToSeconds(t.val, false)).toBe(t.result); expect(millisToSeconds(t.val)).toBe(t.result);
}); });
it('test with negative millis', () => { it('test with negative millis', () => {
const t = { val: -3600000, result: -3600 }; const t = { val: -3600000, result: -3600 };
expect(millisToSeconds(t.val, false)).toBe(t.result); expect(millisToSeconds(t.val)).toBe(t.result);
}); });
it('test with 0', () => { it('test with 0', () => {
const t = { val: 0, result: 0 }; const t = { val: 0, result: 0 };
expect(millisToSeconds(t.val, false)).toBe(t.result); expect(millisToSeconds(t.val)).toBe(t.result);
}); });
it('test with -0', () => { it('test with -0', () => {
@@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
const areEqual = (prevProps, nextProps) => { const areEqual = (prevProps, nextProps) => {
return ( return (
prevProps.timer.running === nextProps.timer.running && prevProps.timer.running === nextProps.timer.running &&
prevProps.timer.isNegative === nextProps.timer.isNegative &&
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish && prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
prevProps.timer.startedAt === nextProps.timer.startedAt && prevProps.timer.startedAt === nextProps.timer.startedAt &&
prevProps.playback === nextProps.playback && prevProps.playback === nextProps.playback &&
@@ -21,7 +22,6 @@ const PlaybackTimer = (props) => {
const { timer, playback, handleIncrement, selectedId } = props; const { timer, playback, handleIncrement, selectedId } = props;
const started = stringFromMillis(timer.startedAt, true); const started = stringFromMillis(timer.startedAt, true);
const finish = stringFromMillis(timer.expectedFinish, true); const finish = stringFromMillis(timer.expectedFinish, true);
const isNegative = timer.running < 0;
const isRolling = playback === 'roll'; const isRolling = playback === 'roll';
const isWaiting = timer.secondary > 0 && timer.running == null; const isWaiting = timer.secondary > 0 && timer.running == null;
const disableButtons = selectedId == null || isRolling; const disableButtons = selectedId == null || isRolling;
@@ -42,15 +42,15 @@ const PlaybackTimer = (props) => {
<div className={isRolling ? style.indRollActive : style.indRoll} /> <div className={isRolling ? style.indRollActive : style.indRoll} />
</Tooltip> </Tooltip>
<div <div
className={isNegative ? style.indNegativeActive : style.indNegative} className={timer.isNegative ? style.indNegativeActive : style.indNegative}
/> />
<div className={style.indDelay} /> <div className={style.indDelay} />
</div> </div>
<div className={style.timer}> <div className={style.timer}>
<Countdown <Countdown
time={isWaiting ? timer.secondary : timer.running} time={isWaiting ? timer.secondary : timer.running}
isNegative={timer.isNegative}
small small
negative={isNegative}
/> />
</div> </div>
{isWaiting ? ( {isWaiting ? (
+2 -1
View File
@@ -30,6 +30,7 @@ const withSocket = (Component) => {
const [timer, setTimer] = useState({ const [timer, setTimer] = useState({
clock: null, clock: null,
running: null, running: null,
isNegative: null,
startedAt: null, startedAt: null,
expectedFinish: null, expectedFinish: null,
}); });
@@ -226,7 +227,7 @@ const withSocket = (Component) => {
// get clock string // get clock string
const timeManager = { const timeManager = {
...timer, ...timer,
finished: playback === 'start' && timer.running <= 0 && timer.startedAt, finished: playback === 'start' && timer.isNegative && timer.startedAt,
clock: stringFromMillis(timer.clock), clock: stringFromMillis(timer.clock),
clockNoSeconds: stringFromMillis(timer.clock, false), clockNoSeconds: stringFromMillis(timer.clock, false),
playstate: playback, playstate: playback,
@@ -27,7 +27,6 @@ export default function StageManager(props) {
}, [backstageEvents]); }, [backstageEvents]);
// Format messages // Format messages
const showPubl = publ.text !== '' && publ.visible; const showPubl = publ.text !== '' && publ.visible;
let stageTimer; let stageTimer;
@@ -35,7 +34,7 @@ export default function StageManager(props) {
stageTimer = '- - : - -'; stageTimer = '- - : - -';
} else { } else {
stageTimer = formatDisplay(Math.abs(time.running), true); stageTimer = formatDisplay(Math.abs(time.running), true);
if (time.running < 0) stageTimer = `-${stageTimer}`; if (time.isNegative) stageTimer = `-${stageTimer}`;
} }
// motion // motion
@@ -52,7 +52,7 @@ export default function Pip(props) {
const showInfo = const showInfo =
general.backstageInfo !== '' && general.backstageInfo != null; general.backstageInfo !== '' && general.backstageInfo != null;
let stageTimer = formatDisplay(Math.abs(time.running), true); let stageTimer = formatDisplay(Math.abs(time.running), true);
if (time.running < 0) stageTimer = `-${stageTimer}`; if (time.isNegative) stageTimer = `-${stageTimer}`;
return ( return (
<div className={style.container__gray}> <div className={style.container__gray}>
@@ -49,7 +49,7 @@ export default function StudioClock(props) {
> >
{title.titleNext} {title.titleNext}
</div> </div>
<div className={time.running > 0 ? style.nextCountdown : style.nextCountdown__overtime}> <div className={time.isNegative ? style.nextCountdown : style.nextCountdown__overtime}>
{selectedId != null && formatDisplay(time.running)} {selectedId != null && formatDisplay(time.running)}
</div> </div>
<div className={style.indicators}> <div className={style.indicators}>
@@ -18,11 +18,7 @@ export default function MinimalTimer(props) {
return ( return (
<div className={time.finished ? style.containerFinished : style.container}> <div className={time.finished ? style.containerFinished : style.container}>
<div <div className={showOverlay ? style.messageOverlayActive : style.messageOverlay}>
className={
showOverlay ? style.messageOverlayActive : style.messageOverlay
}
>
<div className={style.message}>{pres.text}</div> <div className={style.message}>{pres.text}</div>
</div> </div>
<NavLogo /> <NavLogo />
@@ -30,7 +26,7 @@ export default function MinimalTimer(props) {
style={{ fontSize: `${89 / (clean.length - 1)}vw` }} style={{ fontSize: `${89 / (clean.length - 1)}vw` }}
className={isPlaying ? style.timer : style.timerPaused} className={isPlaying ? style.timer : style.timerPaused}
> >
{time.running < 0 ? `-${timer}` : timer} {time.isNegative ? `-${timer}` : timer}
</div> </div>
</div> </div>
); );
+5 -1
View File
@@ -37,7 +37,11 @@ export default function Timer(props) {
// show timer if end message is empty // show timer if end message is empty
const endMessage = const endMessage =
general.endMessage == null || general.endMessage === '' ? ( general.endMessage == null || general.endMessage === '' ? (
<Countdown time={time.running} hideZeroHours negative /> <Countdown
time={time.running}
isNegative={time.isNegative}
hideZeroHours
/>
) : ( ) : (
general.endMessage general.endMessage
); );
+17 -63
View File
@@ -1,11 +1,6 @@
import { Timer } from './Timer.js'; import { Timer } from './Timer.js';
import { Server } from 'socket.io'; import { Server } from 'socket.io';
import { import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js';
DAY_TO_MS,
getSelectionByRoll,
replacePlaceholder,
updateRoll,
} from './classUtils.js';
import { OSCIntegration } from './integrations/Osc.js'; import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js'; import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../utils/url.js'; import { cleanURL } from '../utils/url.js';
@@ -90,10 +85,7 @@ export class EventTimer extends Timer {
}); });
// set recurrent emits // set recurrent emits
this._interval = setInterval( this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
() => this.runCycle(),
timerConfig?.refresh || 1000
);
// listen to new connections // listen to new connections
this._listenToConnections(); this._listenToConnections();
@@ -306,10 +298,7 @@ export class EventTimer extends Timer {
// _finish at is only set when an event is loaded // _finish at is only set when an event is loaded
if (this._finishAt > 0) { if (this._finishAt > 0) {
this.sendOsc(this.osc.implemented.play); this.sendOsc(this.osc.implemented.play);
this.sendOsc( this.sendOsc(this.osc.implemented.eventNumber, this.selectedEventIndex || 0);
this.osc.implemented.eventNumber,
this.selectedEventIndex || 0
);
} }
// check integrations - http // check integrations - http
if (h?.onLoad?.enabled) { if (h?.onLoad?.enabled) {
@@ -330,18 +319,9 @@ export class EventTimer extends Timer {
if (this.state === 'start' || this.state === 'roll') { if (this.state === 'start' || this.state === 'roll') {
if (this.current != null && this.secondaryTimer == null) { if (this.current != null && this.secondaryTimer == null) {
this.sendOsc(this.osc.implemented.time, this.timeTag); this.sendOsc(this.osc.implemented.time, this.timeTag);
this.sendOsc( this.sendOsc(this.osc.implemented.overtime, this.current > 0 ? 0 : 1);
this.osc.implemented.overtime, this.sendOsc(this.osc.implemented.title, this.titles?.titleNow || '');
this.current > 0 ? 0 : 1 this.sendOsc(this.osc.implemented.presenter, this.titles?.presenterNow || '');
);
this.sendOsc(
this.osc.implemented.title,
this.titles?.titleNow || ''
);
this.sendOsc(
this.osc.implemented.presenter,
this.titles?.presenterNow || ''
);
} }
} }
@@ -474,17 +454,13 @@ export class EventTimer extends Timer {
selectedEventId: this.selectedEventId, selectedEventId: this.selectedEventId,
current: this.current, current: this.current,
// safeguard on midnight rollover // safeguard on midnight rollover
_finishAt: _finishAt: this._finishAt >= this._startedAt ? this._finishAt : this._finishAt + DAY_TO_MS,
this._finishAt >= this._startedAt
? this._finishAt
: this._finishAt + DAY_TO_MS,
clock: this.clock, clock: this.clock,
secondaryTimer: this.secondaryTimer, secondaryTimer: this.secondaryTimer,
_secondaryTarget: this._secondaryTarget, _secondaryTarget: this._secondaryTarget,
}; };
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(u);
updateRoll(u);
this.current = updatedTimer; this.current = updatedTimer;
this.secondaryTimer = updatedSecondaryTimer; this.secondaryTimer = updatedSecondaryTimer;
@@ -560,9 +536,7 @@ export class EventTimer extends Timer {
// keep track of connections // keep track of connections
this._numClients++; this._numClients++;
this._clientNames[socket.id] = getRandomName(); this._clientNames[socket.id] = getRandomName();
const m = `${this._numClients} Clients with new connection: ${ const m = `${this._numClients} Clients with new connection: ${this._clientNames[socket.id]}`;
this._clientNames[socket.id]
}`;
this.info('CLIENT', m); this.info('CLIENT', m);
// send state // send state
@@ -579,9 +553,7 @@ export class EventTimer extends Timer {
/********************************/ /********************************/
socket.on('disconnect', () => { socket.on('disconnect', () => {
this._numClients--; this._numClients--;
const m = `${this._numClients} Clients with disconnection: ${ const m = `${this._numClients} Clients with disconnection: ${this._clientNames[socket.id]}`;
this._clientNames[socket.id]
}`;
delete this._clientNames[socket.id]; delete this._clientNames[socket.id];
this.info('CLIENT', m); this.info('CLIENT', m);
}); });
@@ -807,9 +779,7 @@ export class EventTimer extends Timer {
} else if (this.selectedEventId != null) { } else if (this.selectedEventId != null) {
// handle reload selected // handle reload selected
// Look for event (order might have changed) // Look for event (order might have changed)
const eventIndex = this._eventlist.findIndex( const eventIndex = this._eventlist.findIndex((e) => e.id === this.selectedEventId);
(e) => e.id === this.selectedEventId
);
// Maybe is missing // Maybe is missing
if (eventIndex === -1) { if (eventIndex === -1) {
@@ -849,10 +819,7 @@ export class EventTimer extends Timer {
if (e.id === this.selectedEventId) { if (e.id === this.selectedEventId) {
// handle reload selected // handle reload selected
// Reload data if running // Reload data if running
let type = let type = this.selectedEventId === id && this._startedAt != null ? 'reload' : 'load';
this.selectedEventId === id && this._startedAt != null
? 'reload'
: 'load';
this.loadEvent(this.selectedEventIndex, type); this.loadEvent(this.selectedEventIndex, type);
} else if (e.id === this.nextEventId) { } else if (e.id === this.nextEventId) {
// roll needs to recalculate // roll needs to recalculate
@@ -898,9 +865,7 @@ export class EventTimer extends Timer {
} }
// update selected event index // update selected event index
this.selectedEventIndex = this._eventlist.findIndex( this.selectedEventIndex = this._eventlist.findIndex((e) => e.id === this.selectedEventId);
(e) => e.id === this.selectedEventId
);
// reload titles if necessary // reload titles if necessary
if (eventId === this.nextEventId || eventId === this.nextPublicEventId) { if (eventId === this.nextEventId || eventId === this.nextPublicEventId) {
@@ -1005,10 +970,7 @@ export class EventTimer extends Timer {
// iterate backwards to find it // iterate backwards to find it
for (let i = this.selectedEventIndex; i >= 0; i--) { for (let i = this.selectedEventIndex; i >= 0; i--) {
if ( if (this._eventlist[i].type === 'event' && this._eventlist[i].isPublic) {
this._eventlist[i].type === 'event' &&
this._eventlist[i].isPublic
) {
this._loadThisTitles(this._eventlist[i], 'now-public'); this._loadThisTitles(this._eventlist[i], 'now-public');
break; break;
} }
@@ -1221,15 +1183,8 @@ export class EventTimer extends Timer {
this._resetSelection(); this._resetSelection();
} }
const { const { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timers, timeToNext } =
nowIndex, getSelectionByRoll(this._eventlist, now);
nowId,
publicIndex,
nextIndex,
publicNextIndex,
timers,
timeToNext,
} = getSelectionByRoll(this._eventlist, now);
// nothing to play, unload // nothing to play, unload
if (nowIndex === null && nextIndex === null) { if (nowIndex === null && nextIndex === null) {
@@ -1334,8 +1289,7 @@ export class EventTimer extends Timer {
// change playstate // change playstate
this.pause(); this.pause();
const gotoEvent = const gotoEvent = this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
if (gotoEvent === this.selectedEventIndex) return; if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent); this.loadEvent(gotoEvent);
+4 -4
View File
@@ -50,8 +50,7 @@ export class Timer {
if (this._startedAt == null) this._startedAt = now; if (this._startedAt == null) this._startedAt = now;
// update current timer // update current timer
this.current = this.current = this._startedAt + this.duration + this._pausedTotal - now;
this._startedAt + this.duration + this._pausedTotal - now;
// enable flag // enable flag
checkFinish = true; checkFinish = true;
@@ -93,8 +92,8 @@ export class Timer {
// helpers // helpers
static toSeconds(millis) { static toSeconds(millis) {
if (millis == null) return null; if (millis == null) return 0;
return Math.ceil(millis * 0.001); return millis < 0 ? Math.ceil(millis * 0.001) : Math.floor(millis * 0.001);
} }
// get current time in epoc // get current time in epoc
@@ -151,6 +150,7 @@ export class Timer {
getTimeObject() { getTimeObject() {
return { return {
clock: this.clock, clock: this.clock,
isNegative: this.current < 0,
running: Timer.toSeconds(this.current), running: Timer.toSeconds(this.current),
secondary: Timer.toSeconds(this.secondaryTimer), secondary: Timer.toSeconds(this.secondaryTimer),
durationSeconds: Timer.toSeconds(this.duration), durationSeconds: Timer.toSeconds(this.duration),
+21 -11
View File
@@ -1,4 +1,4 @@
import {Timer} from "../Timer"; import { Timer } from '../Timer';
test('object instantiates correctly', () => { test('object instantiates correctly', () => {
const t = new Timer(); const t = new Timer();
@@ -21,15 +21,15 @@ test('object instantiates correctly', () => {
test('convert between mills and seconds correctly', () => { test('convert between mills and seconds correctly', () => {
expect(Timer.toSeconds(10000)).toBe(10); expect(Timer.toSeconds(10000)).toBe(10);
expect(Timer.toSeconds(9016)).toBe(10); expect(Timer.toSeconds(9016)).toBe(9);
expect(Timer.toSeconds(8016)).toBe(9); expect(Timer.toSeconds(8016)).toBe(8);
expect(Timer.toSeconds(7010)).toBe(8); expect(Timer.toSeconds(7010)).toBe(7);
expect(Timer.toSeconds(6006)).toBe(7); expect(Timer.toSeconds(6006)).toBe(6);
expect(Timer.toSeconds(4999)).toBe(5); expect(Timer.toSeconds(4999)).toBe(4);
expect(Timer.toSeconds(2995)).toBe(3); expect(Timer.toSeconds(2995)).toBe(2);
expect(Timer.toSeconds(1991)).toBe(2); expect(Timer.toSeconds(1991)).toBe(1);
expect(Timer.toSeconds(992)).toBe(1); expect(Timer.toSeconds(992)).toBe(0);
expect(Timer.toSeconds(127)).toBe(1); expect(Timer.toSeconds(127)).toBe(0);
expect(Timer.toSeconds(0)).toBe(0); expect(Timer.toSeconds(0)).toBe(0);
expect(Timer.toSeconds(-0)).toBe(-0); expect(Timer.toSeconds(-0)).toBe(-0);
expect(Timer.toSeconds(-127)).toBe(-0); expect(Timer.toSeconds(-127)).toBe(-0);
@@ -41,4 +41,14 @@ test('convert between mills and seconds correctly', () => {
expect(Timer.toSeconds(-7010)).toBe(-7); expect(Timer.toSeconds(-7010)).toBe(-7);
expect(Timer.toSeconds(-8016)).toBe(-8); expect(Timer.toSeconds(-8016)).toBe(-8);
expect(Timer.toSeconds(-10000)).toBe(-10); expect(Timer.toSeconds(-10000)).toBe(-10);
}); });
test('converting between millis to seconds handles partials correctly', () => {
const finish = 82162001;
const now = 80364519;
const runningMs = finish - now;
expect(Timer.toSeconds(runningMs)).toBe(1797);
expect(Timer.toSeconds(1800000)).toBe(1800);
expect(Timer.toSeconds(1799761)).toBe(1799);
});
+11
View File
@@ -57,6 +57,17 @@ describe('test string to millis function', () => {
}); });
}); });
describe('test stringFromMillis handles partial secs', () => {
it('test with 1795829', () => {
const t = { val: 1795829, result: '00:29:55' };
expect(stringFromMillis(t.val)).toBe(t.result);
});
it('test with 1797482', () => {
const t = { val: 1797482, result: '00:29:57' };
expect(stringFromMillis(t.val)).toBe(t.result);
});
});
describe('test excel date parser', () => { describe('test excel date parser', () => {
it('handles an invalid date string', () => { it('handles an invalid date string', () => {
const s = 'hello'; const s = 'hello';