mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +00:00
V2 ws store (#310)
* Update TimerService.ts * refactor: message service publishes to store * refactor: several type improvements * V2 ws store wss (#309) * refactor: shared logging types * refactor: simplify message service consumption * refactor: create discrete logging system * refactor: move socket.io > websocket
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export interface IAdapter {
|
||||
shutdown: () => void;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Server } from 'node-osc';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
|
||||
export class OscServer implements IAdapter {
|
||||
private osc: Server;
|
||||
|
||||
constructor(config: OSCSettings) {
|
||||
this.osc = new Server(config.portIn, '0.0.0.0');
|
||||
|
||||
this.osc.on('error', console.error);
|
||||
|
||||
this.osc.on('message', (msg) => {
|
||||
// message should look like /ontime/{path} {args} where
|
||||
// ontime: fixed message for app
|
||||
// path: command to be called
|
||||
// args: extra data, only used on some API entries (delay, goto)
|
||||
|
||||
// split message
|
||||
const [, address, path] = msg[0].split('/');
|
||||
const args = msg[1];
|
||||
|
||||
// get first part before (ontime)
|
||||
if (address !== 'ontime') {
|
||||
logger.error('RX', `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// get second part (command)
|
||||
if (!path) {
|
||||
logger.error('RX', 'OSC IN: No path found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const reply = dispatchFromAdapter(path, args, 'osc');
|
||||
if (reply) {
|
||||
const { topic, payload } = reply;
|
||||
this.osc.emit(topic, payload);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('RX', `OSC IN: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
console.log('Shutting down OSC Server');
|
||||
this.osc?.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* DESIGN BY CONTRACT
|
||||
* ===================
|
||||
* All websocket calls are expected to follow the defined format,
|
||||
* otherwise they will be ignored by Ontime server
|
||||
*
|
||||
* Messages should be in JSON format with two top level objects
|
||||
* {
|
||||
* type: ...
|
||||
* payload: ...
|
||||
* }
|
||||
*
|
||||
* Type: describes the action to be performed as enumerated in the API design
|
||||
* Payload: adds necessary payload for the request to be completed
|
||||
*/
|
||||
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
import getRandomName from '../utils/getRandomName.js';
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
|
||||
let instance;
|
||||
|
||||
export class SocketServer implements IAdapter {
|
||||
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
||||
|
||||
private wss: WebSocketServer | null;
|
||||
private clientIds: Set<string>;
|
||||
|
||||
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.clientIds = new Set<string>();
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
init(server) {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
const clientId = getRandomName();
|
||||
this.clientIds.add(clientId);
|
||||
logger.info('RX', `${this.wss.clients.size} Connections with new: ${clientId}`);
|
||||
|
||||
// send store payload on connect
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'ontime',
|
||||
payload: eventStore.poll(),
|
||||
}),
|
||||
);
|
||||
|
||||
ws.on('error', console.error);
|
||||
|
||||
ws.on('close', () => {
|
||||
logger.info('RX', `${this.wss.clients.size} Connections with disconnected: ${clientId}`);
|
||||
this.clientIds.delete(clientId);
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
if (data.length > this.MAX_PAYLOAD) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
// TODO: protocol specific stuff should be handled here
|
||||
// eg: rename-client
|
||||
// socket.on('rename-client', (newName) => {
|
||||
// if (newName) {
|
||||
// const previousName = this._clientNames[socket.id];
|
||||
// this._clientNames[socket.id] = newName;
|
||||
// this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
|
||||
// }
|
||||
// });
|
||||
|
||||
try {
|
||||
const message = JSON.parse(data);
|
||||
const { type, payload } = message;
|
||||
|
||||
if (type === 'hello') {
|
||||
ws.send('hi');
|
||||
}
|
||||
|
||||
if (type === 'ontime-log') {
|
||||
console.log('attempted adding to log');
|
||||
}
|
||||
|
||||
try {
|
||||
const reply = dispatchFromAdapter(type, payload, 'ws');
|
||||
if (reply) {
|
||||
const { topic, payload } = reply;
|
||||
ws.send(topic, payload);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('RX', `WS IN: ${error}`);
|
||||
}
|
||||
} catch (_) {
|
||||
// we ignore unknown
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// message is any serializable value
|
||||
send(message: any) {
|
||||
this.wss?.clients.forEach((client) => {
|
||||
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this.wss?.close();
|
||||
}
|
||||
}
|
||||
|
||||
export const socket = new SocketServer();
|
||||
+33
-29
@@ -6,8 +6,7 @@ import cors from 'cors';
|
||||
// import utils
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { initSentry } from './modules/sentry.js';
|
||||
import { initSentry, reportSentryException } from './modules/sentry.js';
|
||||
import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
@@ -18,13 +17,18 @@ import { router as eventDataRouter } from './routes/eventDataRouter.js';
|
||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||
import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
|
||||
// Services
|
||||
// Import adapters
|
||||
import { OscServer } from './adapters/OscAdapter.js';
|
||||
import { socket } from './adapters/WebsocketAdapter.js';
|
||||
import { DataProvider } from './classes/data-provider/DataProvider.js';
|
||||
import { socketProvider } from './classes/socket/SocketController.js';
|
||||
import { eventTimer } from './services/TimerService.js';
|
||||
import { dbLoadingProcess } from './modules/loadDb.js';
|
||||
|
||||
// Services
|
||||
import { eventTimer } from './services/TimerService.js';
|
||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||
import { OscIntegration } from './services/integration-service/OscIntegration.js';
|
||||
import { logger } from './classes/Logger.js';
|
||||
import { eventLoader } from './classes/event-loader/EventLoader.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -33,10 +37,7 @@ if (!isProduction) {
|
||||
console.log(`Ontime directory at ${currentDirectory} `);
|
||||
}
|
||||
|
||||
initSentry(environment);
|
||||
|
||||
// import socket provider
|
||||
const socketServer = socketProvider;
|
||||
initSentry(isProduction);
|
||||
|
||||
// Create express APP
|
||||
const app = express();
|
||||
@@ -100,6 +101,9 @@ enum OntimeStartOrder {
|
||||
}
|
||||
|
||||
let step = OntimeStartOrder.InitDB;
|
||||
let expressServer = null;
|
||||
let oscServer = null;
|
||||
|
||||
const checkStart = (currentState: OntimeStartOrder) => {
|
||||
if (step !== currentState) {
|
||||
step = OntimeStartOrder.Error;
|
||||
@@ -116,9 +120,6 @@ export const startDb = async () => {
|
||||
await dbLoadingProcess;
|
||||
};
|
||||
|
||||
// create HTTP server
|
||||
const expressServer = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Starts servers
|
||||
* @return {Promise<string>}
|
||||
@@ -128,11 +129,13 @@ export const startServer = async () => {
|
||||
|
||||
const serverPort = 4001; // hardcoded for now
|
||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||
expressServer.listen(serverPort, '0.0.0.0');
|
||||
|
||||
socketServer.initServer(expressServer);
|
||||
socketServer.info('SERVER', returnMessage);
|
||||
socketServer.startListener();
|
||||
expressServer = http.createServer(app);
|
||||
|
||||
socket.init(expressServer);
|
||||
eventLoader.init();
|
||||
|
||||
expressServer.listen(serverPort, '0.0.0.0');
|
||||
|
||||
return returnMessage;
|
||||
};
|
||||
@@ -149,7 +152,7 @@ export const startOSCServer = async (overrideConfig = null) => {
|
||||
const { osc } = DataProvider.getData();
|
||||
|
||||
if (!osc.enabledIn) {
|
||||
socketServer.info('RX', 'OSC Input Disabled');
|
||||
logger.info('RX', 'OSC Input Disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,8 +163,8 @@ export const startOSCServer = async (overrideConfig = null) => {
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
socketServer.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||
initiateOSC(oscSettings);
|
||||
logger.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||
oscServer = new OscServer(oscSettings);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -178,7 +181,7 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
||||
|
||||
const oscIntegration = new OscIntegration();
|
||||
const { success, message } = oscIntegration.init(osc);
|
||||
socketServer.info('RX', message);
|
||||
logger.info('RX', message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
@@ -193,25 +196,26 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
||||
export const shutdown = async (exitCode = 0) => {
|
||||
console.log(`Ontime shutting down with code ${exitCode}`);
|
||||
|
||||
expressServer.close();
|
||||
shutdownOSCServer();
|
||||
expressServer?.close();
|
||||
oscServer?.shutdown();
|
||||
eventTimer.shutdown();
|
||||
socketServer.shutdown();
|
||||
integrationService.shutdown();
|
||||
logger.shutdown();
|
||||
socket.shutdown();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`));
|
||||
|
||||
process.on('unhandledRejection', async (error, promise) => {
|
||||
console.error(error, 'Error: unhandled rejection', promise);
|
||||
socketServer.error('SERVER', 'Error: unhandled rejection');
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
reportSentryException(error);
|
||||
logger.error('SERVER', 'Error: unhandled rejection');
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error, promise) => {
|
||||
console.error(error, 'Error: uncaught exception', promise);
|
||||
socketServer.error('SERVER', 'Error: uncaught exception');
|
||||
process.on('uncaughtException', async (error) => {
|
||||
reportSentryException(error);
|
||||
logger.error('SERVER', 'Error: uncaught exception');
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Log, LogLevel } from 'ontime-types';
|
||||
import { generateId, millisToString } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { isProduction } from '../setup.js';
|
||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
|
||||
class Logger {
|
||||
private queue: Log[];
|
||||
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabling setup logger after init
|
||||
*/
|
||||
init() {
|
||||
this.queue.forEach((log) => {
|
||||
this._push(log);
|
||||
});
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
private addToQueue(log: Log) {
|
||||
this.queue.push(log);
|
||||
if (this.queue.length > 100) {
|
||||
this.queue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal safe push method, adds log to queue if callback not available
|
||||
* @param log
|
||||
*/
|
||||
private _push(log: Log) {
|
||||
if (!isProduction) {
|
||||
console.log(`[${log.level}] \t ${log.origin} \t ${log.text}`);
|
||||
}
|
||||
|
||||
try {
|
||||
socket.send({
|
||||
type: 'ontime-log',
|
||||
payload: log,
|
||||
});
|
||||
} catch (_e) {
|
||||
this.addToQueue(log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits logging message
|
||||
* @param level
|
||||
* @param origin
|
||||
* @param text
|
||||
*/
|
||||
emit(level, origin: string, text: string) {
|
||||
const log = {
|
||||
id: generateId(),
|
||||
level,
|
||||
origin,
|
||||
text,
|
||||
time: millisToString(clock.getSystemTime() || 0),
|
||||
};
|
||||
this._push(log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to emit logging message of type INFO
|
||||
* @param origin
|
||||
* @param text
|
||||
*/
|
||||
info(origin: string, text: string) {
|
||||
this.emit(LogLevel.Info, origin, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to emit logging message of type WARN
|
||||
* @param origin
|
||||
* @param text
|
||||
*/
|
||||
warning(origin: string, text: string) {
|
||||
this.emit(LogLevel.Warn, origin, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to emit logging message of type ERROR
|
||||
* @param origin
|
||||
* @param text
|
||||
*/
|
||||
error(origin: string, text: string) {
|
||||
this.emit(LogLevel.Error, origin, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown logger
|
||||
*/
|
||||
shutdown() {
|
||||
console.log('Shutting down logger');
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
@@ -1,31 +1,17 @@
|
||||
import { OntimeEvent, TitleBlock, Loaded } from 'ontime-types';
|
||||
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
import { getRollTimers } from '../../services/rollUtils.js';
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
let instance;
|
||||
|
||||
type TitleBlock = {
|
||||
titleNow: string | null;
|
||||
subtitleNow: string | null;
|
||||
presenterNow: string | null;
|
||||
noteNow: string | null;
|
||||
titleNext: string | null;
|
||||
subtitleNext: string | null;
|
||||
presenterNext: string | null;
|
||||
noteNext: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages business logic around loading events
|
||||
*/
|
||||
export class EventLoader {
|
||||
loadedEvent: object | null;
|
||||
numEvents: number | null;
|
||||
selectedEventIndex: number | null;
|
||||
selectedEventId: string | null;
|
||||
selectedPublicEventId: string | null;
|
||||
nextEventId: string | null;
|
||||
nextPublicEventId: string | null;
|
||||
loadedEvent: OntimeEvent | null;
|
||||
loaded: Loaded;
|
||||
titles: TitleBlock;
|
||||
titlesPublic: TitleBlock;
|
||||
|
||||
@@ -36,7 +22,10 @@ export class EventLoader {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||
instance = this;
|
||||
this.reset(false);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.reset();
|
||||
this.loadedEvent = null;
|
||||
}
|
||||
|
||||
@@ -122,16 +111,16 @@ export class EventLoader {
|
||||
*/
|
||||
findPrevious() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
if (timedEvents === null || !timedEvents.length || this.selectedEventIndex === 0) {
|
||||
if (timedEvents === null || !timedEvents.length || this.loaded.selectedEventIndex === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
if (this.loaded.selectedEventIndex === null) {
|
||||
return timedEvents[0];
|
||||
}
|
||||
|
||||
const newIndex = this.selectedEventIndex - 1;
|
||||
const newIndex = this.loaded.selectedEventIndex - 1;
|
||||
return timedEvents?.[newIndex];
|
||||
}
|
||||
|
||||
@@ -141,15 +130,15 @@ export class EventLoader {
|
||||
*/
|
||||
findNext() {
|
||||
const timedEvents = EventLoader.getPlayableEvents();
|
||||
if (timedEvents === null || !timedEvents.length || this.selectedEventIndex === this.numEvents - 1) {
|
||||
if (timedEvents === null || !timedEvents.length || this.loaded.selectedEventIndex === this.loaded.numEvents - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (this.selectedEventIndex === null) {
|
||||
if (this.loaded.selectedEventIndex === null) {
|
||||
return timedEvents[0];
|
||||
}
|
||||
const newIndex = this.selectedEventIndex + 1;
|
||||
const newIndex = this.loaded.selectedEventIndex + 1;
|
||||
return timedEvents?.[newIndex];
|
||||
}
|
||||
|
||||
@@ -167,9 +156,9 @@ export class EventLoader {
|
||||
getRollTimers(timedEvents, timeNow);
|
||||
|
||||
this.loadedEvent = currentEvent;
|
||||
this.selectedEventIndex = nowIndex;
|
||||
this.selectedEventId = currentEvent?.id || null;
|
||||
this.numEvents = timedEvents.length;
|
||||
this.loaded.selectedEventIndex = nowIndex;
|
||||
this.loaded.selectedEventId = currentEvent?.id || null;
|
||||
this.loaded.numEvents = timedEvents.length;
|
||||
|
||||
// titles
|
||||
this._loadThisTitles(currentEvent, 'now-private');
|
||||
@@ -187,28 +176,33 @@ export class EventLoader {
|
||||
getLoaded() {
|
||||
return {
|
||||
loadedEvent: this.loadedEvent,
|
||||
selectedEventIndex: this.selectedEventIndex,
|
||||
selectedEventId: this.selectedEventId,
|
||||
selectedPublicEventId: this.selectedPublicEventId,
|
||||
nextEventId: this.nextEventId,
|
||||
nextPublicEventId: this.nextPublicEventId,
|
||||
numEvents: this.numEvents,
|
||||
loaded: this.loaded,
|
||||
titles: this.titles,
|
||||
titlesPublic: this.titlesPublic,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?: boolean) {
|
||||
reset(emit = true) {
|
||||
this.loadedEvent = null;
|
||||
this.selectedEventIndex = null;
|
||||
this.selectedEventId = null;
|
||||
this.selectedPublicEventId = null;
|
||||
this.nextEventId = null;
|
||||
this.nextPublicEventId = null;
|
||||
this.numEvents = null;
|
||||
this.loaded = {
|
||||
selectedEventIndex: null,
|
||||
selectedEventId: null,
|
||||
selectedPublicEventId: null,
|
||||
nextEventId: null,
|
||||
nextPublicEventId: null,
|
||||
numEvents: EventLoader.getPlayableEvents().length,
|
||||
};
|
||||
this.titles = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
@@ -250,9 +244,9 @@ export class EventLoader {
|
||||
|
||||
// we know some stuff now
|
||||
this.loadedEvent = event;
|
||||
this.selectedEventIndex = eventIndex;
|
||||
this.selectedEventId = event.id;
|
||||
this.numEvents = timedEvents.length;
|
||||
this.loaded.selectedEventIndex = eventIndex;
|
||||
this.loaded.selectedEventId = event.id;
|
||||
this.loaded.numEvents = timedEvents.length;
|
||||
// this.nextEventId = playableEvents[eventIndex + 1].id;
|
||||
this._loadTitlesNow(event, playableEvents);
|
||||
this._loadTitlesNext(playableEvents);
|
||||
@@ -266,6 +260,7 @@ export class EventLoader {
|
||||
* Handle side effects from event loading
|
||||
*/
|
||||
private _loadEvent() {
|
||||
eventStore.set('loaded', this.loaded);
|
||||
eventStore.set('titles', this.titles);
|
||||
eventStore.set('titlesPublic', this.titlesPublic);
|
||||
}
|
||||
@@ -288,13 +283,13 @@ export class EventLoader {
|
||||
this.titlesPublic.titleNow = null;
|
||||
this.titlesPublic.subtitleNow = null;
|
||||
this.titlesPublic.presenterNow = null;
|
||||
this.selectedPublicEventId = null;
|
||||
this.loaded.selectedPublicEventId = null;
|
||||
|
||||
// if there is nothing before, return
|
||||
if (this.selectedEventIndex === 0) return;
|
||||
if (this.loaded.selectedEventIndex === 0) return;
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = this.selectedEventIndex; i >= 0; i--) {
|
||||
for (let i = this.loaded.selectedEventIndex; i >= 0; i--) {
|
||||
if (rundown[i].isPublic) {
|
||||
this._loadThisTitles(rundown[i], 'now-public');
|
||||
break;
|
||||
@@ -309,27 +304,27 @@ export class EventLoader {
|
||||
*/
|
||||
private _loadTitlesNext(rundown) {
|
||||
// maybe there is nothing to load
|
||||
if (this.selectedEventIndex === null) return;
|
||||
if (this.loaded.selectedEventIndex === null) return;
|
||||
|
||||
// assume there is no next event
|
||||
this.titles.titleNext = null;
|
||||
this.titles.subtitleNext = null;
|
||||
this.titles.presenterNext = null;
|
||||
this.titles.noteNext = null;
|
||||
this.nextEventId = null;
|
||||
this.loaded.nextEventId = null;
|
||||
|
||||
this.titlesPublic.titleNext = null;
|
||||
this.titlesPublic.subtitleNext = null;
|
||||
this.titlesPublic.presenterNext = null;
|
||||
this.nextPublicEventId = null;
|
||||
this.loaded.nextPublicEventId = null;
|
||||
|
||||
const numEvents = rundown.length;
|
||||
|
||||
if (this.selectedEventIndex < numEvents - 1) {
|
||||
if (this.loaded.selectedEventIndex < numEvents - 1) {
|
||||
let nextPublic = false;
|
||||
let nextPrivate = false;
|
||||
|
||||
for (let i = this.selectedEventIndex + 1; i < numEvents; i++) {
|
||||
for (let i = this.loaded.selectedEventIndex + 1; i < numEvents; i++) {
|
||||
// if we have not set private
|
||||
if (!nextPrivate) {
|
||||
this._loadThisTitles(rundown[i], 'next-private');
|
||||
@@ -367,14 +362,14 @@ export class EventLoader {
|
||||
this.titlesPublic.subtitleNow = event.subtitle;
|
||||
this.titlesPublic.presenterNow = event.presenter;
|
||||
this.titlesPublic.noteNow = event.note;
|
||||
this.selectedPublicEventId = event.id;
|
||||
this.loaded.selectedPublicEventId = event.id;
|
||||
|
||||
// private
|
||||
this.titles.titleNow = event.title;
|
||||
this.titles.subtitleNow = event.subtitle;
|
||||
this.titles.presenterNow = event.presenter;
|
||||
this.titles.noteNow = event.note;
|
||||
this.selectedEventId = event.id;
|
||||
this.loaded.selectedEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'now-public':
|
||||
@@ -382,7 +377,7 @@ export class EventLoader {
|
||||
this.titlesPublic.subtitleNow = event.subtitle;
|
||||
this.titlesPublic.presenterNow = event.presenter;
|
||||
this.titlesPublic.noteNow = event.note;
|
||||
this.selectedPublicEventId = event.id;
|
||||
this.loaded.selectedPublicEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'now-private':
|
||||
@@ -390,7 +385,7 @@ export class EventLoader {
|
||||
this.titles.subtitleNow = event.subtitle;
|
||||
this.titles.presenterNow = event.presenter;
|
||||
this.titles.noteNow = event.note;
|
||||
this.selectedEventId = event.id;
|
||||
this.loaded.selectedEventId = event.id;
|
||||
break;
|
||||
|
||||
// next, load to both public and private
|
||||
@@ -400,14 +395,14 @@ export class EventLoader {
|
||||
this.titlesPublic.subtitleNext = event.subtitle;
|
||||
this.titlesPublic.presenterNext = event.presenter;
|
||||
this.titlesPublic.noteNext = event.note;
|
||||
this.nextPublicEventId = event.id;
|
||||
this.loaded.nextPublicEventId = event.id;
|
||||
|
||||
// private
|
||||
this.titles.titleNext = event.title;
|
||||
this.titles.subtitleNext = event.subtitle;
|
||||
this.titles.presenterNext = event.presenter;
|
||||
this.titles.noteNext = event.note;
|
||||
this.nextEventId = event.id;
|
||||
this.loaded.nextEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'next-public':
|
||||
@@ -415,7 +410,7 @@ export class EventLoader {
|
||||
this.titlesPublic.subtitleNext = event.subtitle;
|
||||
this.titlesPublic.presenterNext = event.presenter;
|
||||
this.titlesPublic.noteNext = event.note;
|
||||
this.nextPublicEventId = event.id;
|
||||
this.loaded.nextPublicEventId = event.id;
|
||||
break;
|
||||
|
||||
case 'next-private':
|
||||
@@ -423,7 +418,7 @@ export class EventLoader {
|
||||
this.titles.subtitleNext = event.subtitle;
|
||||
this.titles.presenterNext = event.presenter;
|
||||
this.titles.noteNext = event.note;
|
||||
this.nextEventId = event.id;
|
||||
this.loaded.nextEventId = event.id;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
import { Server } from 'socket.io';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
import getRandomName from '../../utils/getRandomName.js';
|
||||
import { stringFromMillis } from '../../utils/time.js';
|
||||
import { messageManager } from '../message-manager/MessageManager.js';
|
||||
import { PlaybackService } from '../../services/PlaybackService.js';
|
||||
|
||||
import { eventTimer } from '../../services/TimerService.js';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
import { clock } from '../../services/Clock.js';
|
||||
|
||||
class SocketController {
|
||||
constructor() {
|
||||
this.numClients = 0;
|
||||
this.messageStack = [];
|
||||
this._MAX_MESSAGES = 100;
|
||||
this._clientNames = {};
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
initServer(httpServer) {
|
||||
this.socket = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
startListener() {
|
||||
this._socketMessageHandler();
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this.info('SERVER', 'Shutting down ontime');
|
||||
if (this.socket) {
|
||||
this.info('TX', '... Closing socket server');
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle socket io connections
|
||||
* @private
|
||||
*/
|
||||
_socketMessageHandler() {
|
||||
this.socket.on('connection', (socket) => {
|
||||
/*******************************/
|
||||
/*** HANDLE NEW CONNECTION ***/
|
||||
/*** --------------------- ***/
|
||||
/*******************************/
|
||||
// keep track of connections
|
||||
this.numClients++;
|
||||
this._clientNames[socket.id] = getRandomName();
|
||||
const message = `${this.numClients} Clients with new connection: ${this._clientNames[socket.id]}`;
|
||||
this.info('CLIENT', message);
|
||||
|
||||
// Todo: review in favour of features
|
||||
// send state
|
||||
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
|
||||
*/
|
||||
socket.on('disconnect', () => {
|
||||
this.numClients--;
|
||||
const message = `${this.numClients} Clients with disconnection: ${this._clientNames[socket.id]}`;
|
||||
delete this._clientNames[socket.id];
|
||||
this.info('CLIENT', message);
|
||||
});
|
||||
|
||||
/**
|
||||
* @description utility for renaming a user
|
||||
*/
|
||||
socket.on('rename-client', (newName) => {
|
||||
if (newName) {
|
||||
const previousName = this._clientNames[socket.id];
|
||||
this._clientNames[socket.id] = newName;
|
||||
this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
|
||||
}
|
||||
});
|
||||
|
||||
/***************************************/
|
||||
/*** TIMER STATE GETTERS / SETTERS ***/
|
||||
/*** ------- WEBSOCKET API ------- ***/
|
||||
/*** ----------------------------- ***/
|
||||
/***************************************/
|
||||
|
||||
/*******************************************/
|
||||
socket.on('ontime-test', () => {
|
||||
socket.emit('hello', socket.id);
|
||||
});
|
||||
|
||||
socket.on('set-start', () => {
|
||||
PlaybackService.start();
|
||||
});
|
||||
|
||||
socket.on('set-startid', (data) => {
|
||||
if (data) {
|
||||
PlaybackService.startById(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('set-startindex', (data) => {
|
||||
const eventIndex = Number(data);
|
||||
if (!isNaN(eventIndex)) {
|
||||
PlaybackService.startByIndex(eventIndex);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('set-loadid', (data) => {
|
||||
if (data) {
|
||||
PlaybackService.loadById(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('set-loadindex', (data) => {
|
||||
const eventIndex = Number(data);
|
||||
if (!isNaN(eventIndex)) {
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('set-pause', () => {
|
||||
PlaybackService.pause();
|
||||
});
|
||||
|
||||
socket.on('set-stop', () => {
|
||||
PlaybackService.stop();
|
||||
});
|
||||
|
||||
socket.on('set-reload', () => {
|
||||
PlaybackService.reload();
|
||||
});
|
||||
|
||||
socket.on('set-previous', () => {
|
||||
PlaybackService.loadPrevious();
|
||||
});
|
||||
|
||||
socket.on('set-next', () => {
|
||||
PlaybackService.loadNext();
|
||||
});
|
||||
|
||||
socket.on('set-roll', () => {
|
||||
PlaybackService.roll();
|
||||
});
|
||||
|
||||
socket.on('set-delay', (data) => {
|
||||
const delayTime = Number(data);
|
||||
if (!isNaN(delayTime)) {
|
||||
PlaybackService.setDelay(delayTime);
|
||||
}
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// general playback state, useful for external sync
|
||||
// Todo: add delayed value (will come from rundownService)
|
||||
socket.on('ontime-poll', () => {
|
||||
const timerPoll = eventTimer.timer;
|
||||
const isDelayed = false;
|
||||
const colour = '';
|
||||
socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll });
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
socket.on('get-playback', () => {
|
||||
socket.emit('playback', eventTimer.playback);
|
||||
});
|
||||
|
||||
socket.on('get-onAir', () => {
|
||||
socket.emit('onAir', messageManager.onAir);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
socket.on('get-selected', () => {
|
||||
socket.emit('selected', {
|
||||
id: eventLoader.selectedEventId,
|
||||
index: eventLoader.selectedEventIndex,
|
||||
total: eventLoader.numEvents,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('get-titles', () => {
|
||||
socket.emit('titles', eventLoader.titles);
|
||||
});
|
||||
|
||||
socket.on('get-publictitles', () => {
|
||||
socket.emit('publictitles', eventLoader.titlesPublic);
|
||||
});
|
||||
|
||||
/***********************************/
|
||||
/*** MESSAGE GETTERS / SETTERS ***/
|
||||
/*** ------------------------- ***/
|
||||
/***********************************/
|
||||
|
||||
// On Air
|
||||
socket.on('set-onAir', (data) => {
|
||||
if (typeof data === 'boolean') {
|
||||
try {
|
||||
const featureData = messageManager.setOnAir(data);
|
||||
this.info('PLAYBACK', featureData.onAir ? 'Going On Air' : 'Going Off Air');
|
||||
} catch (error) {
|
||||
this.error('RX', `Failed to parse message ${data} : ${error}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Presenter message
|
||||
socket.on('set-timer-message-text', (data) => {
|
||||
if (typeof data !== 'string') {
|
||||
return;
|
||||
}
|
||||
messageManager.setTimerText(data);
|
||||
});
|
||||
|
||||
socket.on('set-timer-message-visible', (data) => {
|
||||
if (typeof data !== 'boolean') {
|
||||
return;
|
||||
}
|
||||
messageManager.setTimerVisibility(data);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// Public message
|
||||
socket.on('set-public-message-text', (data) => {
|
||||
if (typeof data !== 'string') {
|
||||
return;
|
||||
}
|
||||
messageManager.setPublicText(data);
|
||||
});
|
||||
|
||||
socket.on('set-public-message-visible', (data) => {
|
||||
if (typeof data !== 'boolean') {
|
||||
return;
|
||||
}
|
||||
messageManager.setPublicVisibility(data);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// Lower third message
|
||||
socket.on('set-lower-message-text', (data) => {
|
||||
if (typeof data !== 'string') {
|
||||
return;
|
||||
}
|
||||
messageManager.setLowerText(data);
|
||||
});
|
||||
|
||||
socket.on('set-lower-message-visible', (data) => {
|
||||
if (typeof data !== 'boolean') {
|
||||
return;
|
||||
}
|
||||
messageManager.setLowerVisibility(data);
|
||||
});
|
||||
|
||||
/* MOLECULAR ENDPOINTS
|
||||
* =====================
|
||||
* 1. RUNDOWN
|
||||
* 2. MESSAGE CONTROL
|
||||
* 3. PLAYBACK CONTROL
|
||||
* 4. INFO
|
||||
* 5. CUE SHEET
|
||||
* 6. TIMER OBJECT
|
||||
* */
|
||||
|
||||
// 1. RUNDOWN
|
||||
socket.on('get-feat-rundown', () => {
|
||||
this.broadcastFeatureRundown();
|
||||
});
|
||||
|
||||
// 2. MESSAGE CONTROL
|
||||
socket.on('get-feat-messagecontrol', () => {
|
||||
this.broadcastFeatureMessageControl();
|
||||
});
|
||||
|
||||
// 3. PLAYBACK CONTROL
|
||||
socket.on('get-feat-playbackcontrol', () => {
|
||||
this.broadcastFeaturePlaybackControl();
|
||||
});
|
||||
|
||||
// 4. INFO
|
||||
socket.on('get-feat-info', () => {
|
||||
this.broadcastFeatureInfo();
|
||||
});
|
||||
|
||||
// 5. CUE SHEET
|
||||
socket.on('get-feat-cuesheet', () => {
|
||||
this.broadcastFeatureCuesheet();
|
||||
});
|
||||
|
||||
// 6. TIMER
|
||||
socket.on('get-timer', () => {
|
||||
// TODO: Not ideal workaround
|
||||
socket.emit('timer', eventTimer.timer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
send(topic, payload) {
|
||||
this.socket?.emit(topic, payload);
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
|
||||
/**
|
||||
* Logger logic
|
||||
* -------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility method, sends message and pushes into stack
|
||||
* @param {string} level
|
||||
* @param {string} origin
|
||||
* @param {string} text
|
||||
*/
|
||||
_push(level, origin, text) {
|
||||
const logMessage = {
|
||||
id: generateId(),
|
||||
level,
|
||||
origin,
|
||||
text,
|
||||
time: stringFromMillis(clock.getSystemTime() || 0),
|
||||
};
|
||||
|
||||
this.messageStack.unshift(logMessage);
|
||||
this.socket?.emit('logger', logMessage);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(`[${logMessage.level}] \t ${logMessage.origin} \t ${logMessage.text}`);
|
||||
}
|
||||
|
||||
if (this.messageStack.length > this._MAX_MESSAGES) {
|
||||
this.messageStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('feat-messagecontrol', 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);
|
||||
}
|
||||
|
||||
// TODO: ouch, services should update the store
|
||||
// make middleware to maintain the features OR remove the feature endpoints
|
||||
broadcastState() {
|
||||
this.broadcastFeatureRundown();
|
||||
this.broadcastFeatureMessageControl();
|
||||
this.broadcastFeaturePlaybackControl();
|
||||
this.broadcastFeatureInfo();
|
||||
this.broadcastFeatureCuesheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with level LOG
|
||||
* @param {string} origin
|
||||
* @param {string} text
|
||||
*/
|
||||
info(origin, text) {
|
||||
this._push('INFO', origin, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with level WARN
|
||||
* @param {string} origin
|
||||
* @param {string} text
|
||||
*/
|
||||
warning(origin, text) {
|
||||
this._push('WARN', origin, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with level ERROR
|
||||
* @param {string} origin
|
||||
* @param {string} text
|
||||
*/
|
||||
error(origin, text) {
|
||||
this._push('ERROR', origin, text);
|
||||
}
|
||||
}
|
||||
|
||||
export const socketProvider = new SocketController();
|
||||
@@ -1,159 +0,0 @@
|
||||
import { Server } from 'node-osc';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
let oscServer = null;
|
||||
|
||||
/**
|
||||
* @description utility function to shut down osc server
|
||||
*/
|
||||
export const shutdownOSCServer = () => {
|
||||
if (oscServer != null) oscServer.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialises OSC server
|
||||
*/
|
||||
export const initiateOSC = (config: OSCSettings) => {
|
||||
oscServer = new Server(config.portIn, '0.0.0.0');
|
||||
|
||||
oscServer.on('error', console.error);
|
||||
|
||||
oscServer.on('message', function (msg) {
|
||||
// message should look like /ontime/{path} {args} where
|
||||
// ontime: fixed message for app
|
||||
// path: command to be called
|
||||
// args: extra data, only used on some API entries (delay, goto)
|
||||
|
||||
// split message
|
||||
const [, address, path] = msg[0].split('/');
|
||||
const args = msg[1];
|
||||
|
||||
// get first part before (ontime)
|
||||
if (address !== 'ontime') {
|
||||
console.error('RX', `OSC IN: Message address ${address} not recognised`, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// get second part (command)
|
||||
if (!path) {
|
||||
console.error('RX', 'OSC IN: No path found');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (path.toLowerCase()) {
|
||||
case 'onair': {
|
||||
messageManager.setOnAir(true);
|
||||
break;
|
||||
}
|
||||
case 'offair': {
|
||||
messageManager.setOnAir(false);
|
||||
break;
|
||||
}
|
||||
case 'play': {
|
||||
PlaybackService.start();
|
||||
break;
|
||||
}
|
||||
case 'start': {
|
||||
try {
|
||||
const eventIndex = Number(args);
|
||||
if (isNaN(eventIndex)) {
|
||||
socketProvider.error('RX', `OSC IN: event index not recognised ${args}`);
|
||||
return;
|
||||
}
|
||||
PlaybackService.startByIndex(eventIndex);
|
||||
} catch (error) {
|
||||
console.log('Error loading event: ', error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'startid': {
|
||||
if (!args) {
|
||||
socketProvider.error('RX', `OSC IN: No ID in request`);
|
||||
return;
|
||||
}
|
||||
PlaybackService.loadById(args);
|
||||
break;
|
||||
}
|
||||
case 'pause': {
|
||||
PlaybackService.pause();
|
||||
break;
|
||||
}
|
||||
case 'prev': {
|
||||
PlaybackService.loadPrevious();
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
PlaybackService.loadNext();
|
||||
break;
|
||||
}
|
||||
case 'unload':
|
||||
case 'stop': {
|
||||
PlaybackService.stop();
|
||||
break;
|
||||
}
|
||||
case 'reload': {
|
||||
PlaybackService.reload();
|
||||
break;
|
||||
}
|
||||
case 'roll': {
|
||||
PlaybackService.roll();
|
||||
break;
|
||||
}
|
||||
case 'delay': {
|
||||
try {
|
||||
const delayTime = Number(args);
|
||||
if (isNaN(delayTime)) {
|
||||
socketProvider.error('RX', `OSC IN: delay time not recognised ${args}`);
|
||||
return;
|
||||
}
|
||||
PlaybackService.setDelay(delayTime);
|
||||
} catch (error) {
|
||||
console.log('Error adding delay: ', error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'goto':
|
||||
case 'load': {
|
||||
try {
|
||||
const eventIndex = Number(args);
|
||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||
socketProvider.error('RX', `OSC IN: event index not recognised or out of range ${eventIndex}`);
|
||||
} else {
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
}
|
||||
} catch (error) {
|
||||
socketProvider.error('RX', `OSC IN: error calling goto ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gotoid':
|
||||
case 'loadid': {
|
||||
if (!args) {
|
||||
socketProvider.error('RX', `OSC IN: event ID not recognised: ${args}}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
PlaybackService.loadById(args.toString().toLowerCase());
|
||||
} catch (error) {
|
||||
socketProvider.error('RX', `OSC IN: error calling goto ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get-playback': {
|
||||
const playback = global.timer.state;
|
||||
global.timer.sendOsc('playback', playback);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
socketProvider.warning('RX', `OSC IN: unhandled message ${path}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import { messageService } from '../services/message-service/MessageService.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
|
||||
export function dispatchFromAdapter(type: string, payload: unknown, source?: 'osc' | 'ws') {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'test-ontime': {
|
||||
return { topic: 'hello' };
|
||||
}
|
||||
|
||||
case 'ontime-poll': {
|
||||
return {
|
||||
topic: 'poll',
|
||||
payload: eventStore.poll(),
|
||||
};
|
||||
}
|
||||
|
||||
case 'set-onair': {
|
||||
if (typeof payload !== 'undefined') {
|
||||
messageService.setOnAir(Boolean(payload));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'onair': {
|
||||
messageService.setOnAir(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'offair': {
|
||||
messageService.setOnAir(false);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'set-timer-message-text': {
|
||||
if (typeof payload !== 'string') {
|
||||
throw new Error(`Unable to parse payload: ${payload}`);
|
||||
}
|
||||
messageService.setTimerText(payload);
|
||||
break;
|
||||
}
|
||||
case 'set-timer-message-visible': {
|
||||
if (typeof payload === 'undefined') {
|
||||
throw new Error(`Unable to parse payload: ${payload}`);
|
||||
}
|
||||
messageService.setTimerVisibility(Boolean(payload));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'set-public-message-text': {
|
||||
if (typeof payload !== 'string') {
|
||||
throw new Error(`Unable to parse payload: ${payload}`);
|
||||
}
|
||||
messageService.setPublicText(payload);
|
||||
break;
|
||||
}
|
||||
case 'set-public-message-visible': {
|
||||
if (typeof payload === 'undefined') {
|
||||
throw new Error(`Unable to parse payload: ${payload}`);
|
||||
}
|
||||
messageService.setPublicVisibility(Boolean(payload));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'set-lower-message-text': {
|
||||
if (typeof payload !== 'string') {
|
||||
throw new Error(`Unable to parse payload: ${payload}`);
|
||||
}
|
||||
messageService.setLowerText(payload);
|
||||
break;
|
||||
}
|
||||
case 'set-lower-message-visible': {
|
||||
if (typeof payload === 'undefined') {
|
||||
throw new Error(`Unable to parse payload: ${payload}`);
|
||||
}
|
||||
messageService.setLowerVisibility(Boolean(payload));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'start': {
|
||||
PlaybackService.start();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'startindex': {
|
||||
const eventIndex = Number(payload);
|
||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Indexes in frontend are 1 based
|
||||
PlaybackService.startByIndex(eventIndex - 1);
|
||||
} catch (error) {
|
||||
throw new Error(`Error loading event:: ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'startid': {
|
||||
if (!payload) {
|
||||
throw new Error(`Event ID not recognised: ${payload}`);
|
||||
}
|
||||
PlaybackService.startById(payload);
|
||||
break;
|
||||
}
|
||||
case 'pause': {
|
||||
PlaybackService.pause();
|
||||
break;
|
||||
}
|
||||
case 'previous': {
|
||||
PlaybackService.loadPrevious();
|
||||
break;
|
||||
}
|
||||
case 'next': {
|
||||
PlaybackService.loadNext();
|
||||
break;
|
||||
}
|
||||
case 'unload':
|
||||
case 'stop': {
|
||||
PlaybackService.stop();
|
||||
break;
|
||||
}
|
||||
case 'reload': {
|
||||
PlaybackService.reload();
|
||||
break;
|
||||
}
|
||||
case 'roll': {
|
||||
PlaybackService.roll();
|
||||
break;
|
||||
}
|
||||
case 'delay': {
|
||||
const delayTime = Number(payload);
|
||||
if (isNaN(delayTime)) {
|
||||
throw new Error(`Delay time not recognised ${payload}`);
|
||||
}
|
||||
|
||||
try {
|
||||
PlaybackService.setDelay(delayTime);
|
||||
} catch (error) {
|
||||
throw new Error(`Could not add delay: ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gotoindex':
|
||||
case 'loadindex': {
|
||||
const eventIndex = Number(payload);
|
||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Indexes in frontend are 1 based
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
} catch (error) {
|
||||
throw new Error(`Event index not recognised or out of range ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gotoid':
|
||||
case 'loadid': {
|
||||
if (!payload) {
|
||||
throw new Error(`Event ID not recognised: ${payload}`);
|
||||
}
|
||||
|
||||
try {
|
||||
PlaybackService.loadById(payload.toString().toLowerCase());
|
||||
} catch (error) {
|
||||
throw new Error(`OSC IN: error calling goto ${error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get-playback': {
|
||||
const playback = eventStore.get('playback');
|
||||
return { topic: 'playback', payload: playback };
|
||||
}
|
||||
|
||||
case 'get-timer': {
|
||||
const timer = eventStore.get('timer');
|
||||
return { topic: 'timer', payload: timer };
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unhandled message ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
// Create controller for GET request to '/playback'
|
||||
// Returns ACK message
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
|
||||
// Create controller for POST request to '/playback'
|
||||
// Returns playback state
|
||||
export const pbGet = async (req, res) => {
|
||||
res.send({ playback: global.timer.state });
|
||||
res.send({ playback: eventStore.get('playback') });
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/playback/start'
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as Sentry from '@sentry/node';
|
||||
|
||||
let shouldReport;
|
||||
|
||||
export function initSentry(environment) {
|
||||
shouldReport = environment === 'production';
|
||||
export function initSentry(doReport) {
|
||||
shouldReport = doReport;
|
||||
Sentry.init({
|
||||
dsn: 'https://ceb6abdce7374857bb50b65636cbaed1@o4504288369836032.ingest.sentry.io/4504288555565056',
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
+24
-25
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* starts loaded timer
|
||||
*/
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { Playback } from 'ontime-types';
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* Service manages playback status of app
|
||||
@@ -20,9 +19,9 @@ export class PlaybackService {
|
||||
static loadEvent(event) {
|
||||
let success = false;
|
||||
if (!event) {
|
||||
socketProvider.error('PLAYBACK', 'No event found');
|
||||
logger.error('PLAYBACK', 'No event found');
|
||||
} else if (event.skip) {
|
||||
socketProvider.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
||||
logger.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
||||
} else {
|
||||
eventLoader.loadEvent(event);
|
||||
eventTimer.load(event);
|
||||
@@ -41,7 +40,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
PlaybackService.start();
|
||||
}
|
||||
return success;
|
||||
@@ -56,7 +55,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
PlaybackService.start();
|
||||
}
|
||||
return success;
|
||||
@@ -71,7 +70,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventWithId(eventId);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -85,7 +84,7 @@ export class PlaybackService {
|
||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||
const success = PlaybackService.loadEvent(event);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -98,7 +97,7 @@ export class PlaybackService {
|
||||
if (previousEvent) {
|
||||
const success = PlaybackService.loadEvent(previousEvent);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`);
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +110,7 @@ export class PlaybackService {
|
||||
if (nextEvent) {
|
||||
const success = PlaybackService.loadEvent(nextEvent);
|
||||
if (success) {
|
||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
|
||||
logger.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,10 +119,10 @@ export class PlaybackService {
|
||||
* Starts playback on selected event
|
||||
*/
|
||||
static start() {
|
||||
if (eventLoader.selectedEventId) {
|
||||
if (eventTimer.playback === Playback.Armed || eventTimer.playback === Playback.Pause) {
|
||||
eventTimer.start();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +130,10 @@ export class PlaybackService {
|
||||
* Pauses playback on selected event
|
||||
*/
|
||||
static pause() {
|
||||
if (eventLoader.selectedEventId) {
|
||||
if (eventTimer.playback === Playback.Play) {
|
||||
eventTimer.pause();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,11 +141,11 @@ export class PlaybackService {
|
||||
* Stops timer and unloads any events
|
||||
*/
|
||||
static stop() {
|
||||
if (eventLoader.selectedEventId || eventTimer.playback === 'roll') {
|
||||
if (eventTimer.playback !== Playback.Stop) {
|
||||
eventLoader.reset();
|
||||
eventTimer.stop();
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +153,8 @@ export class PlaybackService {
|
||||
* Reloads current event
|
||||
*/
|
||||
static reload() {
|
||||
if (eventLoader.selectedEventId) {
|
||||
this.loadById(eventLoader.selectedEventId);
|
||||
if (eventTimer.loadedTimerId) {
|
||||
this.loadById(eventTimer.loadedTimerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,14 +167,14 @@ export class PlaybackService {
|
||||
|
||||
// nothing to play
|
||||
if (rollTimers === null) {
|
||||
socketProvider.error('SERVER', 'Roll: no events found');
|
||||
logger.warning('SERVER', 'Roll: no events found');
|
||||
PlaybackService.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentEvent, nextEvent, timers } = rollTimers;
|
||||
if (!currentEvent && !nextEvent) {
|
||||
socketProvider.error('SERVER', 'Roll: no events found');
|
||||
logger.warning('SERVER', 'Roll: no events found');
|
||||
PlaybackService.stop();
|
||||
return;
|
||||
}
|
||||
@@ -183,7 +182,7 @@ export class PlaybackService {
|
||||
eventTimer.roll(currentEvent, nextEvent, timers);
|
||||
|
||||
const newState = eventTimer.playback;
|
||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +191,10 @@ export class PlaybackService {
|
||||
* @param {number} delayTime time in minutes
|
||||
*/
|
||||
static setDelay(delayTime) {
|
||||
if (eventLoader.selectedEventId) {
|
||||
if (eventTimer.loadedTimerId) {
|
||||
const delayInMs = delayTime * 1000 * 60;
|
||||
eventTimer.delay(delayInMs);
|
||||
socketProvider.info('PLAYBACK', `Added ${delayTime} min delay`);
|
||||
logger.info('PLAYBACK', `Added ${delayTime} min delay`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,15 @@ import { block as blockDef, delay as delayDef, event as eventDef } from '../mode
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
|
||||
/**
|
||||
* Checks if a list of IDs is in the current selection
|
||||
*/
|
||||
const affectedLoaded = (affectedIds: string[]) => {
|
||||
const now = eventLoader.selectedEventId;
|
||||
const nowPublic = eventLoader.selectedPublicEventId;
|
||||
const next = eventLoader.nextEventId;
|
||||
const nextPublic = eventLoader.nextPublicEventId;
|
||||
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) ||
|
||||
@@ -28,8 +27,8 @@ const affectedLoaded = (affectedIds: string[]) => {
|
||||
*/
|
||||
const isNewNext = () => {
|
||||
const timedEvents = EventLoader.getTimedEvents();
|
||||
const now = eventLoader.selectedEventId;
|
||||
const next = eventLoader.nextEventId;
|
||||
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);
|
||||
@@ -39,8 +38,8 @@ const isNewNext = () => {
|
||||
return true;
|
||||
}
|
||||
// iterate through timed events and see if there are public events between nowPublic and nextPublic
|
||||
const nowPublic = eventLoader.selectedPublicEventId;
|
||||
const nextPublic = eventLoader.nextPublicEventId;
|
||||
const nowPublic = eventLoader.loaded.selectedPublicEventId;
|
||||
const nextPublic = eventLoader.loaded.nextPublicEventId;
|
||||
|
||||
let foundNew = false;
|
||||
let isAfter = false;
|
||||
@@ -67,7 +66,7 @@ const isNewNext = () => {
|
||||
* Updates timer object
|
||||
*/
|
||||
export function updateTimer(affectedIds?: string[]) {
|
||||
const runningEventId = eventLoader.selectedEventId;
|
||||
const runningEventId = eventLoader.loaded.selectedEventId;
|
||||
|
||||
if (runningEventId === null) {
|
||||
return false;
|
||||
@@ -112,7 +111,7 @@ export function updateTimer(affectedIds?: string[]) {
|
||||
* @param {object} eventData
|
||||
* @return {unknown[]}
|
||||
*/
|
||||
export async function addEvent(eventData) {
|
||||
export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
||||
const numEvents = DataProvider.getRundownLength();
|
||||
if (numEvents > MAX_EVENTS) {
|
||||
throw new Error(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||
@@ -145,7 +144,7 @@ export async function addEvent(eventData) {
|
||||
throw new Error(error);
|
||||
}
|
||||
updateTimer([id]);
|
||||
eventStore.broadcast();
|
||||
updateChangeNumEvents();
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -157,7 +156,6 @@ export async function editEvent(eventData) {
|
||||
}
|
||||
const newEvent = await DataProvider.updateEventById(eventId, eventData);
|
||||
updateTimer([eventId]);
|
||||
eventStore.broadcast();
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -169,7 +167,7 @@ export async function editEvent(eventData) {
|
||||
export async function deleteEvent(eventId) {
|
||||
await DataProvider.deleteEvent(eventId);
|
||||
updateTimer([eventId]);
|
||||
eventStore.broadcast();
|
||||
updateChangeNumEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +177,7 @@ export async function deleteEvent(eventId) {
|
||||
export async function deleteAllEvents() {
|
||||
await DataProvider.clearRundown();
|
||||
updateTimer();
|
||||
eventStore.broadcast();
|
||||
updateChangeNumEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +202,6 @@ export async function reorderEvent(eventId, from, to) {
|
||||
// save rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
updateTimer();
|
||||
|
||||
return reorderedItem;
|
||||
}
|
||||
|
||||
@@ -260,5 +257,12 @@ export async function applyDelay(eventId) {
|
||||
// update rundown
|
||||
await DataProvider.setRundown(rundown);
|
||||
updateTimer();
|
||||
eventStore.broadcast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces update in the store
|
||||
* Called when we make changes to the rundown object
|
||||
*/
|
||||
function updateChangeNumEvents() {
|
||||
eventLoader.updateNumEvents();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TimerLifeCycle, TimerType } from 'ontime-types';
|
||||
import { Playback, TimerLifeCycle, TimerState } from 'ontime-types';
|
||||
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { PlaybackService } from './PlaybackService.js';
|
||||
@@ -11,27 +11,14 @@ import { clock } from './Clock.js';
|
||||
export class TimerService {
|
||||
private readonly _interval: NodeJS.Timer;
|
||||
|
||||
playback: string;
|
||||
playback: Playback;
|
||||
timer: TimerState;
|
||||
|
||||
loadedTimerId: null;
|
||||
private pausedTime: number;
|
||||
private pausedAt: number | null;
|
||||
private secondaryTarget: number | null;
|
||||
|
||||
timer: {
|
||||
clock: number; // realtime clock
|
||||
current: number | null; // running countdown
|
||||
elapsed: number | null; // elapsed time in current timer
|
||||
expectedFinish: number | null;
|
||||
addedTime: number; // time added by user, can be negative
|
||||
startedAt: number | null;
|
||||
finishedAt: number | null; // only if timer has already finished
|
||||
secondaryTimer: number | null; // used for roll mode
|
||||
selectedEventId: string | null;
|
||||
duration: number | null;
|
||||
timerType: TimerType | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} [timerConfig]
|
||||
@@ -47,7 +34,7 @@ export class TimerService {
|
||||
* @private
|
||||
*/
|
||||
_clear() {
|
||||
this.playback = 'stop';
|
||||
this.playback = Playback.Stop;
|
||||
this.timer = {
|
||||
clock: clock.timeNow(),
|
||||
current: null,
|
||||
@@ -128,7 +115,7 @@ export class TimerService {
|
||||
this.loadedTimerId = timer.id;
|
||||
this.timer.duration = timer.duration;
|
||||
this.timer.current = timer.duration;
|
||||
this.playback = 'armed';
|
||||
this.playback = Playback.Armed;
|
||||
this.timer.timerType = timer.timerType;
|
||||
this.pausedTime = 0;
|
||||
this.pausedAt = 0;
|
||||
@@ -151,7 +138,7 @@ export class TimerService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.playback === 'play') {
|
||||
if (this.playback === Playback.Play) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,7 +153,7 @@ export class TimerService {
|
||||
this.timer.startedAt = this.timer.clock;
|
||||
}
|
||||
|
||||
this.playback = 'play';
|
||||
this.playback = Playback.Play;
|
||||
this.timer.expectedFinish = getExpectedFinish(
|
||||
this.timer.startedAt,
|
||||
this.timer.finishedAt,
|
||||
@@ -188,11 +175,11 @@ export class TimerService {
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.playback !== 'play') {
|
||||
if (this.playback !== Playback.Play) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playback = 'pause';
|
||||
this.playback = Playback.Pause;
|
||||
this.timer.clock = clock.timeNow();
|
||||
this.pausedAt = this.timer.clock;
|
||||
this._onPause();
|
||||
@@ -205,7 +192,7 @@ export class TimerService {
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.playback === 'stop') {
|
||||
if (this.playback === Playback.Stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -248,7 +235,7 @@ export class TimerService {
|
||||
update() {
|
||||
this.timer.clock = clock.timeNow();
|
||||
|
||||
if (this.playback === 'roll') {
|
||||
if (this.playback === Playback.Roll) {
|
||||
const tempCurrentTimer = {
|
||||
selectedEventId: this.loadedTimerId,
|
||||
current: this.timer.current,
|
||||
@@ -279,11 +266,11 @@ export class TimerService {
|
||||
} else {
|
||||
// we only update timer if a timer has been started
|
||||
if (this.timer.startedAt !== null) {
|
||||
if (this.playback === 'pause') {
|
||||
if (this.playback === Playback.Pause) {
|
||||
this.pausedTime = this.timer.clock - this.pausedAt;
|
||||
}
|
||||
|
||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
if (this.playback === Playback.Play && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
this.timer.finishedAt = this.timer.clock;
|
||||
this._onFinish();
|
||||
} else {
|
||||
@@ -338,7 +325,7 @@ export class TimerService {
|
||||
this.secondaryTarget = nextEvent.timeStart;
|
||||
}
|
||||
|
||||
this.playback = 'roll';
|
||||
this.playback = Playback.Roll;
|
||||
this._onRoll();
|
||||
this.update();
|
||||
}
|
||||
|
||||
+42
-32
@@ -1,11 +1,13 @@
|
||||
import { MessageControl } from 'ontime-types';
|
||||
import { Message } from 'ontime-types';
|
||||
|
||||
import { eventStore } from '../../stores/EventStore.js';
|
||||
|
||||
let instance;
|
||||
|
||||
class MessageService {
|
||||
messages: MessageControl;
|
||||
timerMessage: Message;
|
||||
publicMessage: Message;
|
||||
lowerMessage: Message;
|
||||
onAir: boolean;
|
||||
|
||||
constructor() {
|
||||
@@ -16,20 +18,21 @@ class MessageService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||
instance = this;
|
||||
|
||||
this.messages = {
|
||||
presenter: {
|
||||
text: '',
|
||||
visible: false,
|
||||
},
|
||||
public: {
|
||||
text: '',
|
||||
visible: false,
|
||||
},
|
||||
lower: {
|
||||
text: '',
|
||||
visible: false,
|
||||
},
|
||||
this.timerMessage = {
|
||||
text: '',
|
||||
visible: false,
|
||||
};
|
||||
|
||||
this.publicMessage = {
|
||||
text: '',
|
||||
visible: false,
|
||||
};
|
||||
|
||||
this.lowerMessage = {
|
||||
text: '',
|
||||
visible: false,
|
||||
};
|
||||
|
||||
this.onAir = false;
|
||||
}
|
||||
|
||||
@@ -37,8 +40,8 @@ class MessageService {
|
||||
* @description sets message on stage timer screen
|
||||
*/
|
||||
setTimerText(payload: string) {
|
||||
this.messages.presenter.text = payload;
|
||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
||||
this.timerMessage.text = payload;
|
||||
eventStore.set('timerMessage', this.timerMessage);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
@@ -46,8 +49,8 @@ class MessageService {
|
||||
* @description sets message visibility on stage timer screen
|
||||
*/
|
||||
setTimerVisibility(status: boolean) {
|
||||
this.messages.presenter.visible = status;
|
||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
||||
this.timerMessage.visible = status;
|
||||
eventStore.set('timerMessage', this.timerMessage);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
@@ -55,8 +58,8 @@ class MessageService {
|
||||
* @description sets message on public screen
|
||||
*/
|
||||
setPublicText(payload: string) {
|
||||
this.messages.public.text = payload;
|
||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
||||
this.publicMessage.text = payload;
|
||||
eventStore.set('publicMessage', this.publicMessage);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
@@ -64,8 +67,8 @@ class MessageService {
|
||||
* @description sets message visibility on public screen
|
||||
*/
|
||||
setPublicVisibility(status: boolean) {
|
||||
this.messages.public.visible = status;
|
||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
||||
this.publicMessage.visible = status;
|
||||
eventStore.set('publicMessage', this.publicMessage);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
@@ -73,8 +76,8 @@ class MessageService {
|
||||
* @description sets message on lower third screen
|
||||
*/
|
||||
setLowerText(payload: string) {
|
||||
this.messages.lower.text = payload;
|
||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
||||
this.lowerMessage.text = payload;
|
||||
eventStore.set('lowerMessage', this.lowerMessage);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
@@ -82,16 +85,21 @@ class MessageService {
|
||||
* @description sets message visibility on lower third screen
|
||||
*/
|
||||
setLowerVisibility(status: boolean) {
|
||||
this.messages.lower.visible = status;
|
||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
||||
this.lowerMessage.visible = status;
|
||||
eventStore.set('lowerMessage', this.lowerMessage);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description set state of onAir
|
||||
* @description set state of onAir, toggles if parameters are offered
|
||||
*/
|
||||
setOnAir(status: boolean) {
|
||||
this.onAir = status;
|
||||
setOnAir(status?: boolean) {
|
||||
if (typeof status === 'undefined') {
|
||||
this.onAir = !this.onAir;
|
||||
} else {
|
||||
this.onAir = status;
|
||||
}
|
||||
eventStore.set('onAir', this.onAir);
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
@@ -100,10 +108,12 @@ class MessageService {
|
||||
*/
|
||||
getAll() {
|
||||
return {
|
||||
messages: this.messages,
|
||||
timerMessage: this.timerMessage,
|
||||
publicMessage: this.publicMessage,
|
||||
lowerMessage: this.lowerMessage,
|
||||
onAir: this.onAir,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const messageManager = new MessageService();
|
||||
export const messageService = new MessageService();
|
||||
@@ -1,23 +0,0 @@
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
const store = {};
|
||||
|
||||
/**
|
||||
* A runtime store that broadcasts its payload
|
||||
*/
|
||||
export const eventStore = {
|
||||
get(key) {
|
||||
return store[key];
|
||||
},
|
||||
set(key, value) {
|
||||
store[key] = value;
|
||||
socketProvider.send(key, value);
|
||||
},
|
||||
poll() {
|
||||
return store;
|
||||
},
|
||||
broadcast() {
|
||||
socketProvider.send(store);
|
||||
socketProvider.broadcastState();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { RuntimeStore } from 'ontime-types';
|
||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
|
||||
const store: Partial<RuntimeStore> = {};
|
||||
|
||||
/**
|
||||
* A runtime store that broadcasts its payload
|
||||
*/
|
||||
export const eventStore = {
|
||||
get<T extends keyof RuntimeStore>(key: T) {
|
||||
return store[key];
|
||||
},
|
||||
set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) {
|
||||
store[key] = value;
|
||||
// TODO: Partial updates seems to cause issues on the client
|
||||
// socket.send({
|
||||
// type: `ontime-${key}`,
|
||||
// payload: value,
|
||||
// });
|
||||
this.broadcast();
|
||||
},
|
||||
poll() {
|
||||
return store;
|
||||
},
|
||||
broadcast() {
|
||||
socket.send({
|
||||
type: 'ontime',
|
||||
payload: store,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -26,37 +26,6 @@ export const isTimeString = (string) => {
|
||||
return regex.test(string);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to string representing time
|
||||
* @param {number} ms - time in milliseconds
|
||||
* @param {boolean} showSeconds - weather to show the seconds
|
||||
* @param {string} delim - character between HH MM SS
|
||||
* @param {string} ifNull - what to return if value is null
|
||||
* @returns {string} String representing time 00:12:02
|
||||
*/
|
||||
|
||||
export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
|
||||
if (ms == null || isNaN(ms)) return ifNull;
|
||||
const isNegative = ms < 0 ? '-' : '';
|
||||
const millis = Math.abs(ms);
|
||||
|
||||
/**
|
||||
* @description ensures value is double digit
|
||||
* @param value
|
||||
* @return {string|*}
|
||||
*/
|
||||
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
|
||||
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
|
||||
const minutes = showWith0(Math.floor((millis / mtm) % 60));
|
||||
const seconds = showWith0(Math.floor((millis / mts) % 60));
|
||||
|
||||
return showSeconds
|
||||
? `${isNegative}${
|
||||
parseInt(hours, 10) ? `${hours}${delim}` : `00${delim}`
|
||||
}${minutes}${delim}${seconds}`
|
||||
: `${isNegative}${parseInt(hours, 10) ? `${hours}` : '00'}${delim}${minutes}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts an excel date to milliseconds
|
||||
* @argument {string} date - excel string date
|
||||
|
||||
Reference in New Issue
Block a user