mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 01:43:43 +00:00
v2 event timer (#274)
* lint: cleanup unused * refactor: rename component to avoid conflict * style: gap in element row * refactor: rename playstate -> playback * refactor: cleanup usages of timer and prepare integration manager * style: re-arrange button order * refactor: improve event loading * feat(timer-service): hot reload * fix: issue with duration input * chore: cleanup debug * refactor: resolve poll from runtime store * refactor: cleanup merge * refactor: small improvements in timer hot-reload
This commit is contained in:
@@ -49,7 +49,6 @@ let tray = null;
|
||||
// Start OSC Server
|
||||
await startOSCServer();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
loaded = error;
|
||||
}
|
||||
})();
|
||||
|
||||
+30
-33
@@ -1,33 +1,26 @@
|
||||
// get environment vars
|
||||
import 'dotenv/config';
|
||||
|
||||
// import config
|
||||
import { config } from './config/config.js';
|
||||
|
||||
// import dependencies
|
||||
import { dirname, join, resolve } from 'path';
|
||||
|
||||
// dependencies
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import cors from 'cors';
|
||||
|
||||
// import utils
|
||||
import { config } from './config/config.js';
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { ONTIME_VERSION } from './version.js';
|
||||
import { initSentry } from './modules/sentry.js';
|
||||
import { dirname, join, resolve } from 'path';
|
||||
|
||||
// Import Routes
|
||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
||||
import { router as eventRouter } from './routes/eventRouter.js';
|
||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||
import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
|
||||
// Global Objects
|
||||
import { EventTimer } from './classes/timer/EventTimer.js';
|
||||
import { socketProvider } from './classes/socket/SocketController.js';
|
||||
|
||||
// Start OSC server
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
// Services
|
||||
import { DataProvider } from './classes/data-provider/DataProvider.js';
|
||||
import { ONTIME_VERSION } from './version.js';
|
||||
import { initSentry } from './modules/sentry.js';
|
||||
import { socketProvider } from './classes/socket/SocketController.js';
|
||||
import { eventTimer } from './services/TimerService.js';
|
||||
|
||||
// get environment
|
||||
const env = process.env.NODE_ENV || 'production';
|
||||
@@ -65,7 +58,7 @@ app.use('/playback', playbackRouter);
|
||||
// serve static - css
|
||||
app.use('/external', express.static(join(__dirname, 'external')));
|
||||
|
||||
// serve static - react, in test mode we fetch the react app from module
|
||||
// serve static - react, in test mode we fetch the React app from module
|
||||
const resolvedPath = () => {
|
||||
const sameModule = '../';
|
||||
const siblingModule = '../../';
|
||||
@@ -74,6 +67,7 @@ const resolvedPath = () => {
|
||||
}
|
||||
return siblingModule;
|
||||
};
|
||||
|
||||
app.use(express.static(join(__dirname, resolvedPath(), 'client/build')));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
@@ -81,7 +75,7 @@ app.get('*', (req, res) => {
|
||||
});
|
||||
|
||||
// Implement catch all
|
||||
app.use((error, response, _next) => {
|
||||
app.use((error, response) => {
|
||||
response.status(400).send('Unhandled request');
|
||||
});
|
||||
|
||||
@@ -128,13 +122,10 @@ export const startOSCServer = async (overrideConfig = null) => {
|
||||
const server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* @description Starts all necessary services
|
||||
* @param overrideConfig
|
||||
* Starts servers
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
export const startServer = async (overrideConfig = null) => {
|
||||
const { http } = DataProvider.getData();
|
||||
|
||||
export const startServer = async () => {
|
||||
// Start server
|
||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||
server.listen(serverPort, '0.0.0.0');
|
||||
@@ -143,18 +134,24 @@ export const startServer = async (overrideConfig = null) => {
|
||||
await socket.initServer(server);
|
||||
socket.info('SERVER', 'Socket initialised');
|
||||
|
||||
socket.info('SERVER', returnMessage);
|
||||
socket.startListener();
|
||||
return returnMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* starts integrations
|
||||
* @param overrideConfig
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const startIntegrations = async (overrideConfig = null) => {
|
||||
const { http } = DataProvider.getData();
|
||||
|
||||
// OSC Config
|
||||
const oscConfig = {
|
||||
ip: oscIP,
|
||||
port: overrideConfig?.port || oscOutPort,
|
||||
};
|
||||
|
||||
// init timer
|
||||
global.timer = new EventTimer(socket, config.timer, oscConfig, http);
|
||||
|
||||
socket.info('SERVER', returnMessage);
|
||||
socket.startListener();
|
||||
return returnMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -166,7 +163,7 @@ export const shutdown = async () => {
|
||||
server.close();
|
||||
|
||||
shutdownOSCServer();
|
||||
global.timer.shutdown();
|
||||
eventTimer.shutdown();
|
||||
socket.shutdown();
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getRundownLenght() {
|
||||
static getRundownLength() {
|
||||
return data.rundown.length;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ export class DataProvider {
|
||||
* @param entry
|
||||
* @param index
|
||||
* @return {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
static async insertEventAt(entry, index) {
|
||||
// get events
|
||||
@@ -91,7 +90,6 @@ export class DataProvider {
|
||||
* @param entry
|
||||
* @param id
|
||||
* @return {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
static async insertEventAfterId(entry, id) {
|
||||
const index = [...data.rundown].findIndex((event) => event.id === id);
|
||||
|
||||
@@ -80,7 +80,7 @@ export class EventLoader {
|
||||
*/
|
||||
loadById(eventId) {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
return this._loadEvent(event);
|
||||
return this.loadEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,12 +90,12 @@ export class EventLoader {
|
||||
*/
|
||||
loadByIndex(eventIndex) {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
return this._loadEvent(event);
|
||||
return this.loadEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* finds the ID of the previous event
|
||||
* @returns {{id: string}|null}
|
||||
* finds the previous event
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
findPrevious() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
@@ -105,15 +105,16 @@ export class EventLoader {
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
return { id: timedEvents[0].id };
|
||||
return timedEvents[0];
|
||||
}
|
||||
|
||||
const newIndex = this.selectedEventIndex - 1;
|
||||
return { id: timedEvents?.[newIndex].id };
|
||||
return timedEvents?.[newIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* finds the ID of the next event
|
||||
* @returns {{id: string}|null}
|
||||
* finds the next event
|
||||
* @return {object | undefined}
|
||||
*/
|
||||
findNext() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
@@ -127,10 +128,10 @@ export class EventLoader {
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
return { id: timedEvents[0].id };
|
||||
return timedEvents[0];
|
||||
}
|
||||
const newIndex = this.selectedEventIndex + 1;
|
||||
return { id: timedEvents?.[newIndex].id };
|
||||
return timedEvents?.[newIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +194,7 @@ export class EventLoader {
|
||||
* loads an event given its id
|
||||
* @param {object} event
|
||||
*/
|
||||
_loadEvent(event) {
|
||||
loadEvent(event) {
|
||||
if (typeof event === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { messageManager } from '../message-manager/MessageManager.js';
|
||||
import { PlaybackService } from '../../services/playbackService.js';
|
||||
|
||||
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
|
||||
import { eventTimer } from '../../services/TimerService.js';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
|
||||
class SocketController {
|
||||
constructor() {
|
||||
@@ -61,12 +63,16 @@ class SocketController {
|
||||
|
||||
// Todo: review in favour of features
|
||||
// send state
|
||||
socket.emit('timer', global.timer.getTimeObject());
|
||||
socket.emit('playstate', global.timer.state);
|
||||
socket.emit('selected-id', global.timer.selectedEventId);
|
||||
socket.emit('next-id', global.timer.nextEventId);
|
||||
socket.emit('publicselected-id', global.timer.selectedPublicEventId);
|
||||
socket.emit('publicnext-id', global.timer.nextPublicEventId);
|
||||
socket.emit('timer', eventTimer.timer);
|
||||
socket.emit('playback', eventTimer.playback);
|
||||
socket.emit('selected', {
|
||||
id: eventLoader.selectedEventId,
|
||||
index: eventLoader.selectedEventIndex,
|
||||
total: eventLoader.numEvents,
|
||||
});
|
||||
socket.emit('next-id', eventLoader.nextEventId);
|
||||
socket.emit('publicselected-id', eventLoader.selectedPublicEventId);
|
||||
socket.emit('publicnext-id', eventLoader.nextPublicEventId);
|
||||
|
||||
/**
|
||||
* @description handle disconnecting a user
|
||||
@@ -167,16 +173,15 @@ class SocketController {
|
||||
// general playback state, useful for external sync
|
||||
// Todo: add delayed value (will come from rundownService)
|
||||
socket.on('ontime-poll', () => {
|
||||
const timerPoll = global.timer.poll();
|
||||
const timerPoll = eventTimer.timer;
|
||||
const isDelayed = false;
|
||||
const colour = '';
|
||||
socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll });
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// playstate
|
||||
socket.on('get-playstate', () => {
|
||||
socket.emit('playstate', global.timer.state);
|
||||
socket.on('get-playback', () => {
|
||||
socket.emit('playback', eventTimer.playback);
|
||||
});
|
||||
|
||||
socket.on('get-onAir', () => {
|
||||
@@ -184,30 +189,20 @@ class SocketController {
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// selection data
|
||||
socket.on('get-selected', () => {
|
||||
socket.emit('selected', {
|
||||
id: global.timer.selectedEventId,
|
||||
index: global.timer.selectedEventIndex,
|
||||
total: global.timer._eventlist.length,
|
||||
id: eventLoader.selectedEventId,
|
||||
index: eventLoader.selectedEventIndex,
|
||||
total: eventLoader.numEvents,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('get-selected-id', () => {
|
||||
socket.emit('selected-id', global.timer.selectedEventId);
|
||||
});
|
||||
|
||||
socket.on('get-next-id', () => {
|
||||
socket.emit('next-id', global.timer.nextEventId);
|
||||
});
|
||||
|
||||
// title data
|
||||
socket.on('get-titles', () => {
|
||||
socket.emit('titles', global.timer.titles);
|
||||
socket.emit('titles', eventLoader.titles);
|
||||
});
|
||||
|
||||
socket.on('get-publictitles', () => {
|
||||
socket.emit('publictitles', global.timer.titlesPublic);
|
||||
socket.emit('publictitles', eventLoader.titlesPublic);
|
||||
});
|
||||
|
||||
/***********************************/
|
||||
@@ -294,33 +289,32 @@ class SocketController {
|
||||
|
||||
// 1. RUNDOWN
|
||||
socket.on('get-feat-rundown', () => {
|
||||
global.timer._broadcastFeatureRundown();
|
||||
this.broadcastFeatureRundown();
|
||||
});
|
||||
|
||||
// 2. MESSAGE CONTROL
|
||||
socket.on('get-feat-messagecontrol', () => {
|
||||
const featureData = messageManager.getAll();
|
||||
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
|
||||
this.broadcastFeatureMessageControl();
|
||||
});
|
||||
|
||||
// 3. PLAYBACK CONTROL
|
||||
socket.on('get-feat-playbackcontrol', () => {
|
||||
global.timer._broadcastFeaturePlaybackControl();
|
||||
this.broadcastFeaturePlaybackControl();
|
||||
});
|
||||
|
||||
// 4. INFO
|
||||
socket.on('get-feat-info', () => {
|
||||
global.timer._broadcastFeatureInfo();
|
||||
this.broadcastFeatureInfo();
|
||||
});
|
||||
|
||||
// 5. CUE SHEET
|
||||
socket.on('get-feat-cuesheet', () => {
|
||||
global.timer._broadcastFeatureCuesheet();
|
||||
this.broadcastFeatureCuesheet();
|
||||
});
|
||||
|
||||
// 6. TIMER
|
||||
socket.on('get-ontime-timer', () => {
|
||||
global.timer._broadcastFeatureTimer();
|
||||
this.broadcastTimer();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -363,6 +357,83 @@ class SocketController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast data for Event List feature
|
||||
*/
|
||||
broadcastFeatureRundown() {
|
||||
const featureData = {
|
||||
selectedEventId: eventLoader.selectedEventId,
|
||||
nextEventId: eventLoader.nextEventId,
|
||||
playback: eventTimer.playback,
|
||||
};
|
||||
this.send('feat-rundown', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast data for Message Control feature
|
||||
*/
|
||||
broadcastFeatureMessageControl() {
|
||||
const featureData = messageManager.getAll();
|
||||
this.send(ADDRESS_MESSAGE_CONTROL, featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast data for Playback Control feature
|
||||
*/
|
||||
broadcastFeaturePlaybackControl() {
|
||||
const featureData = {
|
||||
playback: eventTimer.playback,
|
||||
selectedEventId: eventLoader.selectedEventId,
|
||||
numEvents: EventLoader.getNumEvents(),
|
||||
};
|
||||
this.send('feat-playbackcontrol', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast data for Info feature
|
||||
*/
|
||||
broadcastFeatureInfo() {
|
||||
const featureData = {
|
||||
titles: eventLoader.titles,
|
||||
playback: eventTimer.playback,
|
||||
selectedEventId: eventLoader.selectedEventId,
|
||||
selectedEventIndex: eventLoader.selectedEventIndex,
|
||||
numEvents: EventLoader.getNumEvents(),
|
||||
};
|
||||
this.send('feat-info', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast data for Cuesheet feature
|
||||
*/
|
||||
broadcastFeatureCuesheet() {
|
||||
const featureData = {
|
||||
playback: eventTimer.playback,
|
||||
selectedEventId: eventLoader.selectedEventId,
|
||||
selectedEventIndex: eventLoader.selectedEventIndex,
|
||||
numEvents: EventLoader.getNumEvents(),
|
||||
titleNow: eventLoader.titles.titleNow,
|
||||
};
|
||||
this.send('feat-cuesheet', featureData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast Timer feature
|
||||
*/
|
||||
broadcastTimer() {
|
||||
const featureData = eventTimer.timer;
|
||||
this.send('ontime-timer', featureData);
|
||||
}
|
||||
|
||||
broadcastState() {
|
||||
this.broadcastFeatureRundown();
|
||||
this.broadcastFeatureMessageControl();
|
||||
this.broadcastFeaturePlaybackControl();
|
||||
this.broadcastFeatureInfo();
|
||||
this.broadcastFeatureCuesheet();
|
||||
this.broadcastTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with level LOG
|
||||
* @param {string} origin
|
||||
|
||||
@@ -7,12 +7,13 @@ import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||
import { mergeObject } from '../utils/parserUtils.js';
|
||||
import { PlaybackService } from '../services/playbackService.js';
|
||||
import { runtimeState } from '../stores/EventStore.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
export const poll = async (req, res) => {
|
||||
try {
|
||||
const s = global.timer.poll();
|
||||
const s = runtimeState.poll();
|
||||
res.status(200).send(s);
|
||||
} catch (error) {
|
||||
res.status(500).send({
|
||||
|
||||
@@ -3,9 +3,9 @@ export const event = {
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
timeType: 'start-end',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
timeType: 'start-end',
|
||||
duration: 0,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { runtimeState } from '../stores/EventStore.js';
|
||||
|
||||
class TimerService {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} [timerConfig]
|
||||
* @param {number} [timerConfig.refresh]
|
||||
*/
|
||||
constructor(timerConfig) {
|
||||
this._clear();
|
||||
this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time in ms from midnight
|
||||
* @static
|
||||
* @return {number}
|
||||
*/
|
||||
static getCurrentTime() {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns expected time finish
|
||||
* @private
|
||||
*/
|
||||
_getExpectedFinish() {
|
||||
if (this.timer.startedAt === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.timer.finishedAt) {
|
||||
return this.timer.finishedAt;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
this.timer.startedAt + this.timer.duration + this._pausedInterval + this.timer.addedTime,
|
||||
this.timer.startedAt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears internal state
|
||||
* @private
|
||||
*/
|
||||
_clear() {
|
||||
this.playback = 'stop';
|
||||
this.timer = {
|
||||
clock: TimerService.getCurrentTime(),
|
||||
current: null,
|
||||
elapsed: null,
|
||||
expectedFinish: null,
|
||||
addedTime: 0,
|
||||
startedAt: null,
|
||||
finishedAt: null,
|
||||
secondaryTimer: null,
|
||||
};
|
||||
this.loadedTimer = null;
|
||||
this.loadedTimerId = null;
|
||||
this._pausedInterval = 0;
|
||||
this._pausedAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads information for currently running timer
|
||||
* @param timer
|
||||
*/
|
||||
hotReload(timer) {
|
||||
if (timer?.id !== this.loadedTimerId) {
|
||||
return;
|
||||
}
|
||||
if (timer?.skip) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
// update relevant information and force update
|
||||
this.loadedTimer = timer;
|
||||
this.timer.duration = timer.duration;
|
||||
if (this.timer.startedAt === null) {
|
||||
this.timer.current = timer.duration;
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads given timer to object
|
||||
* @param {object} timer
|
||||
* @param {number} timer.id
|
||||
* @param {number} timer.timeStart
|
||||
* @param {number} timer.timeEnd
|
||||
* @param {number} timer.duration
|
||||
* @param {string} timer.timeType
|
||||
* @param {boolean} timer.skip
|
||||
*/
|
||||
load(timer) {
|
||||
if (timer.skip) {
|
||||
throw new Error('Refuse load of skipped event');
|
||||
}
|
||||
|
||||
this._clear();
|
||||
|
||||
this.loadedTimer = timer;
|
||||
this.loadedTimerId = timer.id;
|
||||
this.timer.duration = timer.duration;
|
||||
this.timer.current = timer.duration;
|
||||
this.playback = 'armed';
|
||||
this._pausedInterval = 0;
|
||||
this._pausedAt = 0;
|
||||
|
||||
this._onLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles side effects related to onLoad event
|
||||
* @private
|
||||
*/
|
||||
_onLoad() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.loadedTimerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.playback === 'play') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer.clock = TimerService.getCurrentTime();
|
||||
|
||||
// add paused time
|
||||
if (this._pausedInterval) {
|
||||
this.timer.addedTime += this._pausedInterval;
|
||||
this._pausedAt = null;
|
||||
this._pausedInterval = 0;
|
||||
} else {
|
||||
this.timer.startedAt = this.timer.clock;
|
||||
}
|
||||
|
||||
this.playback = 'play';
|
||||
this.timer.expectedFinish = this._getExpectedFinish();
|
||||
this._onStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles side effects related to onStart event
|
||||
* @private
|
||||
*/
|
||||
_onStart() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.playback !== 'play') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playback = 'pause';
|
||||
this.timer.clock = TimerService.getCurrentTime();
|
||||
this._pausedAt = this.timer.clock;
|
||||
this._onPause();
|
||||
}
|
||||
|
||||
_onPause() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.playback === 'stop') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clear();
|
||||
this._onStop();
|
||||
}
|
||||
|
||||
_onStop() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delays running timer by given amount
|
||||
* @param {number} amount
|
||||
*/
|
||||
delay(amount) {
|
||||
if (!this.loadedTimerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer.addedTime += amount;
|
||||
this.timer.current += amount;
|
||||
this.timer.elapsed += amount;
|
||||
|
||||
// handle edge cases
|
||||
if (amount < 0 && Math.abs(amount) > this.timer.current) {
|
||||
if (this.timer.finishedAt === null) {
|
||||
// if we will make the clock negative
|
||||
this.timer.finishedAt = TimerService.getCurrentTime();
|
||||
}
|
||||
} else if (this.timer.current < 0 && this.timer.current + amount > 0) {
|
||||
// clock will go from negative to positive
|
||||
this.timer.finishedAt = null;
|
||||
}
|
||||
|
||||
// force an update
|
||||
this.update();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.timer.clock = TimerService.getCurrentTime();
|
||||
|
||||
// we only update timer if a timer has been started
|
||||
if (this.timer.startedAt !== null) {
|
||||
if (this.playback === 'pause') {
|
||||
this._pausedInterval = this.timer.clock - this._pausedAt;
|
||||
}
|
||||
|
||||
this.timer.current =
|
||||
this.timer.startedAt +
|
||||
this.timer.duration +
|
||||
this.timer.addedTime +
|
||||
this._pausedInterval -
|
||||
this.timer.clock;
|
||||
this.timer.elapsed = this.timer.duration - this.timer.current;
|
||||
|
||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
this.timer.finishedAt = this.timer.clock;
|
||||
this._onFinish();
|
||||
} else {
|
||||
this.timer.finishedAt = null;
|
||||
}
|
||||
this.timer.expectedFinish = this._getExpectedFinish();
|
||||
}
|
||||
this._onUpdate();
|
||||
}
|
||||
|
||||
_onUpdate() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
_onFinish() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
}
|
||||
|
||||
roll() {
|
||||
this._onRoll();
|
||||
}
|
||||
|
||||
_onRoll() {
|
||||
throw new Error('Roll not implemented');
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
clearInterval(this._interval);
|
||||
}
|
||||
}
|
||||
|
||||
export const eventTimer = new TimerService();
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
|
||||
/**
|
||||
* Service manages playback status of app
|
||||
@@ -15,18 +16,18 @@ export class PlaybackService {
|
||||
* @return {boolean} success
|
||||
*/
|
||||
static loadEvent(event) {
|
||||
let success = false;
|
||||
if (!event) {
|
||||
socketProvider.error('PLAYBACK', 'No event found');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.skip) {
|
||||
} else if (event.skip) {
|
||||
socketProvider.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
||||
return false;
|
||||
} else {
|
||||
eventLoader.loadEvent(event);
|
||||
eventTimer.load(event);
|
||||
success = true;
|
||||
}
|
||||
global.timer.pause();
|
||||
global.timer.loadEvent(event);
|
||||
return true;
|
||||
socketProvider.broadcastState();
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +94,10 @@ export class PlaybackService {
|
||||
static loadPrevious() {
|
||||
const previousEvent = eventLoader.findPrevious();
|
||||
if (previousEvent) {
|
||||
PlaybackService.loadById(previousEvent.id);
|
||||
global.timer.previous();
|
||||
const success = PlaybackService.loadEvent(previousEvent);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +107,10 @@ export class PlaybackService {
|
||||
static loadNext() {
|
||||
const nextEvent = eventLoader.findNext();
|
||||
if (nextEvent) {
|
||||
PlaybackService.loadById(nextEvent.id);
|
||||
global.timer.next();
|
||||
const success = PlaybackService.loadEvent(nextEvent);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,86 +118,72 @@ export class PlaybackService {
|
||||
* Starts playback on selected event
|
||||
*/
|
||||
static start() {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
if (eventLoader.selectedEventId) {
|
||||
eventTimer.start();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
}
|
||||
const newState = global.timer.start();
|
||||
if (newState === 'start') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Start');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback on selected event
|
||||
*/
|
||||
static pause() {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
if (eventLoader.selectedEventId) {
|
||||
eventTimer.pause();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
}
|
||||
const newState = global.timer.pause();
|
||||
if (newState === 'pause') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Paused');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops timer and unloads any events
|
||||
*/
|
||||
static stop() {
|
||||
if (!eventLoader.selectedEventId && global.timer.state !== 'roll') {
|
||||
return;
|
||||
if (eventLoader.selectedEventId || eventTimer.playback === 'roll') {
|
||||
eventLoader.reset();
|
||||
eventTimer.stop();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
}
|
||||
eventLoader.reset();
|
||||
const newState = global.timer.stop();
|
||||
if (newState === 'stop') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Stopped');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads current event
|
||||
*/
|
||||
static reload() {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
if (eventLoader.selectedEventId) {
|
||||
this.loadById(eventLoader.selectedEventId);
|
||||
}
|
||||
const newState = global.timer.reload();
|
||||
socketProvider.info('PLAYBACK', 'Reloaded event');
|
||||
socketProvider.send('playstate', newState);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback to roll
|
||||
*/
|
||||
static roll() {
|
||||
if (!EventLoader.getNumEvents()) {
|
||||
return;
|
||||
if (EventLoader.getNumEvents() && eventTimer.playback !== 'roll') {
|
||||
eventTimer.roll();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState}`);
|
||||
socketProvider.send('playback', newState);
|
||||
}
|
||||
|
||||
if (global.timer.state === 'roll') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = global.timer.roll();
|
||||
if (newState === 'roll') {
|
||||
socketProvider.info('PLAYBACK', 'Play Mode Roll');
|
||||
}
|
||||
socketProvider.send('playstate', newState);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds delay to current event
|
||||
* @param {number} delayTime time in ms
|
||||
* @param {number} delayTime time in minutes
|
||||
*/
|
||||
static setDelay(delayTime) {
|
||||
if (!eventLoader.selectedEventId) {
|
||||
return;
|
||||
if (eventLoader.selectedEventId) {
|
||||
const delayInMs = delayTime * 1000 * 60;
|
||||
eventTimer.delay(delayInMs);
|
||||
socketProvider.info('PLAYBACK', `Added ${delayTime} min delay`);
|
||||
}
|
||||
const delayInMs = delayTime * 1000 * 60;
|
||||
global.timer.increment(delayInMs);
|
||||
socketProvider.info('PLAYBACK', `Added ${delayTime} min delay`);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,20 @@ import {
|
||||
} from '../models/eventsDefinition.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param affectedIds
|
||||
* @returns boolean
|
||||
*/
|
||||
const affectedLoaded = (affectedIds) => {
|
||||
const now = eventLoader.selectedEventId;
|
||||
const nowPublic = eventLoader.selectedPublicEventId;
|
||||
const next = eventLoader.nextEventId;
|
||||
const nextPublic = eventLoader.nextPublicEventId;
|
||||
return (
|
||||
affectedIds.includes(now) ||
|
||||
affectedIds.includes(now) ||
|
||||
affectedIds.includes(nowPublic) ||
|
||||
affectedIds.includes(next) ||
|
||||
@@ -65,24 +71,41 @@ const isNewNext = () => {
|
||||
*/
|
||||
export function updateTimer(affectedIds) {
|
||||
const runningEventId = eventLoader.selectedEventId;
|
||||
|
||||
if (runningEventId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we need to reload in a few scenarios:
|
||||
// 1. we are not confident that changes do not affect running event
|
||||
// 2. the edited event is currently being used (now or next)
|
||||
// 3. the edited event replaces one of the previous (next)
|
||||
if (typeof affectedIds === 'undefined') {
|
||||
global.timer.syncLoaded(runningEventId);
|
||||
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 loadedEvent = eventLoader.loadById(runningEventId);
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
return true;
|
||||
}
|
||||
if (affectedLoaded(affectedIds)) {
|
||||
global.timer.syncLoaded(runningEventId);
|
||||
|
||||
if (eventInMemory) {
|
||||
const loadedEvent = eventLoader.loadById(runningEventId);
|
||||
if (!loadedEvent) {
|
||||
// event was deleted
|
||||
eventLoader.reset();
|
||||
eventTimer.stop();
|
||||
} else {
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isNewNext()) {
|
||||
global.timer.syncLoaded(runningEventId);
|
||||
|
||||
if (isNext) {
|
||||
const loadedEvent = eventLoader.loadById(runningEventId);
|
||||
eventTimer.hotReload(loadedEvent);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -94,7 +117,7 @@ export function updateTimer(affectedIds) {
|
||||
* @return {unknown[]}
|
||||
*/
|
||||
export async function addEvent(eventData) {
|
||||
const numEvents = DataProvider.getRundownLenght();
|
||||
const numEvents = DataProvider.getRundownLength();
|
||||
if (numEvents > MAX_EVENTS) {
|
||||
throw new Error(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||
}
|
||||
@@ -126,6 +149,7 @@ export async function addEvent(eventData) {
|
||||
throw new Error(error);
|
||||
}
|
||||
updateTimer([id]);
|
||||
socketProvider.broadcastState();
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -137,6 +161,7 @@ export async function editEvent(eventData) {
|
||||
}
|
||||
const newEvent = await DataProvider.updateEventById(eventId, eventData);
|
||||
updateTimer([eventId]);
|
||||
socketProvider.broadcastState();
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -148,6 +173,7 @@ export async function editEvent(eventData) {
|
||||
export async function deleteEvent(eventId) {
|
||||
await DataProvider.deleteEvent(eventId);
|
||||
updateTimer([eventId]);
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +183,7 @@ export async function deleteEvent(eventId) {
|
||||
export async function deleteAllEvents() {
|
||||
await DataProvider.clearRundown();
|
||||
updateTimer();
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,4 +264,5 @@ export async function applyDelay(eventId) {
|
||||
// update rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
updateTimer();
|
||||
socketProvider.broadcastState();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
const store = {};
|
||||
|
||||
/**
|
||||
* A runtime store that broadcasts its payload
|
||||
*/
|
||||
export const runtimeState = {
|
||||
get(key) {
|
||||
return store[key];
|
||||
},
|
||||
set(key, value) {
|
||||
store[key] = value;
|
||||
socketProvider.send(key, value);
|
||||
},
|
||||
poll() {
|
||||
return store;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user