mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 23:19:09 +00:00
refactor: runtime service (#715)
* refactor: rename PlaybackService > RuntimeService * refactor: stateful runtime service * refactor: merge event loading * refactor: roll is part of state mutation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import { Playback, RuntimeStore } from 'ontime-types';
|
import { Playback, RuntimeStore } from 'ontime-types';
|
||||||
import { createWithEqualityFn, useStoreWithEqualityFn } from 'zustand/traditional'
|
import { createWithEqualityFn, useStoreWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
export const runtimeStorePlaceholder: RuntimeStore = {
|
export const runtimeStorePlaceholder: RuntimeStore = {
|
||||||
timer: {
|
timer: {
|
||||||
@@ -12,7 +12,6 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
finishedAt: null,
|
finishedAt: null,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
selectedEventId: null,
|
|
||||||
duration: null,
|
duration: null,
|
||||||
timerType: null,
|
timerType: null,
|
||||||
endAction: null,
|
endAction: null,
|
||||||
@@ -55,11 +54,12 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
|||||||
|
|
||||||
const deepCompare = <T>(a: T, b: T) => isEqual(a, b);
|
const deepCompare = <T>(a: T, b: T) => isEqual(a, b);
|
||||||
|
|
||||||
export const runtime = createWithEqualityFn<RuntimeStore>(() => ({
|
export const runtime = createWithEqualityFn<RuntimeStore>(
|
||||||
...runtimeStorePlaceholder,
|
() => ({
|
||||||
}), deepCompare);
|
...runtimeStorePlaceholder,
|
||||||
|
}),
|
||||||
|
deepCompare,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
|
||||||
export const useRuntimeStore = <T>(
|
useStoreWithEqualityFn(runtime, selector, deepCompare);
|
||||||
selector: (state: RuntimeStore) => T,
|
|
||||||
) => useStoreWithEqualityFn(runtime, selector, deepCompare);
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
const isPlaying = playback === Playback.Play;
|
const isPlaying = playback === Playback.Play;
|
||||||
const isPaused = playback === Playback.Pause;
|
const isPaused = playback === Playback.Pause;
|
||||||
const isArmed = playback === Playback.Armed;
|
const isArmed = playback === Playback.Armed;
|
||||||
const isStopped = playback === Playback.Stop;
|
|
||||||
|
|
||||||
const isFirst = selectedEventIndex === 0;
|
const isFirst = selectedEventIndex === 0;
|
||||||
const isLast = selectedEventIndex === numEvents - 1;
|
const isLast = selectedEventIndex === numEvents - 1;
|
||||||
@@ -42,6 +41,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
const disablePause = !playbackCan.pause;
|
const disablePause = !playbackCan.pause;
|
||||||
const disableRoll = !playbackCan.roll || noEvents;
|
const disableRoll = !playbackCan.roll || noEvents;
|
||||||
const disableStop = !playbackCan.stop;
|
const disableStop = !playbackCan.stop;
|
||||||
|
const disableReload = !playbackCan.reload;
|
||||||
|
|
||||||
const goModeText = selectedEventIndex === null || isArmed ? 'Start' : 'Next';
|
const goModeText = selectedEventIndex === null || isArmed ? 'Start' : 'Next';
|
||||||
const goModeAction = () => {
|
const goModeAction = () => {
|
||||||
@@ -83,7 +83,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
<IoTime />
|
<IoTime />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
|
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
|
||||||
<TapButton onClick={setPlayback.reload} disabled={isStopped}>
|
<TapButton onClick={setPlayback.reload} disabled={disableReload}>
|
||||||
<IoReload className={style.invertX} />
|
<IoReload className={style.invertX} />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
+32
-29
@@ -31,19 +31,18 @@ import { DataProvider } from './classes/data-provider/DataProvider.js';
|
|||||||
import { dbLoadingProcess } from './modules/loadDb.js';
|
import { dbLoadingProcess } from './modules/loadDb.js';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { eventTimer } from './services/TimerService.js';
|
import { EventLoader } from './classes/event-loader/EventLoader.js';
|
||||||
import { eventLoader } from './classes/event-loader/EventLoader.js';
|
|
||||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||||
import { logger } from './classes/Logger.js';
|
import { logger } from './classes/Logger.js';
|
||||||
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
import { oscIntegration } from './services/integration-service/OscIntegration.js';
|
||||||
import { httpIntegration } from './services/integration-service/HttpIntegration.js';
|
import { httpIntegration } from './services/integration-service/HttpIntegration.js';
|
||||||
import { populateStyles } from './modules/loadStyles.js';
|
import { populateStyles } from './modules/loadStyles.js';
|
||||||
import { eventStore } from './stores/EventStore.js';
|
import { eventStore } from './stores/EventStore.js';
|
||||||
import { PlaybackService } from './services/PlaybackService.js';
|
import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||||
import { restoreService } from './services/RestoreService.js';
|
import { restoreService } from './services/RestoreService.js';
|
||||||
import { messageService } from './services/message-service/MessageService.js';
|
import { messageService } from './services/message-service/MessageService.js';
|
||||||
import { populateDemo } from './modules/loadDemo.js';
|
import { populateDemo } from './modules/loadDemo.js';
|
||||||
import { state } from './state.js';
|
import { state, stateMutations } from './state.js';
|
||||||
|
|
||||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||||
|
|
||||||
@@ -155,46 +154,50 @@ export const startServer = async () => {
|
|||||||
expressServer = http.createServer(app);
|
expressServer = http.createServer(app);
|
||||||
|
|
||||||
socket.init(expressServer);
|
socket.init(expressServer);
|
||||||
eventLoader.init();
|
|
||||||
|
|
||||||
// load restore point if it exists
|
|
||||||
const maybeRestorePoint = await restoreService.load();
|
|
||||||
if (maybeRestorePoint) {
|
|
||||||
logger.info(LogOrigin.Server, 'Found resumable state');
|
|
||||||
PlaybackService.resume(maybeRestorePoint);
|
|
||||||
}
|
|
||||||
eventTimer.init();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module initialises the services and provides initial payload for the store
|
* Module initialises the services and provides initial payload for the store
|
||||||
* Currently registered objects in store
|
* Currently registered objects in store
|
||||||
* - Timer Service timer
|
*
|
||||||
* - Timer Service playback
|
|
||||||
* - Message Service timerMessage
|
* - Message Service timerMessage
|
||||||
* - Message Service publicMessage
|
* - Message Service publicMessage
|
||||||
* - Message Service lowerMessage
|
* - Message Service lowerMessage
|
||||||
* - Message Service onAir
|
* - Message Service externalMessage
|
||||||
* - Event Loader loaded
|
*
|
||||||
* - Event Loader eventNow
|
* - Runtime Service onAir (derived from playback)
|
||||||
* - Event Loader publicEventNow
|
* - Runtime Service timer
|
||||||
* - Event Loader eventNext
|
* - Runtime Service playback
|
||||||
* - Event Loader publicEventNext
|
* - Runtime Service loaded // TODO: rename to runtime ??
|
||||||
|
* - Runtime Service eventNow
|
||||||
|
* - Runtime Service publicEventNow
|
||||||
|
* - Runtime Service eventNext
|
||||||
|
* - Runtime Service publicEventNext
|
||||||
*/
|
*/
|
||||||
eventStore.init({
|
eventStore.init({
|
||||||
timer: state.timer,
|
|
||||||
playback: state.playback,
|
|
||||||
timerMessage: messageService.timerMessage,
|
timerMessage: messageService.timerMessage,
|
||||||
publicMessage: messageService.publicMessage,
|
publicMessage: messageService.publicMessage,
|
||||||
lowerMessage: messageService.lowerMessage,
|
lowerMessage: messageService.lowerMessage,
|
||||||
externalMessage: messageService.externalMessage,
|
externalMessage: messageService.externalMessage,
|
||||||
onAir: state.playback !== Playback.Stop,
|
onAir: state.playback !== Playback.Stop,
|
||||||
loaded: eventLoader.loaded,
|
timer: state.timer,
|
||||||
eventNow: eventLoader.eventNow,
|
playback: state.playback,
|
||||||
publicEventNow: eventLoader.publicEventNow,
|
loaded: state.runtime,
|
||||||
eventNext: eventLoader.eventNext,
|
eventNow: state.eventNow,
|
||||||
publicEventNext: eventLoader.publicEventNext,
|
publicEventNow: state.publicEventNow,
|
||||||
|
eventNext: state.eventNext,
|
||||||
|
publicEventNext: state.publicEventNext,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// load restore point if it exists
|
||||||
|
const maybeRestorePoint = await restoreService.load();
|
||||||
|
|
||||||
|
// TODO: pass event store to rundownservice
|
||||||
|
runtimeService.init(maybeRestorePoint);
|
||||||
|
|
||||||
|
// TODO: do this on the init of the runtime service
|
||||||
|
const numEvents = EventLoader.getNumEvents();
|
||||||
|
stateMutations.updateNumEvents(numEvents);
|
||||||
|
|
||||||
// eventStore set is a dependency of the services that publish to it
|
// eventStore set is a dependency of the services that publish to it
|
||||||
messageService.init(eventStore.set.bind(eventStore));
|
messageService.init(eventStore.set.bind(eventStore));
|
||||||
|
|
||||||
@@ -277,7 +280,7 @@ export const shutdown = async (exitCode = 0) => {
|
|||||||
|
|
||||||
expressServer?.close();
|
expressServer?.close();
|
||||||
oscServer?.shutdown();
|
oscServer?.shutdown();
|
||||||
eventTimer.shutdown();
|
runtimeService.shutdown();
|
||||||
integrationService.shutdown();
|
integrationService.shutdown();
|
||||||
logger.shutdown();
|
logger.shutdown();
|
||||||
socket.shutdown();
|
socket.shutdown();
|
||||||
|
|||||||
@@ -1,53 +1,19 @@
|
|||||||
import { Loaded, OntimeEvent, SupportedEvent } from 'ontime-types';
|
import { OntimeEvent, isOntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||||
import { getRollTimers } from '../../services/rollUtils.js';
|
|
||||||
import { eventStore } from '../../stores/EventStore.js';
|
|
||||||
|
|
||||||
let instance;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages business logic around loading events
|
* Manages business logic around loading and finding events
|
||||||
*/
|
*/
|
||||||
export class EventLoader {
|
export class EventLoader {
|
||||||
loaded: Loaded;
|
// TODO: migrate logic to RundownService
|
||||||
eventNow: OntimeEvent | null;
|
|
||||||
publicEventNow: OntimeEvent | null;
|
|
||||||
eventNext: OntimeEvent | null;
|
|
||||||
publicEventNext: OntimeEvent | null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (instance) {
|
|
||||||
throw new Error('There can be only one');
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
|
||||||
instance = this;
|
|
||||||
this.eventNow = null;
|
|
||||||
this.publicEventNow = null;
|
|
||||||
this.eventNext = null;
|
|
||||||
this.publicEventNext = null;
|
|
||||||
this.loaded = {
|
|
||||||
selectedEventIndex: null,
|
|
||||||
selectedEventId: null,
|
|
||||||
selectedPublicEventId: null,
|
|
||||||
nextEventId: null,
|
|
||||||
nextPublicEventId: null,
|
|
||||||
numEvents: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to delay init until the store is ready
|
|
||||||
init() {
|
|
||||||
this.reset(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns all events that contain time data
|
* returns all events that contain time data
|
||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
static getTimedEvents(): OntimeEvent[] {
|
static getTimedEvents(): OntimeEvent[] {
|
||||||
return DataProvider.getRundown().filter((event) => event.type === SupportedEvent.Event) as OntimeEvent[];
|
return DataProvider.getRundown().filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,17 +21,15 @@ export class EventLoader {
|
|||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
static getPlayableEvents(): OntimeEvent[] {
|
static getPlayableEvents(): OntimeEvent[] {
|
||||||
return DataProvider.getRundown().filter(
|
return DataProvider.getRundown().filter((event) => isOntimeEvent(event) && !event.skip) as OntimeEvent[];
|
||||||
(event) => event.type === SupportedEvent.Event && !event.skip,
|
|
||||||
) as OntimeEvent[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns number of events
|
* returns number of events
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
static getNumEvents() {
|
static getNumEvents(): number {
|
||||||
return EventLoader.getTimedEvents().length;
|
return EventLoader.getPlayableEvents().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +39,7 @@ export class EventLoader {
|
|||||||
*/
|
*/
|
||||||
static getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
static getEventAtIndex(eventIndex: number): OntimeEvent | undefined {
|
||||||
const timedEvents = EventLoader.getTimedEvents();
|
const timedEvents = EventLoader.getTimedEvents();
|
||||||
return timedEvents?.[eventIndex];
|
return timedEvents.at(eventIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +47,7 @@ export class EventLoader {
|
|||||||
* @param {string} eventId
|
* @param {string} eventId
|
||||||
* @return {object | undefined}
|
* @return {object | undefined}
|
||||||
*/
|
*/
|
||||||
static getEventWithId(eventId): OntimeEvent | undefined {
|
static getEventWithId(eventId: string): OntimeEvent | undefined {
|
||||||
const timedEvents = EventLoader.getTimedEvents();
|
const timedEvents = EventLoader.getTimedEvents();
|
||||||
return timedEvents.find((event) => event.id === eventId);
|
return timedEvents.find((event) => event.id === eventId);
|
||||||
}
|
}
|
||||||
@@ -93,255 +57,50 @@ export class EventLoader {
|
|||||||
* @param {string} cue
|
* @param {string} cue
|
||||||
* @return {object | undefined}
|
* @return {object | undefined}
|
||||||
*/
|
*/
|
||||||
static getEventWithCue(cue) {
|
static getEventWithCue(cue: string): OntimeEvent | undefined {
|
||||||
const timedEvents = EventLoader.getTimedEvents();
|
const timedEvents = EventLoader.getTimedEvents();
|
||||||
return timedEvents.find((event) => event.cue.toLowerCase() === cue.toLowerCase());
|
return timedEvents.find((event) => event.cue.toLowerCase() === cue.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* loads an event given its id
|
|
||||||
* @param {string} eventId
|
|
||||||
* @returns {{loadedEvent: null, selectedEventId: null, nextEventId: null, selectedPublicEventId: null, nextPublicEventId: null, numEvents: null, titles: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null, noteNow: null, noteNext: null}, titlesPublic: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null}, selectedEventIndex: null}}
|
|
||||||
*/
|
|
||||||
loadById(eventId) {
|
|
||||||
const event = EventLoader.getEventWithId(eventId);
|
|
||||||
return this.loadEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loads an event given its index
|
|
||||||
* @param {number} eventIndex
|
|
||||||
* @returns {{loadedEvent: null, selectedEventId: null, nextEventId: null, selectedPublicEventId: null, nextPublicEventId: null, numEvents: null, titles: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null, noteNow: null, noteNext: null}, titlesPublic: {presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null}, selectedEventIndex: null}}
|
|
||||||
*/
|
|
||||||
loadByIndex(eventIndex) {
|
|
||||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
|
||||||
return this.loadEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* finds the previous event
|
* finds the previous event
|
||||||
* @return {object | undefined}
|
* @return {object | undefined}
|
||||||
*/
|
*/
|
||||||
findPrevious() {
|
static findPrevious(currentEventId: string | null): OntimeEvent | null {
|
||||||
const timedEvents = EventLoader.getPlayableEvents();
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
if (timedEvents === null || !timedEvents.length || this.loaded.selectedEventIndex === 0) {
|
if (!timedEvents || !timedEvents.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is no event running, go to first
|
// if there is no event running, go to first
|
||||||
if (this.loaded.selectedEventIndex === null) {
|
if (!currentEventId) {
|
||||||
return timedEvents[0];
|
return timedEvents.at(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIndex = this.loaded.selectedEventIndex - 1;
|
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||||
return timedEvents?.[newIndex];
|
const newIndex = Math.max(currentIndex - 1, 0);
|
||||||
|
const previousEvent = timedEvents.at(newIndex);
|
||||||
|
return previousEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* finds the next event
|
* finds the next event
|
||||||
* @return {object | undefined}
|
* @return {object | undefined}
|
||||||
*/
|
*/
|
||||||
findNext() {
|
static findNext(currentEventId: string | null): OntimeEvent | null {
|
||||||
const timedEvents = EventLoader.getPlayableEvents();
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
if (timedEvents === null || !timedEvents.length || this.loaded.selectedEventIndex === this.loaded.numEvents - 1) {
|
if (!timedEvents || !timedEvents.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is no event running, go to first
|
// if there is no event running, go to first
|
||||||
if (this.loaded.selectedEventIndex === null) {
|
if (!currentEventId) {
|
||||||
return timedEvents[0];
|
return timedEvents.at(0);
|
||||||
}
|
|
||||||
const newIndex = this.loaded.selectedEventIndex + 1;
|
|
||||||
return timedEvents?.[newIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* finds next event within Roll context
|
|
||||||
* @param {number} timeNow - current time in ms
|
|
||||||
*/
|
|
||||||
findRoll(timeNow) {
|
|
||||||
const timedEvents = EventLoader.getPlayableEvents();
|
|
||||||
if (!timedEvents.length) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nowIndex, timeToNext, nextEvent, nextPublicEvent, currentEvent, currentPublicEvent } = getRollTimers(
|
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||||
timedEvents,
|
const newIndex = (currentIndex + 1) % timedEvents.length;
|
||||||
timeNow,
|
const nextEvent = timedEvents.at(newIndex);
|
||||||
);
|
return nextEvent;
|
||||||
|
|
||||||
// load events
|
|
||||||
this.eventNow = currentEvent;
|
|
||||||
this.publicEventNow = currentPublicEvent;
|
|
||||||
this.eventNext = nextEvent;
|
|
||||||
this.publicEventNext = nextPublicEvent;
|
|
||||||
|
|
||||||
// loaded data summary
|
|
||||||
this.loaded.selectedEventIndex = nowIndex;
|
|
||||||
this.loaded.selectedEventId = currentEvent?.id || null;
|
|
||||||
this.loaded.numEvents = timedEvents.length;
|
|
||||||
this.loaded.nextEventId = nextEvent?.id || null;
|
|
||||||
this.loaded.nextPublicEventId = nextPublicEvent?.id || null;
|
|
||||||
|
|
||||||
this._loadEvent();
|
|
||||||
|
|
||||||
return { currentEvent, nextEvent, timeToNext };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns data for currently loaded event
|
|
||||||
* @returns {{loadedEvent: null, selectedEventId: (null|*), nextEventId: (null|*), selectedPublicEventId: (null|*), nextPublicEventId: (null|*), numEvents: (null|number|*), titles: (*|{presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null, noteNow: null, noteNext: null}), titlesPublic: (*|{presenterNext: null, titleNow: null, subtitleNow: null, titleNext: null, subtitleNext: null, presenterNow: null}), selectedEventIndex: (null|number|*)}}
|
|
||||||
*/
|
|
||||||
getLoaded() {
|
|
||||||
return {
|
|
||||||
loaded: this.loaded,
|
|
||||||
eventNow: this.eventNow,
|
|
||||||
publicEventNow: this.publicEventNow,
|
|
||||||
eventNext: this.eventNext,
|
|
||||||
publicEventNext: this.publicEventNext,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces event loader to update the event count
|
|
||||||
*/
|
|
||||||
updateNumEvents() {
|
|
||||||
this.loaded.numEvents = EventLoader.getPlayableEvents().length;
|
|
||||||
eventStore.set('loaded', this.loaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets instance state
|
|
||||||
*/
|
|
||||||
reset(emit = true) {
|
|
||||||
this.eventNow = null;
|
|
||||||
this.publicEventNow = null;
|
|
||||||
this.eventNext = null;
|
|
||||||
this.publicEventNext = null;
|
|
||||||
this.loaded = {
|
|
||||||
selectedEventIndex: null,
|
|
||||||
selectedEventId: null,
|
|
||||||
selectedPublicEventId: null,
|
|
||||||
nextEventId: null,
|
|
||||||
nextPublicEventId: null,
|
|
||||||
numEvents: EventLoader.getPlayableEvents().length,
|
|
||||||
};
|
|
||||||
|
|
||||||
// workaround for socket not being ready in constructor
|
|
||||||
if (emit) {
|
|
||||||
this._loadEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loads an event given its id
|
|
||||||
* @param {object} event
|
|
||||||
*/
|
|
||||||
loadEvent(event?: OntimeEvent) {
|
|
||||||
if (typeof event === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const timedEvents = EventLoader.getPlayableEvents();
|
|
||||||
const eventIndex = timedEvents.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
|
||||||
const playableEvents = EventLoader.getPlayableEvents();
|
|
||||||
|
|
||||||
// we know some stuff now
|
|
||||||
this.loaded.selectedEventIndex = eventIndex;
|
|
||||||
this.loaded.selectedEventId = event.id;
|
|
||||||
this.loaded.numEvents = timedEvents.length;
|
|
||||||
this.eventNow = event;
|
|
||||||
this._loadEventNow(event, playableEvents);
|
|
||||||
this._loadEventNext(playableEvents);
|
|
||||||
|
|
||||||
this._loadEvent();
|
|
||||||
|
|
||||||
return this.getLoaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle side effects from event loading
|
|
||||||
*/
|
|
||||||
private _loadEvent() {
|
|
||||||
eventStore.batchSet({
|
|
||||||
loaded: this.loaded,
|
|
||||||
eventNow: this.eventNow,
|
|
||||||
publicEventNow: this.publicEventNow,
|
|
||||||
eventNext: this.eventNext,
|
|
||||||
publicEventNext: this.publicEventNext,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description loads currently running events
|
|
||||||
* @private
|
|
||||||
* @param {object} event
|
|
||||||
* @param {array} rundown
|
|
||||||
*/
|
|
||||||
private _loadEventNow(event, rundown) {
|
|
||||||
this.eventNow = event;
|
|
||||||
|
|
||||||
// check if current is also public
|
|
||||||
if (event.isPublic) {
|
|
||||||
this.publicEventNow = event;
|
|
||||||
this.loaded.selectedPublicEventId = event.id;
|
|
||||||
} else {
|
|
||||||
// assume there is no public event
|
|
||||||
this.publicEventNow = null;
|
|
||||||
this.loaded.selectedPublicEventId = null;
|
|
||||||
|
|
||||||
// if there is nothing before, return
|
|
||||||
if (this.loaded.selectedEventIndex === 0) return;
|
|
||||||
|
|
||||||
// iterate backwards to find it
|
|
||||||
for (let i = this.loaded.selectedEventIndex; i >= 0; i--) {
|
|
||||||
if (rundown[i].isPublic) {
|
|
||||||
this.publicEventNow = rundown[i];
|
|
||||||
this.loaded.selectedPublicEventId = rundown[i].id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description look for next events
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private _loadEventNext(rundown) {
|
|
||||||
// assume there are no next events
|
|
||||||
this.eventNext = null;
|
|
||||||
this.publicEventNext = null;
|
|
||||||
this.loaded.nextEventId = null;
|
|
||||||
this.loaded.nextPublicEventId = null;
|
|
||||||
|
|
||||||
if (this.loaded.selectedEventIndex === null) return;
|
|
||||||
|
|
||||||
const numEvents = rundown.length;
|
|
||||||
|
|
||||||
if (this.loaded.selectedEventIndex < numEvents - 1) {
|
|
||||||
let nextPublic = false;
|
|
||||||
let nextProduction = false;
|
|
||||||
|
|
||||||
for (let i = this.loaded.selectedEventIndex + 1; i < numEvents; i++) {
|
|
||||||
// if we have not set private
|
|
||||||
if (!nextProduction) {
|
|
||||||
this.eventNext = rundown[i];
|
|
||||||
this.loaded.nextEventId = rundown[i].id;
|
|
||||||
nextProduction = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if event is public
|
|
||||||
if (rundown[i].isPublic) {
|
|
||||||
this.publicEventNext = rundown[i];
|
|
||||||
this.loaded.nextPublicEventId = rundown[i].id;
|
|
||||||
nextPublic = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop if both are set
|
|
||||||
if (nextPublic && nextProduction) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventLoader = new EventLoader();
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as assert from '../utils/assert.js';
|
|||||||
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
|
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
|
||||||
|
|
||||||
import { isPartialTimerMessage, messageService } from '../services/message-service/MessageService.js';
|
import { isPartialTimerMessage, messageService } from '../services/message-service/MessageService.js';
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { parse, updateEvent } from './integrationController.config.js';
|
import { parse, updateEvent } from './integrationController.config.js';
|
||||||
|
|
||||||
@@ -122,12 +122,12 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackService.start();
|
runtimeService.start();
|
||||||
|
|
||||||
return { payload: 'start' };
|
return { payload: 'start' };
|
||||||
},
|
},
|
||||||
'start-next': () => {
|
'start-next': () => {
|
||||||
PlaybackService.startNext();
|
runtimeService.startNext();
|
||||||
return { payload: 'start' };
|
return { payload: 'start' };
|
||||||
},
|
},
|
||||||
startindex: (payload) => {
|
startindex: (payload) => {
|
||||||
@@ -137,7 +137,7 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Indexes in frontend are 1 based
|
// Indexes in frontend are 1 based
|
||||||
const success = PlaybackService.startByIndex(eventIndex - 1);
|
const success = runtimeService.startByIndex(eventIndex - 1);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
||||||
}
|
}
|
||||||
@@ -145,36 +145,36 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
},
|
},
|
||||||
startid: (payload) => {
|
startid: (payload) => {
|
||||||
assert.isString(payload);
|
assert.isString(payload);
|
||||||
PlaybackService.startById(payload);
|
runtimeService.startById(payload);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
startcue: (payload) => {
|
startcue: (payload) => {
|
||||||
assert.isString(payload);
|
assert.isString(payload);
|
||||||
PlaybackService.startByCue(payload);
|
runtimeService.startByCue(payload);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
pause: () => {
|
pause: () => {
|
||||||
PlaybackService.pause();
|
runtimeService.pause();
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
previous: () => {
|
previous: () => {
|
||||||
PlaybackService.loadPrevious();
|
runtimeService.loadPrevious();
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
next: () => {
|
next: () => {
|
||||||
PlaybackService.loadNext();
|
runtimeService.loadNext();
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
PlaybackService.stop();
|
runtimeService.stop();
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
reload: () => {
|
reload: () => {
|
||||||
PlaybackService.reload();
|
runtimeService.reload();
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
roll: () => {
|
roll: () => {
|
||||||
PlaybackService.roll();
|
runtimeService.roll();
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
loadindex: (payload) => {
|
loadindex: (payload) => {
|
||||||
@@ -183,22 +183,25 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
throw new Error(`Event index out of range ${eventIndex}`);
|
throw new Error(`Event index out of range ${eventIndex}`);
|
||||||
}
|
}
|
||||||
// Indexes in frontend are 1 based
|
// Indexes in frontend are 1 based
|
||||||
PlaybackService.loadByIndex(eventIndex - 1);
|
runtimeService.loadByIndex(eventIndex - 1);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
loadid: (payload) => {
|
loadid: (payload) => {
|
||||||
assert.isDefined(payload);
|
assert.isDefined(payload);
|
||||||
PlaybackService.loadById(payload.toString().toLowerCase());
|
runtimeService.loadById(payload.toString().toLowerCase());
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
loadcue: (payload) => {
|
loadcue: (payload) => {
|
||||||
assert.isString(payload);
|
assert.isString(payload);
|
||||||
PlaybackService.loadByCue(payload);
|
runtimeService.loadByCue(payload);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
addtime: (payload) => {
|
addtime: (payload) => {
|
||||||
const time = numberOrError(payload);
|
const time = numberOrError(payload);
|
||||||
PlaybackService.addTime(time);
|
if (time === 0) {
|
||||||
|
return { payload: 'success' };
|
||||||
|
}
|
||||||
|
runtimeService.addTime(time * 1000);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { copyFile, rename, writeFile } from 'fs/promises';
|
|||||||
import { fileHandler } from '../utils/parser.js';
|
import { fileHandler } from '../utils/parser.js';
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import {
|
import {
|
||||||
getAppDataPath,
|
getAppDataPath,
|
||||||
@@ -98,7 +98,7 @@ async function parseFile(file, _req, _res, options) {
|
|||||||
const parseAndApply = async (file, _req, res, options) => {
|
const parseAndApply = async (file, _req, res, options) => {
|
||||||
const result = await parseFile(file, _req, res, options);
|
const result = await parseFile(file, _req, res, options);
|
||||||
|
|
||||||
PlaybackService.stop();
|
runtimeService.stop();
|
||||||
|
|
||||||
const newRundown = result.rundown || [];
|
const newRundown = result.rundown || [];
|
||||||
if (options?.onlyRundown === 'true') {
|
if (options?.onlyRundown === 'true') {
|
||||||
@@ -409,7 +409,7 @@ export async function patchPartialProjectFile(req, res) {
|
|||||||
await DataProvider.mergeIntoData(patchDb);
|
await DataProvider.mergeIntoData(patchDb);
|
||||||
if (patchDb.rundown !== undefined) {
|
if (patchDb.rundown !== undefined) {
|
||||||
// it is likely cheaper to invalidate cache than to calculate diff
|
// it is likely cheaper to invalidate cache than to calculate diff
|
||||||
PlaybackService.stop();
|
runtimeService.stop();
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||||
notifyChanges({ external: true, reset: true });
|
notifyChanges({ external: true, reset: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ enum Source {
|
|||||||
MIDI = 'MIDI',
|
MIDI = 'MIDI',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service manages retrieving current time from a managed time source
|
||||||
|
*/
|
||||||
class Clock {
|
class Clock {
|
||||||
private static instance: Clock;
|
private static instance: Clock;
|
||||||
private readonly source: Source;
|
private readonly source: Source;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ interface Config {
|
|||||||
lastLoadedProject: string;
|
lastLoadedProject: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service manages Ontime's runtime configuration
|
||||||
|
*/
|
||||||
|
|
||||||
class ConfigService {
|
class ConfigService {
|
||||||
private config: Low<Config>;
|
private config: Low<Config>;
|
||||||
private configPath: string;
|
private configPath: string;
|
||||||
|
|||||||
@@ -1,295 +0,0 @@
|
|||||||
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
|
|
||||||
import { validatePlayback } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
|
||||||
import { eventTimer } from './TimerService.js';
|
|
||||||
import { clock } from './Clock.js';
|
|
||||||
import { logger } from '../classes/Logger.js';
|
|
||||||
import { RestorePoint } from './RestoreService.js';
|
|
||||||
import { state } from '../state.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service manages playback status of app
|
|
||||||
* Coordinating with necessary services
|
|
||||||
*/
|
|
||||||
export class PlaybackService {
|
|
||||||
/**
|
|
||||||
* makes calls for loading and starting given event
|
|
||||||
* @param {OntimeEvent} event
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static loadEvent(event: OntimeEvent): boolean {
|
|
||||||
let success = false;
|
|
||||||
if (!event) {
|
|
||||||
logger.error(LogOrigin.Playback, 'No event found');
|
|
||||||
} else if (event.skip) {
|
|
||||||
logger.warning(LogOrigin.Playback, `Refused playback of skipped event ID ${event.id}`);
|
|
||||||
} else {
|
|
||||||
eventLoader.loadEvent(event);
|
|
||||||
eventTimer.load(event);
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
eventStore.broadcast();
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* starts event matching given ID
|
|
||||||
* @param {string} eventId
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static startById(eventId: string): boolean {
|
|
||||||
const event = EventLoader.getEventWithId(eventId);
|
|
||||||
const success = PlaybackService.loadEvent(event);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
|
||||||
PlaybackService.start();
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* starts an event at index
|
|
||||||
* @param {number} eventIndex
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static startByIndex(eventIndex: number): boolean {
|
|
||||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
|
||||||
const success = PlaybackService.loadEvent(event);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
|
||||||
PlaybackService.start();
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* starts first event matching given cue
|
|
||||||
* @param {string} cue
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static startByCue(cue: string): boolean {
|
|
||||||
const event = EventLoader.getEventWithCue(cue);
|
|
||||||
const success = PlaybackService.loadEvent(event);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
|
||||||
PlaybackService.start();
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loads event matching given ID
|
|
||||||
* @param {string} eventId
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static loadById(eventId: string): boolean {
|
|
||||||
const event = EventLoader.getEventWithId(eventId);
|
|
||||||
const success = PlaybackService.loadEvent(event);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loads event matching given ID
|
|
||||||
* @param {number} eventIndex
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static loadByIndex(eventIndex: number): boolean {
|
|
||||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
|
||||||
const success = PlaybackService.loadEvent(event);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loads first event matching given cue
|
|
||||||
* @param {string} cue
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static loadByCue(cue: string): boolean {
|
|
||||||
const event = EventLoader.getEventWithCue(cue);
|
|
||||||
const success = PlaybackService.loadEvent(event);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads event before currently selected
|
|
||||||
*/
|
|
||||||
static loadPrevious() {
|
|
||||||
const previousEvent = eventLoader.findPrevious();
|
|
||||||
if (previousEvent) {
|
|
||||||
const success = PlaybackService.loadEvent(previousEvent);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${previousEvent.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads event after currently selected
|
|
||||||
* @param {string} [fallbackAction] - 'stop', 'pause'
|
|
||||||
* @return {boolean} success
|
|
||||||
*/
|
|
||||||
static loadNext(fallbackAction?: 'stop' | 'pause'): boolean {
|
|
||||||
const nextEvent = eventLoader.findNext();
|
|
||||||
if (nextEvent) {
|
|
||||||
const success = PlaybackService.loadEvent(nextEvent);
|
|
||||||
if (success) {
|
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${nextEvent.id}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (fallbackAction === 'stop') {
|
|
||||||
logger.info(LogOrigin.Playback, 'No next event found! Stopping playback');
|
|
||||||
PlaybackService.stop();
|
|
||||||
return false;
|
|
||||||
} else if (fallbackAction === 'pause') {
|
|
||||||
logger.info(LogOrigin.Playback, 'No next event found! Pausing playback');
|
|
||||||
PlaybackService.pause();
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
logger.info(LogOrigin.Playback, 'No next event found! Continuing playback');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playback on selected event
|
|
||||||
*/
|
|
||||||
static start() {
|
|
||||||
if (validatePlayback(state.playback).start) {
|
|
||||||
eventTimer.start();
|
|
||||||
const newState = state.playback;
|
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playback on next event
|
|
||||||
* @param {string} [fallbackAction] - 'stop', 'pause'
|
|
||||||
*/
|
|
||||||
static startNext(fallbackAction?: 'stop' | 'pause') {
|
|
||||||
const success = PlaybackService.loadNext(fallbackAction);
|
|
||||||
if (success) {
|
|
||||||
PlaybackService.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses playback on selected event
|
|
||||||
*/
|
|
||||||
static pause() {
|
|
||||||
if (validatePlayback(state.playback).pause) {
|
|
||||||
eventTimer.pause();
|
|
||||||
const newState = state.playback;
|
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops timer and unloads any events
|
|
||||||
*/
|
|
||||||
static stop() {
|
|
||||||
if (validatePlayback(state.playback).stop) {
|
|
||||||
eventLoader.reset();
|
|
||||||
eventTimer.stop();
|
|
||||||
const newState = state.playback;
|
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reloads current event
|
|
||||||
*/
|
|
||||||
static reload() {
|
|
||||||
if (state.timer.selectedEventId) {
|
|
||||||
this.loadById(state.timer.selectedEventId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets playback to roll
|
|
||||||
*/
|
|
||||||
static roll() {
|
|
||||||
if (EventLoader.getPlayableEvents()) {
|
|
||||||
const rollTimers = eventLoader.findRoll(clock.timeNow());
|
|
||||||
|
|
||||||
// nothing to play
|
|
||||||
if (rollTimers === null) {
|
|
||||||
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
|
||||||
PlaybackService.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentEvent, nextEvent } = rollTimers;
|
|
||||||
if (!currentEvent && !nextEvent) {
|
|
||||||
logger.warning(LogOrigin.Server, 'Roll: no events found');
|
|
||||||
PlaybackService.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventTimer.roll(currentEvent, nextEvent);
|
|
||||||
|
|
||||||
const newState = state.playback;
|
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description resume playback state given a restore point
|
|
||||||
* @param restorePoint
|
|
||||||
*/
|
|
||||||
static resume(restorePoint: RestorePoint) {
|
|
||||||
const willResume = () => logger.info(LogOrigin.Server, 'Resuming playback');
|
|
||||||
|
|
||||||
if (restorePoint.playback === Playback.Roll) {
|
|
||||||
willResume();
|
|
||||||
PlaybackService.roll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restorePoint.selectedEventId) {
|
|
||||||
const event = EventLoader.getEventWithId(restorePoint.selectedEventId);
|
|
||||||
// the db would have to change for the event not to exist
|
|
||||||
// we do not kow the reason for the crash, so we check anyway
|
|
||||||
if (!event) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventLoader.loadEvent(event);
|
|
||||||
eventTimer.resume(event, restorePoint);
|
|
||||||
eventStore.broadcast();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds time to current event
|
|
||||||
* @param {number} time - time to add in seconds
|
|
||||||
*/
|
|
||||||
static addTime(time: number) {
|
|
||||||
if (state.timer.selectedEventId) {
|
|
||||||
const timeInMs = time * 1000;
|
|
||||||
eventTimer.addTime(timeInMs);
|
|
||||||
timeInMs > 0
|
|
||||||
? logger.info(LogOrigin.Playback, `Added ${time} sec`)
|
|
||||||
: logger.info(LogOrigin.Playback, `Removed ${time} sec`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds delay to current event
|
|
||||||
* @deprecated Use addTime
|
|
||||||
* @param {number} delayTime time in minutes
|
|
||||||
*/
|
|
||||||
static setDelay(delayTime: number) {
|
|
||||||
this.addTime(delayTime * 60);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
|
import { OntimeEvent, Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { logger } from '../classes/Logger.js';
|
|
||||||
import type { RestorePoint } from './RestoreService.js';
|
|
||||||
import { stateMutations, state } from '../state.js';
|
import { stateMutations, state } from '../state.js';
|
||||||
|
|
||||||
type initialLoadingData = {
|
/**
|
||||||
startedAt?: number | null;
|
* Service manages Ontime's main timer
|
||||||
expectedFinish?: number | null;
|
*/
|
||||||
current?: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TimerService {
|
export class TimerService {
|
||||||
private _interval: NodeJS.Timer;
|
private _interval: NodeJS.Timer;
|
||||||
private _updateInterval: number;
|
private _updateInterval: number;
|
||||||
@@ -17,82 +12,17 @@ export class TimerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {object} [timerConfig]
|
|
||||||
* @param {number} [timerConfig.refresh]
|
* @param {number} [timerConfig.refresh]
|
||||||
* @param {number} [timerConfig.updateInterval]
|
* @param {number} [timerConfig.updateInterval]
|
||||||
*/
|
*/
|
||||||
constructor(timerConfig: { refresh: number; updateInterval: number }) {
|
constructor(timerConfig: { refresh: number; updateInterval: number }) {
|
||||||
this._refreshInterval = timerConfig.refresh;
|
this._refreshInterval = timerConfig.refresh;
|
||||||
this._updateInterval = timerConfig.updateInterval;
|
this._updateInterval = timerConfig.updateInterval;
|
||||||
logger.info(LogOrigin.Server, 'Timer service started');
|
this._interval = setInterval(this.update, 32);
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._interval = setInterval(() => this.update(), this._refreshInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resumes a given playback state, same as load
|
|
||||||
* @param {RestorePoint} restorePoint
|
|
||||||
* @param {OntimeEvent} event
|
|
||||||
*/
|
|
||||||
resume(event: OntimeEvent, restorePoint: RestorePoint) {
|
|
||||||
stateMutations.timer.resume(event, restorePoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: can load and hotreload be merged?
|
|
||||||
/**
|
|
||||||
* Reloads information for currently running timer
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
hotReload(event: OntimeEvent | undefined) {
|
|
||||||
if (event === undefined) {
|
|
||||||
this.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is no longer correct
|
|
||||||
if (event.id !== state.timer.selectedEventId) {
|
|
||||||
// we only hot reload if the timer is the same
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.skip) {
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check if any relevant information warrants update
|
|
||||||
stateMutations.timer.reload(event);
|
|
||||||
|
|
||||||
this.update(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads given timer to object
|
|
||||||
* @param {OntimeEvent} event
|
|
||||||
* @param {initialLoadingData} initialData
|
|
||||||
*/
|
|
||||||
load(event: OntimeEvent, initialData?: initialLoadingData) {
|
|
||||||
if (event.skip) {
|
|
||||||
throw new Error('Refuse load of skipped event');
|
|
||||||
}
|
|
||||||
|
|
||||||
stateMutations.timer.clear();
|
|
||||||
|
|
||||||
// TODO: does this replace the need for hot reload?
|
|
||||||
if (initialData) {
|
|
||||||
stateMutations.timer.patch(initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
stateMutations.timer.load(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!state.timer.selectedEventId) {
|
if (!state.runtime.selectedEventId) {
|
||||||
// TODO: we should be able to start
|
|
||||||
if (state.playback === Playback.Roll) {
|
|
||||||
logger.error(LogOrigin.Playback, 'Cannot start while waiting for event');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +30,8 @@ export class TimerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: when we start a timer, we schedule an update to its expected end - 16ms
|
||||||
|
// we need to cancel this timer on pause, stop and addTime
|
||||||
stateMutations.timer.start();
|
stateMutations.timer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,33 +54,34 @@ export class TimerService {
|
|||||||
* @param {number} amount
|
* @param {number} amount
|
||||||
*/
|
*/
|
||||||
addTime(amount: number) {
|
addTime(amount: number) {
|
||||||
if (amount === 0) {
|
if (state.runtime.selectedEventId === null) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.timer.selectedEventId === null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stateMutations.timer.addTime(amount);
|
stateMutations.timer.addTime(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the app at regular intervals
|
||||||
|
* @param {boolean} force whether we should force a broadcast of state
|
||||||
|
*/
|
||||||
update(force = false) {
|
update(force = false) {
|
||||||
stateMutations.timer.update(force, this._updateInterval);
|
stateMutations.timer.update(force, this._updateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads roll information into timer service
|
* Loads roll information into timer service
|
||||||
* @param {OntimeEvent | null} currentEvent -- both current event and next event cant be null
|
* @throws {Error} if rundown is empty
|
||||||
* @param {OntimeEvent | null} nextEvent -- both current event and next event cant be null
|
* @param {OntimeEvent[]} rundown -- list of events to run
|
||||||
*/
|
*/
|
||||||
roll(currentEvent: OntimeEvent | null, nextEvent: OntimeEvent | null) {
|
roll(rundown: OntimeEvent[]) {
|
||||||
stateMutations.timer.roll(currentEvent, nextEvent);
|
if (rundown.length === 0) {
|
||||||
this.update();
|
throw new Error('No events found');
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMutations.timer.roll(rundown);
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
clearInterval(this._interval);
|
clearInterval(this._interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate at 30fps, refresh at 1fps
|
|
||||||
export const eventTimer = new TimerService({ refresh: 32, updateInterval: 1000 });
|
|
||||||
|
|||||||
@@ -1,706 +0,0 @@
|
|||||||
import { OntimeEvent } from 'ontime-types';
|
|
||||||
import { dayInMs } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { getRollTimers, normaliseEndTime, sortArrayByProperty, updateRoll } from '../rollUtils.js';
|
|
||||||
|
|
||||||
// test sortArrayByProperty()
|
|
||||||
describe('sort simple arrays of objects', () => {
|
|
||||||
it('sort array 1-5', () => {
|
|
||||||
const arr1 = [{ timeStart: 1 }, { timeStart: 5 }, { timeStart: 3 }, { timeStart: 2 }, { timeStart: 4 }];
|
|
||||||
|
|
||||||
const arr1Expected = [{ timeStart: 1 }, { timeStart: 2 }, { timeStart: 3 }, { timeStart: 4 }, { timeStart: 5 }];
|
|
||||||
|
|
||||||
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
|
||||||
expect(sorted).toStrictEqual(arr1Expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sort array 1-5 with null', () => {
|
|
||||||
const arr1 = [
|
|
||||||
{ timeStart: 1 },
|
|
||||||
{ timeStart: 5 },
|
|
||||||
{ timeStart: 3 },
|
|
||||||
{ timeStart: 2 },
|
|
||||||
{ timeStart: 4 },
|
|
||||||
{ timeStart: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
const arr1Expected = [
|
|
||||||
{ timeStart: null },
|
|
||||||
{ timeStart: 1 },
|
|
||||||
{ timeStart: 2 },
|
|
||||||
{ timeStart: 3 },
|
|
||||||
{ timeStart: 4 },
|
|
||||||
{ timeStart: 5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
|
||||||
expect(sorted).toStrictEqual(arr1Expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// test getRollTimers()
|
|
||||||
describe('test that roll loads selection in right order', () => {
|
|
||||||
const eventlist: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 5,
|
|
||||||
timeEnd: 10,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 20,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
timeStart: 20,
|
|
||||||
timeEnd: 30,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
timeStart: 30,
|
|
||||||
timeEnd: 40,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
timeStart: 40,
|
|
||||||
timeEnd: 50,
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
timeStart: 50,
|
|
||||||
timeEnd: 60,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
timeStart: 60,
|
|
||||||
timeEnd: 70,
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
timeStart: 70,
|
|
||||||
timeEnd: 80,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('if timer is at 0', () => {
|
|
||||||
const now = 0;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: null,
|
|
||||||
nowId: null,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 0,
|
|
||||||
publicNextIndex: 4,
|
|
||||||
timeToNext: 5,
|
|
||||||
nextEvent: eventlist[0],
|
|
||||||
nextPublicEvent: eventlist[4],
|
|
||||||
currentEvent: null,
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 5', () => {
|
|
||||||
const now = 5;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 0,
|
|
||||||
nowId: eventlist[0].id,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 1,
|
|
||||||
publicNextIndex: 4,
|
|
||||||
timeToNext: 5,
|
|
||||||
nextEvent: eventlist[1],
|
|
||||||
nextPublicEvent: eventlist[4],
|
|
||||||
currentEvent: eventlist[0],
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 15', () => {
|
|
||||||
const now = 15;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 1,
|
|
||||||
nowId: eventlist[1].id,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 2,
|
|
||||||
publicNextIndex: 4,
|
|
||||||
timeToNext: 5,
|
|
||||||
nextEvent: eventlist[2],
|
|
||||||
nextPublicEvent: eventlist[4],
|
|
||||||
currentEvent: eventlist[1],
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 20', () => {
|
|
||||||
const now = 20;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 2,
|
|
||||||
nowId: eventlist[2].id,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 3,
|
|
||||||
publicNextIndex: 4,
|
|
||||||
timeToNext: 10,
|
|
||||||
nextEvent: eventlist[3],
|
|
||||||
nextPublicEvent: eventlist[4],
|
|
||||||
currentEvent: eventlist[2],
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 49', () => {
|
|
||||||
const now = 49;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 4,
|
|
||||||
nowId: eventlist[4].id,
|
|
||||||
publicIndex: 4,
|
|
||||||
nextIndex: 5,
|
|
||||||
publicNextIndex: 6,
|
|
||||||
timeToNext: 1,
|
|
||||||
nextEvent: eventlist[5],
|
|
||||||
nextPublicEvent: eventlist[6],
|
|
||||||
currentEvent: eventlist[4],
|
|
||||||
currentPublicEvent: eventlist[4],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 63', () => {
|
|
||||||
const now = 63;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 6,
|
|
||||||
nowId: eventlist[6].id,
|
|
||||||
publicIndex: 6,
|
|
||||||
nextIndex: 7,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: 7,
|
|
||||||
nextEvent: eventlist[7],
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[6],
|
|
||||||
currentPublicEvent: eventlist[6],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 75', () => {
|
|
||||||
const now = 75;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 7,
|
|
||||||
nowId: eventlist[7].id,
|
|
||||||
publicIndex: 6,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[7],
|
|
||||||
currentPublicEvent: eventlist[6],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 100 we roll to day after', () => {
|
|
||||||
const now = 100;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: null,
|
|
||||||
nowId: null,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 0,
|
|
||||||
publicNextIndex: 4,
|
|
||||||
timeToNext: dayInMs - now + eventlist[0].timeStart,
|
|
||||||
nextEvent: eventlist[0],
|
|
||||||
nextPublicEvent: eventlist[4],
|
|
||||||
currentEvent: null,
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles rolls to next day with real values', () => {
|
|
||||||
const singleEventList: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 36000000, // 10:00
|
|
||||||
timeEnd: 39600000, // 11:00
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const now = 64800000; // 18:00
|
|
||||||
const expected = {
|
|
||||||
nowIndex: null,
|
|
||||||
nowId: null,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 0,
|
|
||||||
publicNextIndex: 0,
|
|
||||||
timeToNext: dayInMs - now + singleEventList[0].timeStart,
|
|
||||||
nextEvent: singleEventList[0],
|
|
||||||
nextPublicEvent: singleEventList[0],
|
|
||||||
currentEvent: null,
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles rolls to next day with real values', () => {
|
|
||||||
const singleEventList: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 36000000, // 10:00
|
|
||||||
timeEnd: 3600000, // 01:00
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const now = 60000; // 00:01
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 0,
|
|
||||||
nowId: singleEventList[0].id,
|
|
||||||
publicIndex: 0,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: singleEventList[0],
|
|
||||||
currentPublicEvent: singleEventList[0],
|
|
||||||
};
|
|
||||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
it('handles rolls to next day with real values', () => {
|
|
||||||
const singleEventList: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 36000000, // 10:00
|
|
||||||
timeEnd: 3600000, // 01:00
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const now = 60000; // 00:01
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 0,
|
|
||||||
nowId: singleEventList[0].id,
|
|
||||||
publicIndex: 0,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: singleEventList[0],
|
|
||||||
currentPublicEvent: singleEventList[0],
|
|
||||||
};
|
|
||||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles roll that goes over midnight', () => {
|
|
||||||
const singleEventList: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 72000000, // 20:00
|
|
||||||
timeEnd: 60000, // 00:10
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const now = 6000; // 00:01
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 0,
|
|
||||||
nowId: singleEventList[0].id,
|
|
||||||
publicIndex: 0,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: singleEventList[0],
|
|
||||||
currentPublicEvent: singleEventList[0],
|
|
||||||
};
|
|
||||||
const state = getRollTimers(singleEventList as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// test getRollTimers()
|
|
||||||
describe('test that roll behaviour with overlapping times', () => {
|
|
||||||
const eventlist: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 10,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 20,
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
timeStart: 10,
|
|
||||||
timeEnd: 30,
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('if timer is at 0', () => {
|
|
||||||
const now = 0;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: null,
|
|
||||||
nowId: null,
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: 0,
|
|
||||||
publicNextIndex: 1,
|
|
||||||
timeToNext: 10,
|
|
||||||
nextEvent: eventlist[0],
|
|
||||||
nextPublicEvent: eventlist[1],
|
|
||||||
currentEvent: null,
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 10', () => {
|
|
||||||
const now = 10;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 1,
|
|
||||||
nowId: eventlist[1].id,
|
|
||||||
publicIndex: 1,
|
|
||||||
nextIndex: 2,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: 0,
|
|
||||||
nextEvent: eventlist[2],
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[1],
|
|
||||||
currentPublicEvent: eventlist[1],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 15', () => {
|
|
||||||
const now = 15;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 1,
|
|
||||||
nowId: eventlist[1].id,
|
|
||||||
publicIndex: 1,
|
|
||||||
nextIndex: 2,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: -5,
|
|
||||||
nextEvent: eventlist[2],
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[1],
|
|
||||||
currentPublicEvent: eventlist[1],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 20', () => {
|
|
||||||
const now = 20;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 2,
|
|
||||||
nowId: eventlist[2].id,
|
|
||||||
publicIndex: 1,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[2],
|
|
||||||
currentPublicEvent: eventlist[1],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if timer is at 25', () => {
|
|
||||||
const now = 25;
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 2,
|
|
||||||
nowId: eventlist[2].id,
|
|
||||||
publicIndex: 1,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[2],
|
|
||||||
currentPublicEvent: eventlist[1],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(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', () => {
|
|
||||||
const now = 66600000; // 19:30
|
|
||||||
const eventlist: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 66000000, // 19:20
|
|
||||||
timeEnd: 54600000, // 16:10
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const expected = {
|
|
||||||
nowIndex: 0,
|
|
||||||
nowId: '1',
|
|
||||||
publicIndex: null,
|
|
||||||
nextIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
currentEvent: eventlist[0],
|
|
||||||
currentPublicEvent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if the start time is the day after end time, and both are later than now', () => {
|
|
||||||
const now = 66840000; // 19:34
|
|
||||||
const eventlist: Partial<OntimeEvent>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timeStart: 67200000, // 19:40
|
|
||||||
timeEnd: 66900000, // 19:35
|
|
||||||
isPublic: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const expected = {
|
|
||||||
currentEvent: {
|
|
||||||
id: '1',
|
|
||||||
isPublic: false,
|
|
||||||
timeEnd: 66900000,
|
|
||||||
timeStart: 67200000,
|
|
||||||
},
|
|
||||||
currentPublicEvent: null,
|
|
||||||
nextEvent: null,
|
|
||||||
nextIndex: null,
|
|
||||||
nextPublicEvent: null,
|
|
||||||
nowId: '1',
|
|
||||||
nowIndex: 0,
|
|
||||||
publicIndex: null,
|
|
||||||
publicNextIndex: null,
|
|
||||||
timeToNext: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = getRollTimers(eventlist as OntimeEvent[], now);
|
|
||||||
expect(state).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// test normaliseEndTime() on issue #58
|
|
||||||
test('test typical scenarios', () => {
|
|
||||||
const t1 = {
|
|
||||||
start: 10,
|
|
||||||
end: 20,
|
|
||||||
};
|
|
||||||
const t1_expected = 20;
|
|
||||||
|
|
||||||
expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected);
|
|
||||||
|
|
||||||
const t2 = {
|
|
||||||
start: 10 + dayInMs,
|
|
||||||
end: 20,
|
|
||||||
};
|
|
||||||
const t2_expected = 20 + dayInMs;
|
|
||||||
|
|
||||||
expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected);
|
|
||||||
|
|
||||||
const t3 = {
|
|
||||||
start: 10,
|
|
||||||
end: 10,
|
|
||||||
};
|
|
||||||
const t3_expected = 10;
|
|
||||||
|
|
||||||
expect(normaliseEndTime(t3.start, t3.end)).toBe(t3_expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
// test updateRoll()
|
|
||||||
describe('typical scenarios', () => {
|
|
||||||
it('it updates running events correctly', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: '1',
|
|
||||||
current: 10,
|
|
||||||
_finishAt: 15,
|
|
||||||
clock: 11,
|
|
||||||
secondaryTimer: null,
|
|
||||||
secondaryTarget: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: timers._finishAt - timers.clock,
|
|
||||||
updatedSecondaryTimer: null,
|
|
||||||
doRollLoad: false,
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
|
|
||||||
// test that it can jump time
|
|
||||||
timers._finishAt = 1000;
|
|
||||||
timers.clock = 600;
|
|
||||||
expected.updatedTimer = timers._finishAt - timers.clock;
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('it updates secondary timer', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: null,
|
|
||||||
current: null,
|
|
||||||
_finishAt: null,
|
|
||||||
clock: 11,
|
|
||||||
secondaryTimer: 1,
|
|
||||||
secondaryTarget: 15,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: null,
|
|
||||||
updatedSecondaryTimer: timers.secondaryTarget - timers.clock,
|
|
||||||
doRollLoad: false,
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('flags an event end', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: '1',
|
|
||||||
current: 10,
|
|
||||||
_finishAt: 11,
|
|
||||||
clock: 12,
|
|
||||||
secondaryTimer: null,
|
|
||||||
secondaryTarget: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: -1,
|
|
||||||
updatedSecondaryTimer: null,
|
|
||||||
doRollLoad: true,
|
|
||||||
isFinished: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('secondary events do not trigger event ends', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: null,
|
|
||||||
current: null,
|
|
||||||
_finishAt: null,
|
|
||||||
clock: 16,
|
|
||||||
secondaryTimer: 1,
|
|
||||||
secondaryTarget: 15,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: null,
|
|
||||||
updatedSecondaryTimer: timers.secondaryTarget - timers.clock,
|
|
||||||
doRollLoad: true,
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when a secondary timer is finished, it prompts for new event load', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: null,
|
|
||||||
current: null,
|
|
||||||
_finishAt: null,
|
|
||||||
clock: 15,
|
|
||||||
secondaryTimer: 0,
|
|
||||||
secondaryTarget: 15,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: null,
|
|
||||||
updatedSecondaryTimer: timers.secondaryTarget - timers.clock,
|
|
||||||
doRollLoad: true,
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('counts over midnight', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: '1',
|
|
||||||
current: 25,
|
|
||||||
_finishAt: 10 + dayInMs,
|
|
||||||
clock: dayInMs - 10,
|
|
||||||
secondaryTimer: null,
|
|
||||||
secondaryTarget: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: 20,
|
|
||||||
updatedSecondaryTimer: null,
|
|
||||||
doRollLoad: false,
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rolls over midnight', () => {
|
|
||||||
const timers = {
|
|
||||||
selectedEventId: '1',
|
|
||||||
current: dayInMs,
|
|
||||||
_finishAt: 10 + dayInMs,
|
|
||||||
clock: 10,
|
|
||||||
secondaryTimer: null,
|
|
||||||
secondaryTarget: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
updatedTimer: dayInMs,
|
|
||||||
updatedSecondaryTimer: null,
|
|
||||||
doRollLoad: false,
|
|
||||||
isFinished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(updateRoll(timers)).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,202 +0,0 @@
|
|||||||
import { OntimeEvent } from 'ontime-types';
|
|
||||||
import { dayInMs } from 'ontime-utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle events that span over midnight
|
|
||||||
*/
|
|
||||||
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Sorts an array of objects by given property
|
|
||||||
* @param {array} arr - array to be sorted
|
|
||||||
* @param {string} property - property to compare
|
|
||||||
* @returns {array} copy of array sorted in ascending order
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
|
|
||||||
return [...arr].sort((a, b) => {
|
|
||||||
return a[property] - b[property];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds loading information given a current rundown and time
|
|
||||||
* @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 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
|
|
||||||
|
|
||||||
const orderedEvents = sortArrayByProperty(rundown, 'timeStart');
|
|
||||||
const lastEvent = orderedEvents[orderedEvents.length - 1];
|
|
||||||
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
|
||||||
|
|
||||||
let nextEvent: OntimeEvent | null = null;
|
|
||||||
let nextPublicEvent: OntimeEvent | null = null;
|
|
||||||
let currentEvent: OntimeEvent | null = null;
|
|
||||||
let currentPublicEvent: OntimeEvent | null = null;
|
|
||||||
|
|
||||||
if (timeNow > lastNormalEnd) {
|
|
||||||
// we are past last end
|
|
||||||
// preload first and find next
|
|
||||||
|
|
||||||
const firstEvent = orderedEvents[0];
|
|
||||||
nextIndex = 0;
|
|
||||||
nextEvent = firstEvent;
|
|
||||||
timeToNext = firstEvent.timeStart + dayInMs - 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;
|
|
||||||
|
|
||||||
for (const event of orderedEvents) {
|
|
||||||
// When does the event end (handle midnight)
|
|
||||||
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
|
|
||||||
|
|
||||||
const hasNotEnded = normalEnd > timeNow;
|
|
||||||
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
|
|
||||||
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
|
|
||||||
|
|
||||||
if (normalEnd <= timeNow) {
|
|
||||||
// event ran already
|
|
||||||
|
|
||||||
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 (hasNotEnded && hasStarted && !nowFound) {
|
|
||||||
// event is running
|
|
||||||
currentEvent = event;
|
|
||||||
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
|
||||||
nowId = event.id;
|
|
||||||
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
|
|
||||||
|
|
||||||
// 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 timeToEventStart = event.timeStart - timeNow;
|
|
||||||
|
|
||||||
// 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 (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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
nowIndex,
|
|
||||||
nowId,
|
|
||||||
publicIndex,
|
|
||||||
nextIndex,
|
|
||||||
publicNextIndex,
|
|
||||||
timeToNext,
|
|
||||||
nextEvent,
|
|
||||||
nextPublicEvent,
|
|
||||||
currentEvent,
|
|
||||||
currentPublicEvent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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 {CurrentTimers} currentTimers
|
|
||||||
* @returns {object} object with selection variables
|
|
||||||
*/
|
|
||||||
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: force reload of events
|
|
||||||
let doRollLoad = false;
|
|
||||||
// whether finished event should trigger
|
|
||||||
let isPrimaryFinished = false;
|
|
||||||
|
|
||||||
if (selectedEventId && current !== null) {
|
|
||||||
// if we have something selected and a timer, we are running
|
|
||||||
|
|
||||||
updatedTimer = _finishAt - clock;
|
|
||||||
if (updatedTimer > dayInMs) {
|
|
||||||
updatedTimer -= dayInMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedTimer < 0) {
|
|
||||||
isPrimaryFinished = true;
|
|
||||||
// we need a new event
|
|
||||||
doRollLoad = true;
|
|
||||||
}
|
|
||||||
} else if (secondaryTimer >= 0) {
|
|
||||||
// if secondaryTimer is running we are in waiting to roll
|
|
||||||
|
|
||||||
updatedSecondaryTimer = secondaryTarget - clock;
|
|
||||||
|
|
||||||
if (updatedSecondaryTimer <= 0) {
|
|
||||||
// we need a new event
|
|
||||||
doRollLoad = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished };
|
|
||||||
};
|
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
import {
|
import { LogOrigin, OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
LogOrigin,
|
|
||||||
OntimeBaseEvent,
|
|
||||||
OntimeBlock,
|
|
||||||
OntimeDelay,
|
|
||||||
OntimeEvent,
|
|
||||||
Playback,
|
|
||||||
SupportedEvent,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { generateId, getCueCandidate } from 'ontime-utils';
|
import { generateId, getCueCandidate } from 'ontime-utils';
|
||||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
||||||
import { MAX_EVENTS } from '../../settings.js';
|
import { MAX_EVENTS } from '../../settings.js';
|
||||||
import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js';
|
import { EventLoader } from '../../classes/event-loader/EventLoader.js';
|
||||||
import { eventTimer } from '../TimerService.js';
|
|
||||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||||
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
||||||
import {
|
import {
|
||||||
@@ -28,130 +19,17 @@ import {
|
|||||||
} from './delayedRundown.utils.js';
|
} from './delayedRundown.utils.js';
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { validateEvent } from '../../utils/parser.js';
|
import { validateEvent } from '../../utils/parser.js';
|
||||||
import { clock } from '../Clock.js';
|
import { stateMutations } from '../../state.js';
|
||||||
import { state } from '../../state.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forces rundown to be recalculated
|
* Forces rundown to be recalculated
|
||||||
* To be used when we know the rundown has changed completely
|
* To be used when we know the rundown has changed completely
|
||||||
*/
|
*/
|
||||||
export function forceReset() {
|
export function forceReset() {
|
||||||
eventLoader.reset();
|
runtimeService.reset();
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a list of IDs is in the current selection
|
|
||||||
*/
|
|
||||||
const affectedLoaded = (affectedIds: string[]) => {
|
|
||||||
const now = eventLoader.loaded.selectedEventId;
|
|
||||||
const nowPublic = eventLoader.loaded.selectedPublicEventId;
|
|
||||||
const next = eventLoader.loaded.nextEventId;
|
|
||||||
const nextPublic = eventLoader.loaded.nextPublicEventId;
|
|
||||||
return (
|
|
||||||
affectedIds.includes(now) ||
|
|
||||||
affectedIds.includes(nowPublic) ||
|
|
||||||
affectedIds.includes(next) ||
|
|
||||||
affectedIds.includes(nextPublic)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if timer replaces the loaded next
|
|
||||||
*/
|
|
||||||
const isNewNext = () => {
|
|
||||||
const timedEvents = EventLoader.getTimedEvents();
|
|
||||||
const now = eventLoader.loaded.selectedEventId;
|
|
||||||
const next = eventLoader.loaded.nextEventId;
|
|
||||||
|
|
||||||
// check whether the index of now and next are consecutive
|
|
||||||
const indexNow = timedEvents.findIndex((event) => event.id === now);
|
|
||||||
const indexNext = timedEvents.findIndex((event) => event.id === next);
|
|
||||||
|
|
||||||
if (indexNext - indexNow !== 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// iterate through timed events and see if there are public events between nowPublic and nextPublic
|
|
||||||
const nowPublic = eventLoader.loaded.selectedPublicEventId;
|
|
||||||
const nextPublic = eventLoader.loaded.nextPublicEventId;
|
|
||||||
|
|
||||||
let foundNew = false;
|
|
||||||
let isAfter = false;
|
|
||||||
for (const event of timedEvents) {
|
|
||||||
if (!isAfter) {
|
|
||||||
if (event.id === nowPublic) {
|
|
||||||
isAfter = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (event.id === nextPublic) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (event.isPublic) {
|
|
||||||
foundNew = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundNew;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates timer service when a relevant piece of data changes
|
|
||||||
*/
|
|
||||||
export function updateTimer(affectedIds?: string[]) {
|
|
||||||
const runningEventId = eventLoader.loaded.selectedEventId;
|
|
||||||
const nextEventId = eventLoader.loaded.nextEventId;
|
|
||||||
|
|
||||||
if (runningEventId === null && nextEventId === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to reload in a few scenarios:
|
|
||||||
// 1. we are not confident that changes do not affect running event
|
|
||||||
const safeOption = typeof affectedIds === 'undefined';
|
|
||||||
// 2. the edited event is in memory (now or next) running
|
|
||||||
const eventInMemory = safeOption ? false : affectedLoaded(affectedIds);
|
|
||||||
// 3. the edited event replaces next event
|
|
||||||
const isNext = isNewNext();
|
|
||||||
|
|
||||||
if (safeOption) {
|
|
||||||
eventLoader.reset();
|
|
||||||
const { eventNow } = eventLoader.loadById(runningEventId) || {};
|
|
||||||
eventTimer.hotReload(eventNow);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventInMemory) {
|
|
||||||
eventLoader.reset();
|
|
||||||
|
|
||||||
if (state.playback === Playback.Roll) {
|
|
||||||
const rollTimers = eventLoader.findRoll(clock.timeNow());
|
|
||||||
if (rollTimers === null) {
|
|
||||||
eventTimer.stop();
|
|
||||||
} else {
|
|
||||||
const { currentEvent, nextEvent } = rollTimers;
|
|
||||||
eventTimer.roll(currentEvent, nextEvent);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { eventNow } = eventLoader.loadById(runningEventId) || {};
|
|
||||||
if (eventNow) {
|
|
||||||
eventTimer.hotReload(eventNow);
|
|
||||||
} else {
|
|
||||||
eventTimer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNext) {
|
|
||||||
const { eventNow } = eventLoader.loadById(runningEventId) || {};
|
|
||||||
eventTimer.hotReload(eventNow);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description creates a new event with given data
|
* @description creates a new event with given data
|
||||||
* @param {object} eventData
|
* @param {object} eventData
|
||||||
@@ -216,8 +94,8 @@ export async function editEvent(eventData: Partial<OntimeEvent> | Partial<Ontime
|
|||||||
export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>) {
|
export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>) {
|
||||||
await cachedBatchEdit(ids, data);
|
await cachedBatchEdit(ids, data);
|
||||||
|
|
||||||
// notify timer service of changed events
|
// notify runtime service of changed events
|
||||||
updateTimer(ids);
|
runtimeService.update(ids);
|
||||||
|
|
||||||
// advice socket subscribers of change
|
// advice socket subscribers of change
|
||||||
sendRefetch();
|
sendRefetch();
|
||||||
@@ -243,7 +121,8 @@ export async function deleteEvent(eventId) {
|
|||||||
export async function deleteAllEvents() {
|
export async function deleteAllEvents() {
|
||||||
await cachedClear();
|
await cachedClear();
|
||||||
|
|
||||||
notifyChanges({ timer: true, external: true, reset: true });
|
// no need to modify timer since we will reset
|
||||||
|
notifyChanges({ external: true, reset: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,7 +163,8 @@ export async function swapEvents(from: string, to: string) {
|
|||||||
* Called when we make changes to the rundown object
|
* Called when we make changes to the rundown object
|
||||||
*/
|
*/
|
||||||
function updateChangeNumEvents() {
|
function updateChangeNumEvents() {
|
||||||
eventLoader.updateNumEvents();
|
const numEvents = EventLoader.getPlayableEvents().length;
|
||||||
|
stateMutations.updateNumEvents(numEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,10 +173,11 @@ function updateChangeNumEvents() {
|
|||||||
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean; reset?: boolean }) {
|
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean; reset?: boolean }) {
|
||||||
if (options.timer) {
|
if (options.timer) {
|
||||||
// notify timer service of changed events
|
// notify timer service of changed events
|
||||||
|
// timer can be true or an array of changed IDs
|
||||||
if (Array.isArray(options.timer)) {
|
if (Array.isArray(options.timer)) {
|
||||||
updateTimer(options.timer);
|
runtimeService.update(options.timer);
|
||||||
}
|
}
|
||||||
updateTimer();
|
runtimeService.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.reset) {
|
if (options.reset) {
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
import { LogOrigin, OntimeEvent, Playback } from 'ontime-types';
|
||||||
|
import { millisToString, validatePlayback } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { EventLoader } from '../../classes/event-loader/EventLoader.js';
|
||||||
|
import { TimerService } from '../TimerService.js';
|
||||||
|
import { logger } from '../../classes/Logger.js';
|
||||||
|
import { RestorePoint } from '../RestoreService.js';
|
||||||
|
import { state, stateMutations } from '../../state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service manages runtime status of app
|
||||||
|
* Coordinating with necessary services
|
||||||
|
*/
|
||||||
|
class RuntimeService {
|
||||||
|
private eventTimer: TimerService;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
init(resumable: RestorePoint | null) {
|
||||||
|
logger.info(LogOrigin.Server, 'Runtime service started');
|
||||||
|
// TODO: refresh at 32ms, slowing down now to keep UI responsive while we dont have granular updates
|
||||||
|
// calculate at 30fps, refresh at 1fps
|
||||||
|
this.eventTimer = new TimerService({ refresh: 1000, updateInterval: 1000 });
|
||||||
|
|
||||||
|
if (resumable) {
|
||||||
|
this.resume(resumable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
logger.info(LogOrigin.Server, 'Runtime service shutting down');
|
||||||
|
this.eventTimer.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a list of IDs is in the current selection
|
||||||
|
*/
|
||||||
|
private affectsLoaded(affectedIds: string[]): boolean {
|
||||||
|
const now = state.runtime.selectedEventId;
|
||||||
|
const nowPublic = state.runtime.selectedPublicEventId;
|
||||||
|
const next = state.runtime.nextEventId;
|
||||||
|
const nextPublic = state.runtime.nextPublicEventId;
|
||||||
|
return (
|
||||||
|
affectedIds.includes(now) ||
|
||||||
|
affectedIds.includes(nowPublic) ||
|
||||||
|
affectedIds.includes(next) ||
|
||||||
|
affectedIds.includes(nextPublic)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNewNext() {
|
||||||
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
|
const now = state.runtime.selectedEventId;
|
||||||
|
const next = state.runtime.nextEventId;
|
||||||
|
|
||||||
|
// check whether the index of now and next are consecutive
|
||||||
|
const indexNow = timedEvents.findIndex((event) => event.id === now);
|
||||||
|
const indexNext = timedEvents.findIndex((event) => event.id === next);
|
||||||
|
|
||||||
|
if (indexNext - indexNow !== 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// iterate through timed events and see if there are public events between nowPublic and nextPublic
|
||||||
|
const nowPublic = state.runtime.selectedPublicEventId;
|
||||||
|
const nextPublic = state.runtime.nextPublicEventId;
|
||||||
|
|
||||||
|
let foundNew = false;
|
||||||
|
let isAfter = false;
|
||||||
|
for (const event of timedEvents) {
|
||||||
|
if (!isAfter) {
|
||||||
|
if (event.id === nowPublic) {
|
||||||
|
isAfter = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.id === nextPublic) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (event.isPublic) {
|
||||||
|
foundNew = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
stateMutations.timer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check whether underlying data of runtime has changed
|
||||||
|
*/
|
||||||
|
update(affectedIds?: string[]) {
|
||||||
|
const hasLoadedElements = state.runtime.selectedEventId && state.runtime.nextEventId;
|
||||||
|
if (!hasLoadedElements) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to reload in a few scenarios:
|
||||||
|
// 1. we are not confident that changes do not affect running event
|
||||||
|
const safeOption = typeof affectedIds === 'undefined';
|
||||||
|
// 2. the edited event is in memory (now or next) running
|
||||||
|
const eventInMemory = safeOption ? false : this.affectsLoaded(affectedIds);
|
||||||
|
// 3. the edited event replaces next event
|
||||||
|
let isNext = false;
|
||||||
|
|
||||||
|
if (safeOption || eventInMemory) {
|
||||||
|
if (state.playback === Playback.Roll) {
|
||||||
|
this.roll();
|
||||||
|
}
|
||||||
|
// load stuff again, but keep running if our events still exist
|
||||||
|
const eventNow = EventLoader.getEventWithId(state.runtime.selectedEventId);
|
||||||
|
if (eventNow) {
|
||||||
|
stateMutations.reload(eventNow);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNext = this.isNewNext();
|
||||||
|
if (isNext) {
|
||||||
|
// TODO: do i need to load here?
|
||||||
|
const playableEvents = EventLoader.getPlayableEvents();
|
||||||
|
stateMutations.loadNext(playableEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* makes calls for loading and starting given event
|
||||||
|
* @param {OntimeEvent} event
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
loadEvent(event: OntimeEvent): boolean {
|
||||||
|
if (event.skip) {
|
||||||
|
logger.warning(LogOrigin.Playback, `Refused skipped event with ID ${event.id}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
|
stateMutations.load(event, timedEvents);
|
||||||
|
const success = event.id === state.runtime.selectedEventId;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts event matching given ID
|
||||||
|
* @param {string} eventId
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
startById(eventId: string): boolean {
|
||||||
|
const event = EventLoader.getEventWithId(eventId);
|
||||||
|
const success = this.loadEvent(event);
|
||||||
|
if (success) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts an event at index
|
||||||
|
* @param {number} eventIndex
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
startByIndex(eventIndex: number): boolean {
|
||||||
|
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||||
|
const success = this.loadEvent(event);
|
||||||
|
if (success) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts first event matching given cue
|
||||||
|
* @param {string} cue
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
startByCue(cue: string): boolean {
|
||||||
|
const event = EventLoader.getEventWithCue(cue);
|
||||||
|
const success = this.loadEvent(event);
|
||||||
|
if (success) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads event matching given ID
|
||||||
|
* @param {string} eventId
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
loadById(eventId: string): boolean {
|
||||||
|
const event = EventLoader.getEventWithId(eventId);
|
||||||
|
const success = this.loadEvent(event);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads event matching given ID
|
||||||
|
* @param {number} eventIndex
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
loadByIndex(eventIndex: number): boolean {
|
||||||
|
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||||
|
const success = this.loadEvent(event);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads first event matching given cue
|
||||||
|
* @param {string} cue
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
loadByCue(cue: string): boolean {
|
||||||
|
const event = EventLoader.getEventWithCue(cue);
|
||||||
|
const success = this.loadEvent(event);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads event before currently selected
|
||||||
|
* @return {boolean} success - whether an event was loaded
|
||||||
|
*/
|
||||||
|
loadPrevious(): boolean {
|
||||||
|
const previousEvent = EventLoader.findPrevious(state.runtime.selectedEventId);
|
||||||
|
if (previousEvent) {
|
||||||
|
const success = this.loadEvent(previousEvent);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads event after currently selected
|
||||||
|
* @return {boolean} success
|
||||||
|
*/
|
||||||
|
loadNext(): boolean {
|
||||||
|
const nextEvent = EventLoader.findNext(state.runtime.selectedEventId);
|
||||||
|
if (nextEvent) {
|
||||||
|
const success = this.loadEvent(nextEvent);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(LogOrigin.Playback, 'No next event found! Continuing playback');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts playback on selected event
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
const canStart = validatePlayback(state.playback).start;
|
||||||
|
if (canStart) {
|
||||||
|
this.eventTimer.start();
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${state.playback.toUpperCase()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts playback on next event
|
||||||
|
*/
|
||||||
|
startNext() {
|
||||||
|
const hasNext = this.loadNext();
|
||||||
|
if (hasNext) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses playback on selected event
|
||||||
|
*/
|
||||||
|
pause() {
|
||||||
|
if (validatePlayback(state.playback).pause) {
|
||||||
|
this.eventTimer.pause();
|
||||||
|
const newState = state.playback;
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops timer and unloads any events
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (validatePlayback(state.playback).stop) {
|
||||||
|
this.eventTimer.stop();
|
||||||
|
const newState = state.playback;
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads current event
|
||||||
|
*/
|
||||||
|
reload() {
|
||||||
|
if (state.runtime.selectedEventId) {
|
||||||
|
stateMutations.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets playback to roll
|
||||||
|
*/
|
||||||
|
roll() {
|
||||||
|
const playableEvents = EventLoader.getPlayableEvents();
|
||||||
|
try {
|
||||||
|
this.eventTimer.roll(playableEvents);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warning(LogOrigin.Server, `Roll: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = state.playback;
|
||||||
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.toUpperCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description resume playback state given a restore point
|
||||||
|
* @param restorePoint
|
||||||
|
*/
|
||||||
|
resume(restorePoint: RestorePoint) {
|
||||||
|
const { selectedEventId, playback } = restorePoint;
|
||||||
|
if (playback === Playback.Roll) {
|
||||||
|
this.roll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the db would have to change for the event not to exist
|
||||||
|
// we do not kow the reason for the crash, so we check anyway
|
||||||
|
const event = EventLoader.getEventWithId(selectedEventId);
|
||||||
|
if (!event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
|
stateMutations.resume(restorePoint, event, timedEvents);
|
||||||
|
logger.info(LogOrigin.Playback, 'Resuming playback');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds time to current event
|
||||||
|
* @param {number} time - time to add in milliseconds
|
||||||
|
*/
|
||||||
|
addTime(time: number) {
|
||||||
|
this.eventTimer.addTime(time);
|
||||||
|
logger.info(LogOrigin.Playback, `${time > 0 ? 'Added' : 'Removed'} ${millisToString(time)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runtimeService = new RuntimeService();
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
import { MaybeNumber, TimerType } from 'ontime-types';
|
import { MaybeNumber, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
import { dayInMs } from 'ontime-utils';
|
import { dayInMs } from 'ontime-utils';
|
||||||
|
import { TState } from '../state.js';
|
||||||
|
import { sortArrayByProperty } from '../utils/arrayUtils.js';
|
||||||
|
|
||||||
// TODO: timerUtils receive entire state object
|
/**
|
||||||
|
* handle events that span over midnight
|
||||||
|
*/
|
||||||
|
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates expected finish time of a running timer
|
* Calculates expected finish time of a running timer
|
||||||
|
* @param {TState} state runtime state
|
||||||
|
* @returns {number | null} new current time or null if nothing is running
|
||||||
*/
|
*/
|
||||||
export function getExpectedFinish(
|
export function getExpectedFinish(state: TState): MaybeNumber {
|
||||||
startedAt: MaybeNumber,
|
const { startedAt, clock, finishedAt, duration, addedTime, timerType, pausedAt } = state.timer;
|
||||||
finishedAt: MaybeNumber,
|
const { timeEnd } = state.eventNow;
|
||||||
duration: number,
|
|
||||||
pausedTime: number,
|
|
||||||
addedTime: number,
|
|
||||||
timeEnd: number,
|
|
||||||
timerType: TimerType,
|
|
||||||
) {
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -23,12 +25,14 @@ export function getExpectedFinish(
|
|||||||
return finishedAt;
|
return finishedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
|
||||||
|
|
||||||
if (timerType === TimerType.TimeToEnd) {
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
return timeEnd + addedTime + pausedTime;
|
return timeEnd + addedTime + pausedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle events that finish the day after
|
// handle events that finish the day after
|
||||||
const expectedFinish = startedAt + duration + pausedTime + addedTime;
|
const expectedFinish = startedAt + duration + addedTime + pausedTime;
|
||||||
if (expectedFinish > dayInMs) {
|
if (expectedFinish > dayInMs) {
|
||||||
return expectedFinish - dayInMs;
|
return expectedFinish - dayInMs;
|
||||||
}
|
}
|
||||||
@@ -39,41 +43,41 @@ export function getExpectedFinish(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates running countdown
|
* Calculates running countdown
|
||||||
|
* @param {TState} state runtime state
|
||||||
|
* @returns {number} current time for timer
|
||||||
*/
|
*/
|
||||||
export function getCurrent(
|
export function getCurrent(state: TState): number {
|
||||||
startedAt: MaybeNumber,
|
const { startedAt, duration, addedTime, clock, timerType, pausedAt } = state.timer;
|
||||||
duration: number,
|
const { timeEnd } = state.eventNow;
|
||||||
addedTime: number,
|
|
||||||
pausedTime: number,
|
|
||||||
clock: number,
|
|
||||||
timeEnd: number,
|
|
||||||
timerType: TimerType,
|
|
||||||
) {
|
|
||||||
if (startedAt === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timerType === TimerType.TimeToEnd) {
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
if (startedAt > timeEnd) {
|
const isNextDay = startedAt > timeEnd;
|
||||||
return timeEnd + addedTime + pausedTime + dayInMs - clock;
|
const correctDay = isNextDay ? dayInMs : 0;
|
||||||
}
|
return timeEnd + addedTime + correctDay - clock;
|
||||||
return timeEnd + addedTime + pausedTime - clock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startedAt > clock) {
|
if (startedAt === null) {
|
||||||
// we are the day after the event was started
|
return duration;
|
||||||
return startedAt + duration + addedTime + pausedTime - clock - dayInMs;
|
|
||||||
}
|
}
|
||||||
return startedAt + duration + addedTime + pausedTime - clock;
|
|
||||||
|
const hasPassedMidnight = startedAt > clock;
|
||||||
|
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
||||||
|
if (pausedAt != null) {
|
||||||
|
return startedAt + duration + addedTime - pausedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return startedAt + duration + addedTime - clock - correctDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function skippedOutOfEvent(
|
/**
|
||||||
previousTime: number,
|
* Checks whether we have skipped out of the event
|
||||||
clock: number,
|
* @param {TState} state runtime state
|
||||||
startedAt: number,
|
* @param {number} previousTime previous clock
|
||||||
expectedFinish: number,
|
* @param {number} skipLimit how much time can we skip
|
||||||
skipLimit: number,
|
* @returns {boolean}
|
||||||
): boolean {
|
*/
|
||||||
|
export function skippedOutOfEvent(state: TState, previousTime: number, skipLimit: number): boolean {
|
||||||
|
const { clock, startedAt, expectedFinish } = state.timer;
|
||||||
const hasPassedMidnight = previousTime > dayInMs - skipLimit && clock < skipLimit;
|
const hasPassedMidnight = previousTime > dayInMs - skipLimit && clock < skipLimit;
|
||||||
const adjustedClock = hasPassedMidnight ? clock + dayInMs : clock;
|
const adjustedClock = hasPassedMidnight ? clock + dayInMs : clock;
|
||||||
|
|
||||||
@@ -83,3 +87,179 @@ export function skippedOutOfEvent(
|
|||||||
|
|
||||||
return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt);
|
return hasSkipped && (adjustedClock > adjustedExpectedFinish || adjustedClock < startedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds loading information given a current rundown and time
|
||||||
|
* @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 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
|
||||||
|
|
||||||
|
const orderedEvents = sortArrayByProperty(rundown, 'timeStart');
|
||||||
|
const lastEvent = orderedEvents[orderedEvents.length - 1];
|
||||||
|
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
|
||||||
|
|
||||||
|
let nextEvent: OntimeEvent | null = null;
|
||||||
|
let nextPublicEvent: OntimeEvent | null = null;
|
||||||
|
let currentEvent: OntimeEvent | null = null;
|
||||||
|
let currentPublicEvent: OntimeEvent | null = null;
|
||||||
|
|
||||||
|
if (timeNow > lastNormalEnd) {
|
||||||
|
// we are past last end
|
||||||
|
// preload first and find next
|
||||||
|
|
||||||
|
const firstEvent = orderedEvents[0];
|
||||||
|
nextIndex = 0;
|
||||||
|
nextEvent = firstEvent;
|
||||||
|
timeToNext = firstEvent.timeStart + dayInMs - 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;
|
||||||
|
|
||||||
|
for (const event of orderedEvents) {
|
||||||
|
// When does the event end (handle midnight)
|
||||||
|
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
|
||||||
|
|
||||||
|
const hasNotEnded = normalEnd > timeNow;
|
||||||
|
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
|
||||||
|
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
|
||||||
|
|
||||||
|
if (normalEnd <= timeNow) {
|
||||||
|
// event ran already
|
||||||
|
|
||||||
|
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 (hasNotEnded && hasStarted && !nowFound) {
|
||||||
|
// event is running
|
||||||
|
currentEvent = event;
|
||||||
|
nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id);
|
||||||
|
nowId = event.id;
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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 timeToEventStart = event.timeStart - timeNow;
|
||||||
|
|
||||||
|
// 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 (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nowIndex,
|
||||||
|
nowId,
|
||||||
|
publicIndex,
|
||||||
|
nextIndex,
|
||||||
|
publicNextIndex,
|
||||||
|
timeToNext,
|
||||||
|
nextEvent,
|
||||||
|
nextPublicEvent,
|
||||||
|
currentEvent,
|
||||||
|
currentPublicEvent,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Implements update functions for roll mode
|
||||||
|
* @param {TState}
|
||||||
|
* @returns object with selection variables
|
||||||
|
*/
|
||||||
|
export const updateRoll = (state: TState) => {
|
||||||
|
const { selectedEventId } = state.runtime;
|
||||||
|
const { current, expectedFinish, startedAt, clock, secondaryTimer, secondaryTarget } = state.timer;
|
||||||
|
|
||||||
|
// timers
|
||||||
|
let updatedTimer = current;
|
||||||
|
let updatedSecondaryTimer = secondaryTimer;
|
||||||
|
// whether rollLoad should be called: force reload of events
|
||||||
|
let doRollLoad = false;
|
||||||
|
// whether finished event should trigger
|
||||||
|
let isPrimaryFinished = false;
|
||||||
|
|
||||||
|
if (selectedEventId && current !== null) {
|
||||||
|
// if we have something selected and a timer, we are running
|
||||||
|
|
||||||
|
const finishAt = expectedFinish >= startedAt ? expectedFinish : expectedFinish + dayInMs;
|
||||||
|
updatedTimer = finishAt - clock;
|
||||||
|
|
||||||
|
if (updatedTimer > dayInMs) {
|
||||||
|
updatedTimer -= dayInMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedTimer < 0) {
|
||||||
|
isPrimaryFinished = true;
|
||||||
|
// we need a new event
|
||||||
|
doRollLoad = true;
|
||||||
|
}
|
||||||
|
} else if (secondaryTimer >= 0) {
|
||||||
|
// if secondaryTimer is running we are in waiting to roll
|
||||||
|
|
||||||
|
updatedSecondaryTimer = secondaryTarget - clock;
|
||||||
|
|
||||||
|
if (updatedSecondaryTimer <= 0) {
|
||||||
|
// we need a new event
|
||||||
|
doRollLoad = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished };
|
||||||
|
};
|
||||||
|
|||||||
+271
-207
@@ -1,54 +1,81 @@
|
|||||||
import type { DeepReadonly, DeepWritable } from 'ts-essentials';
|
import type { DeepReadonly, DeepWritable } from 'ts-essentials';
|
||||||
|
|
||||||
import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState, TimerType } from 'ontime-types';
|
import {
|
||||||
|
EndAction,
|
||||||
|
Runtime,
|
||||||
|
OntimeEvent,
|
||||||
|
Playback,
|
||||||
|
TimerLifeCycle,
|
||||||
|
TimerState,
|
||||||
|
TimerType,
|
||||||
|
MaybeNumber,
|
||||||
|
} from 'ontime-types';
|
||||||
import { calculateDuration, dayInMs } from 'ontime-utils';
|
import { calculateDuration, dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
import { clock } from './services/Clock.js';
|
import { clock } from './services/Clock.js';
|
||||||
import { RestorePoint, restoreService } from './services/RestoreService.js';
|
import { RestorePoint, restoreService } from './services/RestoreService.js';
|
||||||
import { getCurrent, getExpectedFinish, skippedOutOfEvent } from './services/timerUtils.js';
|
import { getCurrent, getExpectedFinish, getRollTimers, skippedOutOfEvent, updateRoll } from './services/timerUtils.js';
|
||||||
import { eventStore } from './stores/EventStore.js';
|
import { eventStore } from './stores/EventStore.js';
|
||||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||||
import { PlaybackService } from './services/PlaybackService.js';
|
import { runtimeService } from './services/runtime-service/RuntimeService.js';
|
||||||
import { updateRoll } from './services/rollUtils.js';
|
import { EventLoader } from './classes/event-loader/EventLoader.js';
|
||||||
|
|
||||||
// TODO: move to timer config
|
// TODO: move to timer config
|
||||||
const timeSkipLimit = 3 * 32;
|
const timeSkipLimit = 1000;
|
||||||
|
|
||||||
type TState = DeepReadonly<{
|
const initialRuntime: Runtime = {
|
||||||
playback: Playback;
|
selectedEventIndex: null,
|
||||||
|
selectedEventId: null, // TODO: remove
|
||||||
|
selectedPublicEventId: null, // TODO: remove
|
||||||
|
nextEventId: null, // TODO: remove
|
||||||
|
nextPublicEventId: null, // TODO: remove
|
||||||
|
numEvents: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialTimer: TimerState = {
|
||||||
|
clock: clock.timeNow(),
|
||||||
|
current: null,
|
||||||
|
elapsed: null,
|
||||||
|
expectedFinish: null, // TODO: expected finish could account for midnight, we cleanup in the clients
|
||||||
|
addedTime: 0,
|
||||||
|
startedAt: null,
|
||||||
|
finishedAt: null,
|
||||||
|
secondaryTimer: null,
|
||||||
|
duration: null,
|
||||||
|
timerType: null, // TODO: remove
|
||||||
|
endAction: null, // TODO: remove
|
||||||
|
timeWarning: null, // TODO: remove
|
||||||
|
timeDanger: null, // TODO: remove
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TState = DeepReadonly<{
|
||||||
|
eventNow: OntimeEvent | null;
|
||||||
|
publicEventNow: OntimeEvent | null;
|
||||||
|
eventNext: OntimeEvent | null;
|
||||||
|
publicEventNext: OntimeEvent | null;
|
||||||
|
runtime: Runtime;
|
||||||
|
playback: Playback; // TODO: merge into timer?
|
||||||
|
// TODO: these are private state and should not be emitted
|
||||||
timer: TimerState & {
|
timer: TimerState & {
|
||||||
pausedTime: number;
|
pausedAt: MaybeNumber;
|
||||||
pausedAt: number | null;
|
|
||||||
timeEnd: number | null;
|
|
||||||
finishedNow: boolean;
|
finishedNow: boolean;
|
||||||
lastUpdate: number | null;
|
lastUpdate: MaybeNumber;
|
||||||
secondaryTarget: number | null;
|
secondaryTarget: MaybeNumber;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const state: TState = {
|
export const state: TState = {
|
||||||
// QUESTION: should merge playback into the timer?
|
eventNow: null,
|
||||||
playback: Playback.Stop,
|
publicEventNow: null,
|
||||||
|
eventNext: null,
|
||||||
|
publicEventNext: null,
|
||||||
|
runtime: initialRuntime,
|
||||||
|
playback: Playback.Stop, // TODO: merge into timer?
|
||||||
timer: {
|
timer: {
|
||||||
clock: clock.timeNow(),
|
...initialTimer,
|
||||||
current: null,
|
|
||||||
elapsed: null,
|
|
||||||
expectedFinish: null,
|
|
||||||
addedTime: 0,
|
|
||||||
pausedTime: 0,
|
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
timeEnd: null,
|
|
||||||
startedAt: null,
|
|
||||||
finishedAt: null,
|
|
||||||
secondaryTimer: null,
|
|
||||||
selectedEventId: null,
|
|
||||||
duration: null,
|
|
||||||
timerType: null,
|
|
||||||
endAction: null,
|
|
||||||
lastUpdate: null,
|
lastUpdate: null,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
timeWarning: null,
|
|
||||||
timeDanger: null,
|
|
||||||
get finishedNow() {
|
get finishedNow() {
|
||||||
return this.current <= 0 && this.finishedAt === null;
|
return this.current <= 0 && this.finishedAt === null;
|
||||||
},
|
},
|
||||||
@@ -56,31 +83,178 @@ export const state: TState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const stateMutations = {
|
export const stateMutations = {
|
||||||
|
load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState>) {
|
||||||
|
mutate((state) => {
|
||||||
|
stateMutations.timer.clear();
|
||||||
|
|
||||||
|
const eventIndex = rundown.findIndex((eventInMemory) => eventInMemory.id === event.id);
|
||||||
|
|
||||||
|
state.runtime.selectedEventIndex = eventIndex;
|
||||||
|
state.runtime.selectedEventId = event.id;
|
||||||
|
state.runtime.numEvents = rundown.length;
|
||||||
|
|
||||||
|
this.loadNow(event, rundown);
|
||||||
|
this.loadNext(rundown);
|
||||||
|
|
||||||
|
state.timer.clock = clock.timeNow();
|
||||||
|
state.playback = Playback.Armed;
|
||||||
|
state.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||||
|
state.timer.current = getCurrent(state);
|
||||||
|
|
||||||
|
state.timer.timerType = event.timerType;
|
||||||
|
state.timer.endAction = event.endAction;
|
||||||
|
state.timer.timeWarning = event.timeWarning;
|
||||||
|
state.timer.timeDanger = event.timeDanger;
|
||||||
|
|
||||||
|
if (initialData) {
|
||||||
|
stateMutations.timer.patch(initialData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) {
|
||||||
|
mutate((state) => {
|
||||||
|
state.eventNow = event;
|
||||||
|
|
||||||
|
// check if current is also public
|
||||||
|
if (event.isPublic) {
|
||||||
|
state.publicEventNow = event;
|
||||||
|
state.runtime.selectedPublicEventId = event.id;
|
||||||
|
} else {
|
||||||
|
// assume there is no public event
|
||||||
|
state.publicEventNow = null;
|
||||||
|
state.runtime.selectedPublicEventId = null;
|
||||||
|
|
||||||
|
// if there is nothing before, return
|
||||||
|
if (!state.runtime.selectedEventIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate backwards to find it
|
||||||
|
for (let i = state.runtime.selectedEventIndex; i >= 0; i--) {
|
||||||
|
if (playableEvents[i].isPublic) {
|
||||||
|
state.publicEventNow = playableEvents[i];
|
||||||
|
state.runtime.selectedPublicEventId = playableEvents[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadNext(playableEvents: OntimeEvent[]) {
|
||||||
|
mutate((state) => {
|
||||||
|
// assume there are no next events
|
||||||
|
state.eventNext = null;
|
||||||
|
state.publicEventNext = null;
|
||||||
|
state.runtime.nextEventId = null;
|
||||||
|
state.runtime.nextPublicEventId = null;
|
||||||
|
|
||||||
|
if (state.runtime.selectedEventIndex === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numEvents = playableEvents.length;
|
||||||
|
|
||||||
|
if (state.runtime.selectedEventIndex < numEvents - 1) {
|
||||||
|
let nextPublic = false;
|
||||||
|
let nextProduction = false;
|
||||||
|
|
||||||
|
for (let i = state.runtime.selectedEventIndex + 1; i < numEvents; i++) {
|
||||||
|
// if we have not set private
|
||||||
|
if (!nextProduction) {
|
||||||
|
state.eventNext = playableEvents[i];
|
||||||
|
state.runtime.nextEventId = playableEvents[i].id;
|
||||||
|
nextProduction = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if event is public
|
||||||
|
if (playableEvents[i].isPublic) {
|
||||||
|
state.publicEventNext = playableEvents[i];
|
||||||
|
state.runtime.nextPublicEventId = playableEvents[i].id;
|
||||||
|
nextPublic = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if both are set
|
||||||
|
if (nextPublic && nextProduction) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resume(restorePoint: RestorePoint, event: OntimeEvent, rundown: OntimeEvent[]) {
|
||||||
|
mutate((state) => {
|
||||||
|
const { playback, ...patch } = restorePoint;
|
||||||
|
|
||||||
|
// TODO: this.load gets typed as any?
|
||||||
|
stateMutations.load(event, rundown, patch);
|
||||||
|
|
||||||
|
// TODO: send as part of the patch when playback is merged to timer
|
||||||
|
state.playback = playback;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* We only pass an event if we are hot reloading
|
||||||
|
* @param {OntimeEvent} event only passed if we are changing the data if a playing timer
|
||||||
|
*/
|
||||||
|
reload(event?: OntimeEvent) {
|
||||||
|
mutate((state) => {
|
||||||
|
if (event) {
|
||||||
|
state.eventNow = event;
|
||||||
|
|
||||||
|
// update data which is duplicate between eventNow and timer objects
|
||||||
|
state.timer.duration = calculateDuration(state.eventNow.timeStart, state.eventNow.timeEnd);
|
||||||
|
state.timer.expectedFinish = getExpectedFinish(state);
|
||||||
|
state.timer.timerType = state.eventNow.timerType;
|
||||||
|
state.timer.endAction = state.eventNow.endAction;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.playback = Playback.Armed;
|
||||||
|
|
||||||
|
state.timer.duration = calculateDuration(state.eventNow.timeStart, state.eventNow.timeEnd);
|
||||||
|
state.timer.current = state.timer.duration;
|
||||||
|
state.timer.elapsed = null;
|
||||||
|
state.timer.timerType = state.eventNow.timerType;
|
||||||
|
state.timer.endAction = state.eventNow.endAction;
|
||||||
|
|
||||||
|
state.timer.startedAt = null;
|
||||||
|
state.timer.finishedAt = null;
|
||||||
|
state.timer.pausedAt = null;
|
||||||
|
state.timer.addedTime = 0;
|
||||||
|
|
||||||
|
state.timer.expectedFinish = getExpectedFinish(state);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateNumEvents(numEvents: number) {
|
||||||
|
mutate((state) => {
|
||||||
|
state.runtime.numEvents = numEvents;
|
||||||
|
});
|
||||||
|
},
|
||||||
timer: {
|
timer: {
|
||||||
// utility to reset the state of the timer
|
// utility to reset the state of the timer
|
||||||
|
// TODO: create smaller utilities to reset parts of the state
|
||||||
clear() {
|
clear() {
|
||||||
mutate((state) => {
|
mutate((state) => {
|
||||||
// TODO: check that entire state is reset here
|
// TODO: check that entire state is reset here
|
||||||
state.playback = Playback.Stop;
|
state.eventNow = null;
|
||||||
state.timer.clock = clock.timeNow();
|
state.publicEventNow = null;
|
||||||
state.timer.current = null;
|
state.eventNext = null;
|
||||||
state.timer.elapsed = null;
|
state.publicEventNext = null;
|
||||||
state.timer.expectedFinish = null;
|
|
||||||
state.timer.addedTime = 0;
|
|
||||||
state.timer.startedAt = null;
|
|
||||||
state.timer.finishedAt = null;
|
|
||||||
state.timer.secondaryTimer = null;
|
|
||||||
state.timer.selectedEventId = null;
|
|
||||||
state.timer.duration = null;
|
|
||||||
state.timer.timerType = null;
|
|
||||||
state.timer.endAction = null;
|
|
||||||
|
|
||||||
state.timer.pausedTime = 0;
|
state.runtime = { ...initialRuntime };
|
||||||
state.timer.pausedAt = null;
|
// TODO: could we avoid having this dependency?
|
||||||
state.timer.secondaryTarget = null;
|
state.runtime.numEvents = EventLoader.getPlayableEvents().length;
|
||||||
|
|
||||||
|
state.playback = Playback.Stop;
|
||||||
|
|
||||||
|
state.timer = {
|
||||||
|
...initialTimer,
|
||||||
|
clock: clock.timeNow(),
|
||||||
|
pausedAt: null,
|
||||||
|
lastUpdate: null,
|
||||||
|
secondaryTarget: null,
|
||||||
|
finishedNow: false,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// utility to allow modifying the state from the outside
|
/** utility to allow modifying the state from the outside */
|
||||||
patch(timer: Partial<TimerState>) {
|
patch(timer: Partial<TimerState>) {
|
||||||
mutate((state) => {
|
mutate((state) => {
|
||||||
for (const key in timer) {
|
for (const key in timer) {
|
||||||
@@ -93,31 +267,23 @@ export const stateMutations = {
|
|||||||
start() {
|
start() {
|
||||||
mutate(
|
mutate(
|
||||||
(state) => {
|
(state) => {
|
||||||
// TODO: we need an event to start, should we get the event or the whole rundown?
|
|
||||||
|
|
||||||
state.timer.clock = clock.timeNow();
|
state.timer.clock = clock.timeNow();
|
||||||
state.timer.secondaryTimer = null;
|
state.timer.secondaryTimer = null;
|
||||||
state.timer.secondaryTarget = null;
|
state.timer.secondaryTarget = null;
|
||||||
|
|
||||||
// add paused time if it exists
|
// add paused time if it exists
|
||||||
if (state.timer.pausedTime) {
|
if (state.timer.pausedAt) {
|
||||||
state.timer.addedTime += state.timer.pausedTime;
|
const timeToAdd = state.timer.clock - state.timer.pausedAt;
|
||||||
|
state.timer.addedTime += timeToAdd;
|
||||||
state.timer.pausedAt = null;
|
state.timer.pausedAt = null;
|
||||||
state.timer.pausedTime = 0;
|
}
|
||||||
} else if (state.timer.startedAt === null) {
|
|
||||||
|
if (state.timer.startedAt === null) {
|
||||||
state.timer.startedAt = state.timer.clock;
|
state.timer.startedAt = state.timer.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playback = Playback.Play;
|
state.playback = Playback.Play;
|
||||||
state.timer.expectedFinish = getExpectedFinish(
|
state.timer.expectedFinish = getExpectedFinish(state);
|
||||||
state.timer.startedAt,
|
|
||||||
state.timer.finishedAt,
|
|
||||||
state.timer.duration,
|
|
||||||
state.timer.pausedTime,
|
|
||||||
state.timer.addedTime,
|
|
||||||
state.timer.timeEnd,
|
|
||||||
state.timer.timerType,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sideEffect() {
|
sideEffect() {
|
||||||
@@ -141,14 +307,20 @@ export const stateMutations = {
|
|||||||
pause() {
|
pause() {
|
||||||
mutate(
|
mutate(
|
||||||
(state) => {
|
(state) => {
|
||||||
// TODO: is it easier to have a beforeAll() function that sets the timer?
|
if (state.playback !== Playback.Play) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state.playback = Playback.Pause;
|
state.playback = Playback.Pause;
|
||||||
state.timer.clock = clock.timeNow();
|
state.timer.clock = clock.timeNow();
|
||||||
state.timer.pausedAt = state.timer.clock;
|
state.timer.pausedAt = state.timer.clock;
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sideEffect() {
|
sideEffect(hasChanged: boolean) {
|
||||||
integrationService.dispatch(TimerLifeCycle.onPause);
|
if (hasChanged) {
|
||||||
|
integrationService.dispatch(TimerLifeCycle.onPause);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -159,9 +331,8 @@ export const stateMutations = {
|
|||||||
|
|
||||||
// TODO: the duplication of timer data would not be necessary
|
// TODO: the duplication of timer data would not be necessary
|
||||||
// once event loader is merged here
|
// once event loader is merged here
|
||||||
state.timer.selectedEventId = event.id;
|
state.runtime.selectedEventId = event.id;
|
||||||
state.timer.startedAt = restorePoint.startedAt;
|
state.timer.startedAt = restorePoint.startedAt;
|
||||||
state.timer.timeEnd = event.timeEnd;
|
|
||||||
state.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
state.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||||
state.timer.current = state.timer.duration;
|
state.timer.current = state.timer.duration;
|
||||||
|
|
||||||
@@ -174,98 +345,26 @@ export const stateMutations = {
|
|||||||
|
|
||||||
// check if event finished meanwhile
|
// check if event finished meanwhile
|
||||||
if (state.timer.timerType === TimerType.TimeToEnd) {
|
if (state.timer.timerType === TimerType.TimeToEnd) {
|
||||||
state.timer.current = getCurrent(
|
state.timer.current = getCurrent(state);
|
||||||
state.timer.startedAt,
|
|
||||||
state.timer.duration,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
state.timer.clock,
|
|
||||||
event.timeEnd,
|
|
||||||
state.timer.timerType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
load(event: OntimeEvent, patch?: Partial<TimerState>) {
|
|
||||||
mutate(
|
|
||||||
(state) => {
|
|
||||||
// TODO: resume and load logic are very similar
|
|
||||||
state.timer.clock = clock.timeNow();
|
|
||||||
state.timer.selectedEventId = event.id;
|
|
||||||
state.timer.timeEnd = event.timeEnd;
|
|
||||||
|
|
||||||
state.playback = Playback.Armed;
|
|
||||||
state.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
|
||||||
state.timer.timerType = event.timerType;
|
|
||||||
state.timer.endAction = event.endAction;
|
|
||||||
state.timer.timeWarning = event.timeWarning;
|
|
||||||
state.timer.timeDanger = event.timeDanger;
|
|
||||||
state.timer.pausedTime = 0;
|
|
||||||
state.timer.pausedAt = 0; // TODO: should this not be null?
|
|
||||||
|
|
||||||
state.timer.current = state.timer.duration;
|
|
||||||
if (state.timer.timerType === TimerType.TimeToEnd) {
|
|
||||||
state.timer.current = getCurrent(
|
|
||||||
state.timer.clock,
|
|
||||||
state.timer.duration,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
state.timer.clock,
|
|
||||||
event.timeEnd,
|
|
||||||
state.timer.timerType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patch) {
|
|
||||||
state.timer = { ...state.timer, ...patch };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sideEffect() {
|
|
||||||
integrationService.dispatch(TimerLifeCycle.onLoad);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
reload(timer: OntimeEvent) {
|
|
||||||
mutate((state) => {
|
|
||||||
state.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd);
|
|
||||||
state.timer.timerType = timer.timerType;
|
|
||||||
state.timer.endAction = timer.endAction;
|
|
||||||
state.timer.timeEnd = timer.timeEnd;
|
|
||||||
|
|
||||||
state.timer.finishedAt = null;
|
|
||||||
state.timer.expectedFinish = getExpectedFinish(
|
|
||||||
state.timer.startedAt,
|
|
||||||
state.timer.finishedAt,
|
|
||||||
state.timer.duration,
|
|
||||||
state.timer.pausedTime,
|
|
||||||
state.timer.addedTime,
|
|
||||||
state.timer.timeEnd,
|
|
||||||
state.timer.timerType,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state.timer.startedAt === null) {
|
|
||||||
state.timer.current = state.timer.duration;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addTime(amount: number) {
|
addTime(amount: number) {
|
||||||
mutate((state) => {
|
mutate((state) => {
|
||||||
// TODO: what kinds of validation go here or in the consumer?
|
// TODO: what kind of validation go here or in the consumer?
|
||||||
if (!state.timer.selectedEventId) {
|
if (state.timer.startedAt === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove pausedTime in favour of addedTime
|
|
||||||
state.timer.addedTime += amount;
|
state.timer.addedTime += amount;
|
||||||
|
state.timer.expectedFinish += amount;
|
||||||
|
state.timer.current += amount;
|
||||||
|
|
||||||
// handle edge cases
|
// handle edge cases
|
||||||
const willGoNegative = amount < 0 && Math.abs(amount) > state.timer.current;
|
const willGoNegative = amount < 0 && Math.abs(amount) > state.timer.current;
|
||||||
if (willGoNegative) {
|
const hasFinished = state.timer.finishedAt !== null;
|
||||||
if (state.timer.finishedAt === null) {
|
if (willGoNegative && !hasFinished) {
|
||||||
state.timer.finishedAt = clock.timeNow();
|
state.timer.finishedAt = clock.timeNow();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const willGoPositive = state.timer.current < 0 && state.timer.current + amount > 0;
|
const willGoPositive = state.timer.current < 0 && state.timer.current + amount > 0;
|
||||||
if (willGoPositive) {
|
if (willGoPositive) {
|
||||||
@@ -274,85 +373,45 @@ export const stateMutations = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// TODO: make options an object
|
// TODO: make options an object ??? maybe we can remove the options altogether?
|
||||||
|
// TODO: should we have a semaphore to stop update while other things are running?
|
||||||
update(force: boolean, updateInterval: number) {
|
update(force: boolean, updateInterval: number) {
|
||||||
return mutate(
|
return mutate(
|
||||||
(state) => {
|
(state) => {
|
||||||
function roll() {
|
function roll() {
|
||||||
const hasSkippedOutOfEvent = skippedOutOfEvent(
|
const hasSkippedOutOfEvent = skippedOutOfEvent(state, previousTime, timeSkipLimit);
|
||||||
previousTime,
|
|
||||||
state.timer.clock,
|
|
||||||
state.timer.startedAt,
|
|
||||||
state.timer.expectedFinish,
|
|
||||||
timeSkipLimit,
|
|
||||||
);
|
|
||||||
if (hasSkippedOutOfEvent) {
|
if (hasSkippedOutOfEvent) {
|
||||||
return { doRoll: true };
|
return { doRoll: true };
|
||||||
}
|
}
|
||||||
|
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(state);
|
||||||
const tempCurrentTimer = {
|
|
||||||
selectedEventId: state.timer.selectedEventId,
|
|
||||||
current: state.timer.current,
|
|
||||||
// safeguard on midnight rollover
|
|
||||||
_finishAt:
|
|
||||||
state.timer.expectedFinish >= state.timer.startedAt
|
|
||||||
? state.timer.expectedFinish
|
|
||||||
: state.timer.expectedFinish + dayInMs,
|
|
||||||
clock: state.timer.clock,
|
|
||||||
secondaryTimer: state.timer.secondaryTimer,
|
|
||||||
secondaryTarget: state.timer.secondaryTarget,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updated = updateRoll(tempCurrentTimer);
|
|
||||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updated;
|
|
||||||
state.timer.current = updatedTimer;
|
state.timer.current = updatedTimer;
|
||||||
state.timer.secondaryTimer = updatedSecondaryTimer;
|
state.timer.secondaryTimer = updatedSecondaryTimer;
|
||||||
state.timer.elapsed = state.timer.duration - state.timer.current;
|
state.timer.elapsed = state.timer.duration - state.timer.current;
|
||||||
|
|
||||||
if (isFinished) {
|
if (isFinished) {
|
||||||
state.timer.selectedEventId = null;
|
state.runtime.selectedEventId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { doRoll: doRollLoad, isFinished };
|
return { doRoll: doRollLoad, isFinished };
|
||||||
}
|
}
|
||||||
|
|
||||||
function play() {
|
function play() {
|
||||||
if (state.playback === Playback.Pause) {
|
|
||||||
state.timer.pausedTime = state.timer.clock - state.timer.pausedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFinished = false;
|
let isFinished = false;
|
||||||
|
state.timer.current = getCurrent(state);
|
||||||
|
|
||||||
if (state.playback === Playback.Play && state.timer.finishedNow) {
|
if (state.playback === Playback.Play && state.timer.finishedNow) {
|
||||||
state.timer.finishedAt = state.timer.clock;
|
state.timer.finishedAt = state.timer.clock;
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
} else {
|
} else {
|
||||||
state.timer.expectedFinish = getExpectedFinish(
|
state.timer.expectedFinish = getExpectedFinish(state);
|
||||||
state.timer.startedAt,
|
|
||||||
state.timer.finishedAt,
|
|
||||||
state.timer.duration,
|
|
||||||
state.timer.pausedTime,
|
|
||||||
state.timer.addedTime,
|
|
||||||
state.timer.timeEnd,
|
|
||||||
state.timer.timerType,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.timer.current = getCurrent(
|
|
||||||
state.timer.startedAt,
|
|
||||||
state.timer.duration,
|
|
||||||
state.timer.addedTime,
|
|
||||||
state.timer.pausedTime,
|
|
||||||
state.timer.clock,
|
|
||||||
state.timer.timeEnd,
|
|
||||||
state.timer.timerType,
|
|
||||||
);
|
|
||||||
|
|
||||||
state.timer.elapsed = state.timer.duration - state.timer.current;
|
state.timer.elapsed = state.timer.duration - state.timer.current;
|
||||||
|
|
||||||
return { isFinished };
|
return { isFinished };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: should this logic be moved up?
|
||||||
// force indicates whether the state change should be broadcast to socket
|
// force indicates whether the state change should be broadcast to socket
|
||||||
let _force = force;
|
let _force = force;
|
||||||
let _didUpdate = false;
|
let _didUpdate = false;
|
||||||
@@ -362,8 +421,9 @@ export const stateMutations = {
|
|||||||
|
|
||||||
const previousTime = state.timer.clock;
|
const previousTime = state.timer.clock;
|
||||||
state.timer.clock = clock.timeNow();
|
state.timer.clock = clock.timeNow();
|
||||||
|
const hasSkippedBack = previousTime > state.timer.clock;
|
||||||
|
|
||||||
if (previousTime > state.timer.clock) {
|
if (hasSkippedBack) {
|
||||||
_force = true;
|
_force = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +448,6 @@ export const stateMutations = {
|
|||||||
// TODO: can we simplify the didUpdate and shouldNotify
|
// TODO: can we simplify the didUpdate and shouldNotify
|
||||||
_didUpdate = true;
|
_didUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
didUpdate: _didUpdate,
|
didUpdate: _didUpdate,
|
||||||
doRoll: _doRoll,
|
doRoll: _doRoll,
|
||||||
@@ -405,7 +464,7 @@ export const stateMutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (doRoll) {
|
if (doRoll) {
|
||||||
PlaybackService.roll();
|
runtimeService.roll();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFinished) {
|
if (isFinished) {
|
||||||
@@ -414,12 +473,12 @@ export const stateMutations = {
|
|||||||
// handle end action if there was a timer playing
|
// handle end action if there was a timer playing
|
||||||
if (newState.playback === Playback.Play) {
|
if (newState.playback === Playback.Play) {
|
||||||
if (newState.timer.endAction === EndAction.Stop) {
|
if (newState.timer.endAction === EndAction.Stop) {
|
||||||
PlaybackService.stop();
|
runtimeService.stop();
|
||||||
} else if (newState.timer.endAction === EndAction.LoadNext) {
|
} else if (newState.timer.endAction === EndAction.LoadNext) {
|
||||||
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
|
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
|
||||||
setTimeout(PlaybackService.loadNext, 0);
|
setTimeout(runtimeService.loadNext, 0);
|
||||||
} else if (newState.timer.endAction === EndAction.PlayNext) {
|
} else if (newState.timer.endAction === EndAction.PlayNext) {
|
||||||
PlaybackService.startNext();
|
runtimeService.startNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,11 +486,11 @@ export const stateMutations = {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
roll(currentEvent: OntimeEvent | null, nextEvent: OntimeEvent | null) {
|
roll(rundown: OntimeEvent[]) {
|
||||||
mutate((state) => {
|
mutate((state) => {
|
||||||
stateMutations.timer.clear();
|
stateMutations.timer.clear();
|
||||||
// TODO: should we have a pre-action that updates time in all mutations?
|
|
||||||
state.timer.clock = clock.timeNow();
|
const { nextEvent, currentEvent } = getRollTimers(rundown, state.timer.clock);
|
||||||
|
|
||||||
if (currentEvent) {
|
if (currentEvent) {
|
||||||
// there is something running, load
|
// there is something running, load
|
||||||
@@ -444,7 +503,7 @@ export const stateMutations = {
|
|||||||
|
|
||||||
// when we load a timer in roll, we do the same things as before
|
// when we load a timer in roll, we do the same things as before
|
||||||
// but also pre-populate some data as to the running state
|
// but also pre-populate some data as to the running state
|
||||||
this.load(currentEvent, {
|
stateMutations.load(currentEvent, rundown, {
|
||||||
startedAt: currentEvent.timeStart,
|
startedAt: currentEvent.timeStart,
|
||||||
expectedFinish: currentEvent.timeEnd,
|
expectedFinish: currentEvent.timeEnd,
|
||||||
current: endTime - state.timer.clock,
|
current: endTime - state.timer.clock,
|
||||||
@@ -467,7 +526,7 @@ export const stateMutations = {
|
|||||||
* This function is the only way to write to the state.
|
* This function is the only way to write to the state.
|
||||||
* Mock this in the unit tests to assert state transitions and side effects.
|
* Mock this in the unit tests to assert state transitions and side effects.
|
||||||
*/
|
*/
|
||||||
function mutate<R>(
|
export function mutate<R>(
|
||||||
/**
|
/**
|
||||||
* A pure function that receives the current state and modifies it in place.
|
* A pure function that receives the current state and modifies it in place.
|
||||||
* The function can return a value that will be passed to the side effects, and
|
* The function can return a value that will be passed to the side effects, and
|
||||||
@@ -500,6 +559,11 @@ function mutate<R>(
|
|||||||
// set eventStore any time a mutation happens
|
// set eventStore any time a mutation happens
|
||||||
// this means we are pushing this data to the client every 32ms
|
// this means we are pushing this data to the client every 32ms
|
||||||
eventStore.batchSet({
|
eventStore.batchSet({
|
||||||
|
eventNow: newState.eventNow,
|
||||||
|
publicEventNow: newState.publicEventNow,
|
||||||
|
eventNext: newState.eventNext,
|
||||||
|
publicEventNext: newState.publicEventNext,
|
||||||
|
loaded: newState.runtime, // TODO: rename to runtime
|
||||||
playback: newState.playback,
|
playback: newState.playback,
|
||||||
timer: newState.timer,
|
timer: newState.timer,
|
||||||
});
|
});
|
||||||
@@ -507,7 +571,7 @@ function mutate<R>(
|
|||||||
// we write to restore service if the underlying data changes
|
// we write to restore service if the underlying data changes
|
||||||
restoreService.save({
|
restoreService.save({
|
||||||
playback: state.playback,
|
playback: state.playback,
|
||||||
selectedEventId: state.timer.selectedEventId,
|
selectedEventId: state.runtime.selectedEventId,
|
||||||
startedAt: state.timer.startedAt,
|
startedAt: state.timer.startedAt,
|
||||||
addedTime: state.timer.addedTime,
|
addedTime: state.timer.addedTime,
|
||||||
pausedAt: state.timer.pausedAt,
|
pausedAt: state.timer.pausedAt,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Playback, RuntimeStore } from 'ontime-types';
|
import { RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
import { messageService } from '../services/message-service/MessageService.js';
|
|
||||||
import { eventLoader } from '../classes/event-loader/EventLoader.js';
|
|
||||||
import { state } from '../state.js';
|
|
||||||
|
|
||||||
export type PublishFn = <T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) => void;
|
export type PublishFn = <T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) => void;
|
||||||
|
|
||||||
@@ -48,34 +45,3 @@ export const eventStore = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Module initialises the services and provides initial payload for the store
|
|
||||||
* Currently registered objects in store
|
|
||||||
* - Timer Service timer
|
|
||||||
* - Timer Service playback
|
|
||||||
* - Timer Service onAir
|
|
||||||
* - Message Service timerMessage
|
|
||||||
* - Message Service publicMessage
|
|
||||||
* - Message Service lowerMessage
|
|
||||||
* - Event Loader loaded
|
|
||||||
* - Event Loader eventNow
|
|
||||||
* - Event Loader publicEventNow
|
|
||||||
* - Event Loader eventNext
|
|
||||||
* - Event Loader publicEventNext
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const getInitialPayload = () => ({
|
|
||||||
timer: state.timer,
|
|
||||||
playback: state.playback,
|
|
||||||
onAir: state.playback !== Playback.Stop,
|
|
||||||
timerMessage: messageService.timerMessage,
|
|
||||||
publicMessage: messageService.publicMessage,
|
|
||||||
lowerMessage: messageService.lowerMessage,
|
|
||||||
externalMessage: messageService.externalMessage,
|
|
||||||
loaded: eventLoader.loaded,
|
|
||||||
eventNow: eventLoader.eventNow,
|
|
||||||
publicEventNow: eventLoader.publicEventNow,
|
|
||||||
eventNext: eventLoader.eventNext,
|
|
||||||
publicEventNext: eventLoader.publicEventNext,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { insertAtIndex, reorderArray } from '../arrayUtils.js';
|
import { insertAtIndex, reorderArray, sortArrayByProperty } from '../arrayUtils.js';
|
||||||
|
|
||||||
describe('insertAtIndex', () => {
|
describe('insertAtIndex', () => {
|
||||||
it('should insert an item at the beginning of the array', () => {
|
it('should insert an item at the beginning of the array', () => {
|
||||||
@@ -52,3 +52,37 @@ describe('reorderArray', () => {
|
|||||||
expect(result).toEqual(['b', 'c', 'a']);
|
expect(result).toEqual(['b', 'c', 'a']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sortArrayByProperty()', () => {
|
||||||
|
it('sort array 1-5', () => {
|
||||||
|
const arr1 = [{ timeStart: 1 }, { timeStart: 5 }, { timeStart: 3 }, { timeStart: 2 }, { timeStart: 4 }];
|
||||||
|
|
||||||
|
const arr1Expected = [{ timeStart: 1 }, { timeStart: 2 }, { timeStart: 3 }, { timeStart: 4 }, { timeStart: 5 }];
|
||||||
|
|
||||||
|
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
||||||
|
expect(sorted).toStrictEqual(arr1Expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sort array 1-5 with null', () => {
|
||||||
|
const arr1 = [
|
||||||
|
{ timeStart: 1 },
|
||||||
|
{ timeStart: 5 },
|
||||||
|
{ timeStart: 3 },
|
||||||
|
{ timeStart: 2 },
|
||||||
|
{ timeStart: 4 },
|
||||||
|
{ timeStart: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
const arr1Expected = [
|
||||||
|
{ timeStart: null },
|
||||||
|
{ timeStart: 1 },
|
||||||
|
{ timeStart: 2 },
|
||||||
|
{ timeStart: 3 },
|
||||||
|
{ timeStart: 4 },
|
||||||
|
{ timeStart: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
||||||
|
expect(sorted).toStrictEqual(arr1Expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -48,3 +48,16 @@ export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number)
|
|||||||
modifiedArray.splice(toIndex, 0, reorderedItem);
|
modifiedArray.splice(toIndex, 0, reorderedItem);
|
||||||
return modifiedArray;
|
return modifiedArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Sorts an array of objects by given property
|
||||||
|
* @param {array} arr - array to be sorted
|
||||||
|
* @param {string} property - property to compare
|
||||||
|
* @returns {array} copy of array sorted in ascending order
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
|
||||||
|
return [...arr].sort((a, b) => {
|
||||||
|
return a[property] - b[property];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
export type Loaded = {
|
export type Runtime = {
|
||||||
numEvents: number;
|
numEvents: number;
|
||||||
selectedEventIndex: number | null;
|
selectedEventIndex: number | null;
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Playback } from './Playback.type.js';
|
import { Playback } from './Playback.type.js';
|
||||||
import { Message, TimerMessage } from './MessageControl.type.js';
|
import { Message, TimerMessage } from './MessageControl.type.js';
|
||||||
import { TimerState } from './TimerState.type.js';
|
import { TimerState } from './TimerState.type.js';
|
||||||
import { Loaded } from './Playlist.type.js';
|
import { Runtime } from './Runtime.type.js';
|
||||||
import { OntimeEvent } from '../core/OntimeEvent.type.js';
|
import { OntimeEvent } from '../core/OntimeEvent.type.js';
|
||||||
|
|
||||||
export type RuntimeStore = {
|
export type RuntimeStore = {
|
||||||
@@ -17,7 +17,7 @@ export type RuntimeStore = {
|
|||||||
onAir: boolean;
|
onAir: boolean;
|
||||||
|
|
||||||
// event loader
|
// event loader
|
||||||
loaded: Loaded;
|
loaded: Runtime;
|
||||||
eventNow: OntimeEvent | null;
|
eventNow: OntimeEvent | null;
|
||||||
publicEventNow: OntimeEvent | null;
|
publicEventNow: OntimeEvent | null;
|
||||||
eventNext: OntimeEvent | null;
|
eventNext: OntimeEvent | null;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export type TimerState = {
|
|||||||
startedAt: number | null;
|
startedAt: number | null;
|
||||||
finishedAt: number | null; // only if timer has already finished
|
finishedAt: number | null; // only if timer has already finished
|
||||||
secondaryTimer: number | null; // used for roll mode
|
secondaryTimer: number | null; // used for roll mode
|
||||||
selectedEventId: string | null;
|
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
timerType: TimerType | null;
|
timerType: TimerType | null;
|
||||||
endAction: EndAction | null;
|
endAction: EndAction | null;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export { Playback } from './definitions/runtime/Playback.type.js';
|
|||||||
export { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
|
export { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
|
||||||
export type { Message, TimerMessage } from './definitions/runtime/MessageControl.type.js';
|
export type { Message, TimerMessage } from './definitions/runtime/MessageControl.type.js';
|
||||||
|
|
||||||
export type { Loaded } from './definitions/runtime/Playlist.type.js';
|
export type { Runtime } from './definitions/runtime/Runtime.type.js';
|
||||||
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
||||||
export type { TimerState } from './definitions/runtime/TimerState.type.js';
|
export type { TimerState } from './definitions/runtime/TimerState.type.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect } from 'vitest';
|
import { dayInMs } from '../timeConstants';
|
||||||
|
import { MILLIS_PER_HOUR } from './conversionUtils';
|
||||||
import { millisToString } from './timeFormatting';
|
import { millisToString } from './timeFormatting';
|
||||||
|
|
||||||
describe('millisToString()', () => {
|
describe('millisToString()', () => {
|
||||||
@@ -22,8 +22,8 @@ describe('millisToString()', () => {
|
|||||||
{ millis: -3600000, expected: '-01:00:00' },
|
{ millis: -3600000, expected: '-01:00:00' },
|
||||||
{ millis: -36000000, expected: '-10:00:00' },
|
{ millis: -36000000, expected: '-10:00:00' },
|
||||||
{ millis: -86399000, expected: '-23:59:59' },
|
{ millis: -86399000, expected: '-23:59:59' },
|
||||||
{ millis: -86400000, expected: '-00:00:00' },
|
{ millis: -86400000, expected: '-24:00:00' },
|
||||||
{ millis: -86401000, expected: '-00:00:01' },
|
{ millis: -86401000, expected: '-24:00:01' },
|
||||||
];
|
];
|
||||||
|
|
||||||
testScenarios.forEach((scenario) => {
|
testScenarios.forEach((scenario) => {
|
||||||
@@ -31,6 +31,10 @@ describe('millisToString()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles times over 24 hours', () => {
|
||||||
|
expect(millisToString(dayInMs + MILLIS_PER_HOUR)).toBe('25:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
test('random properties', () => {
|
test('random properties', () => {
|
||||||
const testScenarios = [
|
const testScenarios = [
|
||||||
{ millis: 300, expected: '00:00:00' },
|
{ millis: 300, expected: '00:00:00' },
|
||||||
@@ -41,27 +45,8 @@ describe('millisToString()', () => {
|
|||||||
{ millis: 3600000, expected: '01:00:00' },
|
{ millis: 3600000, expected: '01:00:00' },
|
||||||
{ millis: 36000000, expected: '10:00:00' },
|
{ millis: 36000000, expected: '10:00:00' },
|
||||||
{ millis: 86399000, expected: '23:59:59' },
|
{ millis: 86399000, expected: '23:59:59' },
|
||||||
{ millis: 86400000, expected: '00:00:00' },
|
{ millis: 86400000, expected: '24:00:00' },
|
||||||
{ millis: 86401000, expected: '00:00:01' },
|
{ millis: 86401000, expected: '24:00:01' },
|
||||||
];
|
|
||||||
|
|
||||||
testScenarios.forEach((scenario) => {
|
|
||||||
expect(millisToString(scenario.millis)).toBe(scenario.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.skip('random properties without seconds', () => {
|
|
||||||
const testScenarios = [
|
|
||||||
{ millis: 300, expected: '00:00' },
|
|
||||||
{ millis: 1000, expected: '00:00' },
|
|
||||||
{ millis: 1500, expected: '00:00' },
|
|
||||||
{ millis: 60000, expected: '00:01' },
|
|
||||||
{ millis: 600000, expected: '00:10' },
|
|
||||||
{ millis: 3600000, expected: '01:00' },
|
|
||||||
{ millis: 36000000, expected: '10:00' },
|
|
||||||
{ millis: 86399000, expected: '23:59' },
|
|
||||||
{ millis: 86400000, expected: '00:00' },
|
|
||||||
{ millis: 86401000, expected: '00:00' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
testScenarios.forEach((scenario) => {
|
testScenarios.forEach((scenario) => {
|
||||||
@@ -69,98 +54,3 @@ describe('millisToString()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* import { formatDisplay } from './formatDisplay';
|
|
||||||
|
|
||||||
describe('test string from formatDisplay function', () => {
|
|
||||||
it('test with null values', () => {
|
|
||||||
const t = { val: null, result: '00:00:00' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with not numbers', () => {
|
|
||||||
const t = { val: 'test', result: '00:00:00' };
|
|
||||||
// @ts-expect-error -- indulge me for the test
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with valid millis', () => {
|
|
||||||
const t = { val: 3600000, result: '01:00:00' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with negative millis', () => {
|
|
||||||
const t = { val: -3600000, result: '01:00:00' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with 0', () => {
|
|
||||||
const t = { val: 0, result: '00:00:00' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with -0', () => {
|
|
||||||
const t = { val: -0, result: '00:00:00' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with 86400 (24 hours)', () => {
|
|
||||||
const t = { val: 86400000, result: '00:00:00' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with 86401 (24 hours and 1 second)', () => {
|
|
||||||
const t = { val: 86401000, result: '00:00:01' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
|
||||||
const t = { val: -86401000, result: '00:00:01' };
|
|
||||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test string from formatDisplay function with hidezero', () => {
|
|
||||||
it('test with null values', () => {
|
|
||||||
const t = { val: null, result: '00:00' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with valid millis', () => {
|
|
||||||
const t = { val: 3600000, result: '01:00:00' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with negative millis', () => {
|
|
||||||
const t = { val: -3600000, result: '01:00:00' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with 0', () => {
|
|
||||||
const t = { val: 0, result: '00:00' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with -0', () => {
|
|
||||||
const t = { val: -0, result: '00:00' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with 86400 (24 hours)', () => {
|
|
||||||
const t = { val: 86400000, result: '00:00' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with 86401 (24 hours and 1 second)', () => {
|
|
||||||
const t = { val: 86401000, result: '00:01' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
|
||||||
const t = { val: -86401000, result: '00:01' };
|
|
||||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -25,7 +25,7 @@ export function millisToString(millis?: MaybeNumber, options?: FormatOptions): s
|
|||||||
const absoluteMillis = Math.abs(millis);
|
const absoluteMillis = Math.abs(millis);
|
||||||
const seconds = millisToSeconds(absoluteMillis) % 60;
|
const seconds = millisToSeconds(absoluteMillis) % 60;
|
||||||
const minutes = millisToMinutes(absoluteMillis) % 60;
|
const minutes = millisToMinutes(absoluteMillis) % 60;
|
||||||
const hours = millisToHours(absoluteMillis) % 24;
|
const hours = millisToHours(absoluteMillis);
|
||||||
|
|
||||||
const isNegative = millis < 0;
|
const isNegative = millis < 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Playback } from 'ontime-types';
|
import { Playback } from 'ontime-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple rules to determine whether a playback action is valid
|
||||||
|
*/
|
||||||
export function validatePlayback(currentPlayback: Playback) {
|
export function validatePlayback(currentPlayback: Playback) {
|
||||||
return {
|
return {
|
||||||
start: currentPlayback !== Playback.Stop,
|
start: currentPlayback !== Playback.Stop && currentPlayback !== Playback.Roll,
|
||||||
pause: currentPlayback === Playback.Play || currentPlayback === Playback.Roll,
|
pause: currentPlayback === Playback.Play,
|
||||||
roll: true,
|
roll: currentPlayback !== Playback.Roll,
|
||||||
stop: currentPlayback !== Playback.Stop,
|
stop: currentPlayback !== Playback.Stop,
|
||||||
|
reload: currentPlayback !== Playback.Stop && currentPlayback !== Playback.Roll,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user