Files
ontime/server/src/classes/EventTimer.js
T
Carlos Valente 2fa397548a Feat/navigate (#81)
* feat/navigate upgrade react router
* feat/navigate migrate to react router 6
* feat/navigate style modal
* feat/navigate create endpoints for app settings
* feat/navigate protect editor with pin
* feat/navigate apply dynamic routing draft
* feat/navigate upgrade relevant packages
* feat/navigate restructure directory and add tests
* feat/navigate test aliases validation
* feat/navigate validate aliases before sending
* feat/navigate create data endpoint
* feat/navigate config: prettier
* feat/navigate invalidate empty strings
* feat/navigate create endpoints
* feat/navigate parse on import
* feat/navigate navigate to alias
* feat/navigate user help and sample data
* feat/navigate refact aliases modal
* feat/navigate refact settings style
* feat/navigate update sample db
* feat/navigate link is relative to hostname
* feat/navigate navigate to first match
* feat/navigate update readme and version bump
* feat/navigate config: create shared module
* feat/navigate config: cheat module install
* feat/navigate fix tests
* feat/navigate run tests in pull request
* Update ontime_cy.yml
2022-01-04 22:12:59 +01:00

1477 lines
38 KiB
JavaScript

import { Timer } from './Timer.js';
import { Server } from 'socket.io';
import {
DAY_TO_MS,
getSelectionByRoll,
replacePlaceholder,
updateRoll,
} from './classUtils.js';
import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../utils/url.js';
import getRandomName from '../utils/getRandomName.js';
import { stringFromMillis } from 'ontime-utils/time.js';
import { generateId } from 'ontime-utils/generate_id.js';
/*
* Class EventTimer adds functions specific to APP
* @extends Timer
*/
export class EventTimer extends Timer {
/**
* Instantiates an event timer object
* @param {object} httpServer
* @param {object} timerConfig
* @param {object} [oscConfig]
* @param {object} [httpConfig]
*/
constructor(httpServer, timerConfig, oscConfig, httpConfig) {
// call super constructor
super();
this.cycleState = {
/* idle: before it is initialised */
idle: 'idle',
/* onLoad: when a new event is loaded */
onLoad: 'onLoad',
/* armed: when a new event is loaded but hasn't started */
armed: 'armed',
onStart: 'onStart',
/* update: every update call cycle (1 x second) */
onUpdate: 'onUpdate',
onPause: 'onPause',
onStop: 'onStop',
onFinish: 'onFinish',
};
this.ontimeCycle = 'idle';
this.prevCycle = null;
// OSC Object
this.osc = null;
// HTTP Client Object
this.http = null;
this._numClients = 0;
this._interval = null;
this.presenter = {
text: '',
visible: false,
};
this.public = {
text: '',
visible: false,
};
this.lower = {
text: '',
visible: false,
};
// call general title reset
this._resetSelection();
this.numEvents = 0;
this._eventlist = null;
this.onAir = false;
// initialise socketIO server
this.messageStack = [];
this.MAX_MESSAGES = 100;
this._clientNames = {};
this.io = new Server(httpServer, {
cors: {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
},
});
// set recurrent emits
this._interval = setInterval(
() => this.runCycle(),
timerConfig?.refresh || 1000
);
// listen to new connections
this._listenToConnections();
if (oscConfig != null) {
this._initOscClient(oscConfig);
}
if (httpConfig != null) {
this._initHTTPClient(httpConfig);
}
}
/**
* @description Shutdown process
*/
shutdown() {
clearInterval(this._interval);
this.info('SERVER', 'Shutting down ontime');
if (this.io != null) {
this.info('TX', '... Closing socket server');
this.io.close();
}
if (this.osc != null) {
this.info('TX', '... Closing OSC Client');
this.osc.shutdown();
}
if (this.http != null) {
this.info('TX', '... Closing HTTP Client');
this.http.shutdown();
}
}
/**
* Initialises OSC Integration object
* @param {object} oscConfig
* @private
*/
_initOscClient(oscConfig) {
this.osc = new OSCIntegration();
const r = this.osc.init(oscConfig);
r.success ? this.info('TX', r.message) : this.error('TX', r.message);
}
/**
* Initialises HTTP Integration object
* @param {object} httpConfig
* @private
*/
_initHTTPClient(httpConfig) {
this.info('TX', `Initialise HTTP Client on port`);
this.http = new HTTPIntegration();
this.http.init(httpConfig);
this.httpMessages = httpConfig.messages;
}
/**
* Sends time object over websockets
*/
broadcastTimer() {
// through websockets
this.io.emit('timer', this.getTimeObject());
}
/**
* Broadcasts complete object state
*/
broadcastState() {
this.broadcastTimer();
this.io.emit('playstate', this.state);
this.io.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
total: this.numEvents,
});
this.io.emit('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId);
this.io.emit('numevents', this.numEvents);
this.io.emit('publicselected-id', this.selectedPublicEventId);
this.io.emit('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles);
this.io.emit('publictitles', this.titlesPublic);
this.io.emit('onAir', this.onAir);
}
/**
* Broadcast given message
* @param {string} address - socket io address
* @param {any} payload - message body
*/
broadcastThis(address, payload) {
this.io.emit(address, payload);
}
/**
* @description Interface for triggering playback actions
* @param {string} action - state to be triggered
* @returns {boolean} Whether action was called
*/
trigger(action) {
// Todo: reply should come from status change
let reply = true;
switch (action) {
case 'start':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Start');
this.start();
break;
case 'pause':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Pause');
this.pause();
break;
case 'stop':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Stop');
this.stop();
break;
case 'roll':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Roll');
this.roll();
break;
case 'previous':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Previous');
this.previous();
break;
case 'next':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Play Mode Next');
this.next();
break;
case 'unload':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Events unloaded');
this.unload();
break;
case 'reload':
if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update
this.info('PLAYBACK', 'Reloaded event');
this.reload();
break;
case 'onAir':
// Call action
this.info('PLAYBACK', 'Going On Air');
this.setonAir(true);
break;
case 'offAir':
// Call action and force update
this.info('PLAYBACK', 'Going Off Air');
this.setonAir(false);
break;
default:
// Error, disable flag
this.error('RX', `Unhandled action triggered ${action}`);
reply = false;
break;
}
// update state
this.runCycle();
return reply;
}
/**
* @description State machine checks what actions need to
* happen at every app cycle
*/
runCycle() {
const h = this.httpMessages?.messages;
let httpMessage = null;
switch (this.ontimeCycle) {
case 'idle':
break;
case 'armed':
// if we come from roll, see if we can start
if (this.state === 'roll') {
this.update();
}
break;
case 'onLoad':
// broadcast change
this.broadcastState();
// Todo: wrap in reusable function
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onLoad?.url !== '') {
httpMessage = h?.onLoad?.url;
}
}
// update lifecycle: armed
this.ontimeCycle = this.cycleState.armed;
break;
case 'onStart':
// broadcast current state
this.broadcastState();
// send OSC if there is something running
// _finish at is only set when an event is loaded
if (this._finishAt > 0) {
this.sendOsc(this.osc.implemented.play);
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onStart?.url !== '') {
httpMessage = h?.onStart?.url;
}
}
// update lifecycle: onUpdate
this.ontimeCycle = this.cycleState.onUpdate;
break;
case 'onUpdate':
// call update
this.update();
// broadcast current state
this.broadcastTimer();
// through OSC, only if running
if (this.state === 'start' || this.state === 'roll') {
if (this.current != null && this.secondaryTimer == null) {
this.sendOsc(this.osc.implemented.time, this.timeTag);
this.sendOsc(
this.osc.implemented.overtime,
this.current > 0 ? 0 : 1
);
this.sendOsc(
this.osc.implemented.title,
this.titles?.titleNow || ''
);
}
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onUpdate?.url !== '') {
httpMessage = h?.onUpdate?.url;
}
}
break;
case 'onPause':
// broadcast current state
this.broadcastState();
// send OSC
this.sendOsc(this.osc.implemented.pause);
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onPause?.url !== '') {
httpMessage = h?.onPause?.url;
}
}
// update lifecycle: armed
this.ontimeCycle = this.cycleState.armed;
break;
case 'onStop':
// broadcast change
this.broadcastState();
// send OSC if something was actually stopped
if (this.prevCycle === this.cycleState.onUpdate) {
this.sendOsc(this.osc.implemented.stop);
}
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onStop?.url !== '') {
httpMessage = h?.onStop?.url;
}
}
// update lifecycle: idle
this.ontimeCycle = this.cycleState.idle;
break;
case 'onFinish':
// broadcast change
this.broadcastState();
// finished an event
this.sendOsc(this.osc.implemented.finished);
// check integrations - http
if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onFinish?.url !== '') {
httpMessage = h?.onFinish?.url;
}
}
// update lifecycle: onUpdate
this.ontimeCycle = this.cycleState.onUpdate;
break;
default:
this.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
}
// send http message if any
if (httpMessage != null) {
const v = {
$timer: this.timeTag,
$title: this.titles.titleNow,
$presenter: this.titles.presenterNow,
$subtitle: this.titles.subtitleNow,
'$next-title': this.titles.titleNext,
'$next-presenter': this.titles.presenterNext,
'$next-subtitle': this.titles.subtitleNext,
};
const m = cleanURL(replacePlaceholder(httpMessage, v));
this.http.send(m);
}
// update
this.update();
// reset cycle
this.prevCycle = this.ontimeCycle;
}
update() {
// if there is nothing selected, update clock
const now = this._getCurrentTime();
// if we are not updating, send the timers
if (this.ontimeCycle !== this.cycleState.onUpdate) {
this.clock = now;
this.broadcastThis('timer', {
clock: now,
running: Timer.toSeconds(this.current),
secondary: Timer.toSeconds(this.secondaryTimer),
durationSeconds: Timer.toSeconds(this.duration),
expectedFinish: this._getExpectedFinish(),
startedAt: this._startedAt,
});
}
// Have we skipped onStart?
if (this.state === 'start' || this.state === 'roll') {
if (this.ontimeCycle === this.cycleState.armed) {
// update lifecycle: onStart
this.ontimeCycle = this.cycleState.onStart;
this.runCycle();
}
}
// update default functions
super.update();
if (this._finishedFlag) {
// update lifecycle: onFinish and call cycle
this.ontimeCycle = this.cycleState.onFinish;
this._finishedFlag = false;
this.runCycle();
}
// only implement roll here, rest implemented in super
if (this.state === 'roll') {
const u = {
selectedEventId: this.selectedEventId,
current: this.current,
// safeguard on midnight rollover
_finishAt:
this._finishAt >= this._startedAt
? this._finishAt
: this._finishAt + DAY_TO_MS,
clock: this.clock,
secondaryTimer: this.secondaryTimer,
_secondaryTarget: this._secondaryTarget,
};
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
updateRoll(u);
this.current = updatedTimer;
this.secondaryTimer = updatedSecondaryTimer;
if (isFinished) {
// update lifecycle: onFinish
this.ontimeCycle = this.cycleState.onFinish;
this.runCycle();
}
if (doRollLoad) {
this.rollLoad();
}
}
}
/**
* Set titles and broadcast change
* @param {string} action
* @param {any} payload
* @private
*/
_setTitles(action, payload) {
switch (action) {
/*******************************************/
// Presenter message
case 'set-presenter-text':
this.presenter.text = payload;
this.broadcastThis('messages-presenter', this.presenter);
break;
case 'set-presenter-visible':
this.presenter.visible = payload;
this.broadcastThis('messages-presenter', this.presenter);
break;
/*******************************************/
// Public message
case 'set-public-text':
this.public.text = payload;
this.broadcastThis('messages-public', this.public);
break;
case 'set-public-visible':
this.public.visible = payload;
this.broadcastThis('messages-public', this.public);
break;
/*******************************************/
// Lower third message
case 'set-lower-text':
this.lower.text = payload;
this.broadcastThis('messages-lower', this.lower);
break;
case 'set-lower-visible':
this.lower.visible = payload;
this.broadcastThis('messages-lower', this.lower);
break;
default:
break;
}
}
/**
* Handle socket io connections
* @private
*/
_listenToConnections() {
this.io.on('connection', (socket) => {
/*******************************/
/*** HANDLE NEW CONNECTION ***/
/*** --------------------- ***/
/*******************************/
// keep track of connections
this._numClients++;
this._clientNames[socket.id] = getRandomName();
const m = `${this._numClients} Clients with new connection: ${
this._clientNames[socket.id]
}`;
this.info('CLIENT', m);
// send state
socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.nextPublicEventId);
/********************************/
/*** HANDLE DISCONNECT USER ***/
/*** ---------------------- ***/
/********************************/
socket.on('disconnect', () => {
this._numClients--;
const m = `${this._numClients} Clients with disconnection: ${
this._clientNames[socket.id]
}`;
delete this._clientNames[socket.id];
this.info('CLIENT', m);
});
/***************************************/
/*** TIMER STATE GETTERS / SETTERS ***/
/*** ----------------------------- ***/
/***************************************/
/*******************************************/
// general playback state
socket.on('get-state', () => {
socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.this.nextPublicEventId);
});
/*******************************************/
// timer
socket.on('get-current', () => {
socket.emit('current', this.getCurrentInSeconds());
});
socket.on('get-timer', () => {
socket.emit('timer', this.getTimeObject());
});
socket.on('increment-timer', (data) => {
if (isNaN(parseInt(data))) return;
if (data < -5 || data > 5) return;
this.increment(data * 1000 * 60);
});
/*******************************************/
// playstate
socket.on('set-playstate', (data) => {
this.trigger(data);
});
socket.on('get-playstate', () => {
socket.emit('playstate', this.state);
});
socket.on('set-onAir', (data) => {
this.onAir = data;
this.broadcastThis('onAir', this.onAir);
});
socket.on('get-onAir', () => {
socket.emit('onAir', this.onAir);
});
/*******************************************/
// selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
total: this.numEvents,
});
});
socket.on('get-selected-id', () => {
socket.emit('selected-id', this.selectedEventId);
});
socket.on('get-numevents', () => {
socket.emit('numevents', this.numEvents);
});
socket.on('get-next-id', () => {
socket.emit('next-id', this.nextEventId);
});
socket.on('get-publicselected-id', () => {
socket.emit('publicselected-id', this.selectedPublicEventId);
});
socket.on('get-publicnext-id', () => {
socket.emit('publicnext-id', this.nextPublicEventId);
});
// title data
socket.on('get-titles', () => {
socket.emit('titles', this.titles);
});
// title data
socket.on('get-publictitles', () => {
socket.emit('publictitles', this.titlesPublic);
});
/***********************************/
/*** MESSAGE GETTERS / SETTERS ***/
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// Messages
socket.on('get-messages', () => {
this.broadcastThis('messages-presenter', this.presenter);
this.broadcastThis('messages-public', this.public);
this.broadcastThis('messages-lower', this.lower);
});
// Presenter message
socket.on('set-presenter-text', (data) => {
this._setTitles('set-presenter-text', data);
});
socket.on('set-presenter-visible', (data) => {
this._setTitles('set-presenter-visible', data);
});
socket.on('get-presenter', () => {
this.broadcastThis('messages-presenter', this.presenter);
});
/*******************************************/
// Public message
socket.on('set-public-text', (data) => {
this._setTitles('set-public-text', data);
});
socket.on('set-public-visible', (data) => {
this._setTitles('set-public-visible', data);
});
socket.on('get-public', () => {
socket.emit('messages-public', this.public);
});
/*******************************************/
// Lower third message
socket.on('set-lower-text', (data) => {
this._setTitles('set-lower-text', data);
});
socket.on('set-lower-visible', (data) => {
this._setTitles('set-lower-visible', data);
});
socket.on('get-lower', () => {
socket.emit('messages-lower', this.lower);
});
});
}
/**
* Deletes running event list from object
*/
clearEventList() {
// unload events
this.unload();
// set general
this._eventlist = [];
this.numEvents = 0;
// update lifecycle: onStop
this.ontimeCycle = this.cycleState.onStop;
// update clients
this.broadcastThis('numevents', this.numEvents);
}
/**
* Adds an event list to object
* @param {array} eventlist
*/
setupWithEventList(eventlist) {
if (!Array.isArray(eventlist) || eventlist.length < 1) return;
// filter only events
const events = eventlist.filter((e) => e.type === 'event');
const numEvents = events.length;
// set general
this._eventlist = events;
this.numEvents = numEvents;
// list may contain no events
if (numEvents < 1) return;
// load first event
this.loadEvent(0);
// update clients
this.broadcastState();
// run cycle
this.runCycle();
}
/**
* Updates event list in object
* @param {array} eventlist
*/
updateEventList(eventlist) {
// filter only events
const events = eventlist.filter((e) => e.type === 'event');
const numEvents = events.length;
// is this the first event
let first = this.numEvents === 0;
// set general
this._eventlist = events;
this.numEvents = numEvents;
// list may be empty
if (numEvents < 1) {
this.unload();
return;
}
// auto load if is the only event
if (first) {
this.loadEvent(0);
} else if (this.selectedEventId != null) {
// handle reload selected
// Look for event (order might have changed)
const eventIndex = this._eventlist.findIndex(
(e) => e.id === this.selectedEventId
);
// Maybe is missing
if (eventIndex === -1) {
this._resetTimers();
this._resetSelection();
return;
}
// Reload data if running
const type = this._startedAt != null ? 'reload' : 'load';
this.loadEvent(eventIndex, type);
}
// update clients
this.broadcastState();
// run cycle
this.runCycle();
}
/**
* Updates a single id in the object list
* @param {string} id
* @param {object} entry - new event object
*/
updateSingleEvent(id, entry) {
// find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === id);
if (eventIndex === -1) return;
// update event in memory
const e = this._eventlist[eventIndex];
this._eventlist[eventIndex] = { ...e, ...entry };
try {
// check if entry is running
if (e.id === this.selectedEventId) {
// handle reload selected
// Reload data if running
let type =
this.selectedEventId === id && this._startedAt != null
? 'reload'
: 'load';
this.loadEvent(this.selectedEventIndex, type);
} else if (e.id === this.nextEventId) {
// roll needs to recalculate
if (this.state === 'roll') {
this.rollLoad();
}
}
// load titles
if ('title' in e || 'subtitle' in e || 'presenter' in e) {
// TODO: should be more selective on the need to load titles
this._loadTitlesNext();
this._loadTitlesNow();
}
} catch (error) {
this.error('SERVER', error);
}
// update clients
this.broadcastState();
// run cycle
this.runCycle();
}
/**
* Deleted an event from the list by its id
* @param {string} eventId
*/
deleteId(eventId) {
// find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === eventId);
if (eventIndex === -1) return;
// delete event and update count
this._eventlist.splice(eventIndex, 1);
this.numEvents = this._eventlist.length;
// reload data if necessary
if (eventId === this.selectedEventId) {
this.unload();
return;
}
// update selected event index
this.selectedEventIndex = this._eventlist.findIndex(
(e) => e.id === this.selectedEventId
);
// reload titles if necessary
if (eventId === this.nextEventId || eventId === this.nextPublicEventId) {
this._loadTitlesNext();
} else if (eventId === this.selectedPublicEventId) {
this._loadTitlesNow();
}
// update clients
this.broadcastState();
// run cycle
this.runCycle();
}
/**
* @description loads an event with a given Id
* @param {string} eventId - ID of event in eventlist
*/
loadEventById(eventId) {
const eventIndex = this._eventlist.findIndex((e) => e.id === eventId);
if (eventIndex === -1) return;
this.pause();
this.loadEvent(eventIndex, 'load');
// run cycle
this.runCycle();
}
/**
* @description loads an event with a given index
* @param {number} eventIndex - Index of event in eventlist
*/
loadEventByIndex(eventIndex) {
if (eventIndex === -1 || eventIndex > this.numEvents) return;
this.pause();
this.loadEvent(eventIndex, 'load');
// run cycle
this.runCycle();
}
/**
* Loads a given event by index
* @param {object} eventIndex
* @param {string} [type='load'] - 'load' or 'reload', whether we are keeping running time
*/
loadEvent(eventIndex, type = 'load') {
const e = this._eventlist[eventIndex];
if (e == null) return;
const start = e.timeStart == null || e.timeStart === '' ? 0 : e.timeStart;
let end = e.timeEnd == null || e.timeEnd === '' ? 0 : e.timeEnd;
// in case the end is earlier than start, we assume is the day after
if (end < start) end += DAY_TO_MS;
// time stuff changes on whether we keep the running clock
if (type === 'load') {
this._resetTimers();
this.duration = end - start;
this.current = this.duration;
this.selectedEventIndex = eventIndex;
this.selectedEventId = e.id;
} else if (type === 'reload') {
const now = this._getCurrentTime();
const elapsed = this.getElapsed();
this.duration = end - start;
this.selectedEventIndex = eventIndex;
this._finishAt = now + (this.duration - elapsed);
}
// load current titles
this._loadTitlesNow();
// look for event after
this._loadTitlesNext();
// update lifecycle: onLoad
this.ontimeCycle = this.cycleState.onLoad;
}
_loadTitlesNow() {
const e = this._eventlist[this.selectedEventIndex];
if (e == null) return;
// private title is always current
// check if current is also public
if (e.isPublic) {
this._loadThisTitles(e, 'now');
} else {
this._loadThisTitles(e, 'now-private');
// assume there is no public event
this.titlesPublic.titleNow = null;
this.titlesPublic.subtitleNow = null;
this.titlesPublic.presenterNow = null;
this.selectedPublicEventId = null;
// if there is nothing before, return
if (this.selectedEventIndex === 0) return;
// iterate backwards to find it
for (let i = this.selectedEventIndex; i >= 0; i--) {
if (
this._eventlist[i].type === 'event' &&
this._eventlist[i].isPublic
) {
this._loadThisTitles(this._eventlist[i], 'now-public');
break;
}
}
}
}
_loadThisTitles(e, type) {
if (e == null) return;
switch (type) {
// now, load to both public and private
case 'now':
// public
this.titlesPublic.titleNow = e.title;
this.titlesPublic.subtitleNow = e.subtitle;
this.titlesPublic.presenterNow = e.presenter;
this.selectedPublicEventId = e.id;
// private
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
this.titles.noteNow = e.note;
this.selectedEventId = e.id;
break;
case 'now-public':
this.titlesPublic.titleNow = e.title;
this.titlesPublic.subtitleNow = e.subtitle;
this.titlesPublic.presenterNow = e.presenter;
this.selectedPublicEventId = e.id;
break;
case 'now-private':
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
this.titles.noteNow = e.note;
this.selectedEventId = e.id;
break;
// next, load to both public and private
case 'next':
// public
this.titlesPublic.titleNext = e.title;
this.titlesPublic.subtitleNext = e.subtitle;
this.titlesPublic.presenterNext = e.presenter;
this.nextPublicEventId = e.id;
// private
this.titles.titleNext = e.title;
this.titles.subtitleNext = e.subtitle;
this.titles.presenterNext = e.presenter;
this.titles.noteNext = e.note;
this.nextEventId = e.id;
break;
case 'next-public':
this.titlesPublic.titleNext = e.title;
this.titlesPublic.subtitleNext = e.subtitle;
this.titlesPublic.presenterNext = e.presenter;
this.nextPublicEventId = e.id;
break;
case 'next-private':
this.titles.titleNext = e.title;
this.titles.subtitleNext = e.subtitle;
this.titles.presenterNext = e.presenter;
this.titles.noteNext = e.note;
this.nextEventId = e.id;
break;
default:
break;
}
}
_loadTitlesNext() {
// maybe there is nothing to load
if (this.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.titlesPublic.titleNext = null;
this.titlesPublic.subtitleNext = null;
this.titlesPublic.presenterNext = null;
this.nextPublicEventId = null;
if (this.selectedEventIndex < this.numEvents - 1) {
let nextPublic = false;
let nextPrivate = false;
for (let i = this.selectedEventIndex + 1; i < this.numEvents; i++) {
// check that is the right type
if (this._eventlist[i].type === 'event') {
// if we have not set private
if (!nextPrivate) {
this._loadThisTitles(this._eventlist[i], 'next-private');
nextPrivate = true;
}
// if event is public
if (this._eventlist[i].isPublic) {
this._loadThisTitles(this._eventlist[i], 'next-public');
nextPublic = true;
}
}
// Stop if both are set
if (nextPublic && nextPrivate) break;
}
}
}
_resetSelection() {
this.titles = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
noteNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
noteNext: null,
};
this.titlesPublic = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
this.selectedEventIndex = null;
this.selectedEventId = null;
this.nextEventId = null;
this.selectedPublicEventId = null;
this.nextPublicEventId = null;
}
/**
* @description Set onAir property of timer
* @param {boolean} onAir - whether flag is active
*/
setonAir(onAir) {
this.onAir = onAir;
// broadcast change
this.broadcastThis('onAir', onAir);
}
start() {
// do we need to change
if (this.state === 'start') return;
// if there is nothing selected, no nothing
if (this.selectedEventId == null) return;
// call super
super.start();
// update lifecycle: onStart
this.ontimeCycle = this.cycleState.onStart;
}
pause() {
// do we need to change
if (this.state === 'pause') return;
// if there is nothing selected, no nothing
if (this.selectedEventId == null) return;
// call super
super.pause();
// update lifecycle: onPause
this.ontimeCycle = this.cycleState.onPause;
}
stop() {
// do we need to change
if (this.state === 'stop') return;
// call super
super.stop();
// update lifecycle: onPause
this.ontimeCycle = this.cycleState.onStop;
}
increment(amount) {
// call super
super.increment(amount);
// run cycle
this.runCycle();
}
rollLoad() {
const now = this._getCurrentTime();
let prevLoaded = this.selectedEventId;
// maybe roll has already been loaded
if (this.secondaryTimer === null) {
this._resetTimers(true);
this._resetSelection();
}
const {
nowIndex,
nowId,
publicIndex,
nextIndex,
publicNextIndex,
timers,
timeToNext,
} = getSelectionByRoll(this._eventlist, now);
// nothing to play, unload
if (nowIndex === null && nextIndex === null) {
this.unload();
this.warning('SERVER', 'Roll: no events found');
return;
}
// there is something running, load
if (nowIndex !== null) {
// clear secondary timers
this.secondaryTimer = null;
this._secondaryTarget = null;
// set timers
this._startedAt = timers._startedAt;
this._finishAt = timers._finishAt;
this.duration = timers.duration;
this.current = timers.current;
// set selection
this.selectedEventId = nowId;
this.selectedEventIndex = nowIndex;
}
// found something to run next
if (nextIndex != null) {
// Set running timers
if (nowIndex === null) {
// only warn the first time
if (this.secondaryTimer === null) {
this.info('SERVER', 'Roll: waiting for event start');
}
// reset running timer
// ??? should this not have been reset?
this.current = null;
// timer counts to next event
this.secondaryTimer = timeToNext;
this._secondaryTarget = this._eventlist[nextIndex].timeStart;
}
// TITLES: Load next private
this._loadThisTitles(this._eventlist[nextIndex], 'next-private');
}
// TITLES: Load next public
if (publicNextIndex !== null) {
this._loadThisTitles(this._eventlist[publicNextIndex], 'next-public');
}
// TITLES: Load now private
if (nowIndex !== null) {
this._loadThisTitles(this._eventlist[nowIndex], 'now-private');
}
// TITLES: Load now public
if (publicIndex !== null) {
this._loadThisTitles(this._eventlist[publicIndex], 'now-public');
}
if (prevLoaded !== this.selectedEventId) {
// update lifecycle: onLoad
this.ontimeCycle = this.cycleState.onLoad;
// ensure we go through onLoad cycle
this.runCycle();
}
}
roll() {
// do we need to change
if (this.state === 'roll') return;
if (this.numEvents === 0 || this.numEvents == null) return;
// set state
this.state = 'roll';
// update lifecycle: armed
this.ontimeCycle = this.cycleState.armed;
// load into event
this.rollLoad();
}
previous() {
// check that we have events to run
if (this.numEvents < 1) return;
// maybe this is the first event?
if (this.selectedEventIndex === 0) return;
// if there is no event running, go to first
if (this.selectedEventIndex == null) {
this.loadEvent(0);
return;
}
// send OSC
this.sendOsc(this.osc.implemented.previous);
// change playstate
this.pause();
const gotoEvent =
this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent);
}
next() {
// check that we have events to run
if (this.numEvents < 1) return;
// maybe this is the last event?
if (this.selectedEventIndex === this.numEvents - 1) return;
// if there is no event running, go to first
if (this.selectedEventIndex == null) {
this.loadEvent(0);
return;
}
// send OSC
this.sendOsc(this.osc.implemented.next);
// change playstate
this.pause();
const gotoEvent =
this.selectedEventIndex < this.numEvents - 1
? this.selectedEventIndex + 1
: this.numEvents - 1;
if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent);
}
unload() {
// reset timer
this._resetTimers(true);
// reset selected
this._resetSelection();
// broadcast state
this.broadcastState();
// reset playstate
this.stop();
}
reload() {
if (this.numEvents === 0 || this.numEvents == null) return;
// change playstate
this.pause();
// send OSC
this.sendOsc(this.osc.implemented.reload);
// reload data
this.loadEvent(this.selectedEventIndex);
}
/****************************************************************************/
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
* Utility method, sends message and pushes into stack
* @param {string} level
* @param {string} origin
* @param {string} text
*/
_push(level, origin, text) {
const m = {
id: generateId(),
level,
origin,
text,
time: stringFromMillis(this._getCurrentTime()),
};
this.messageStack.unshift(m);
this.io.emit('logger', m);
if (process.env.NODE_ENV !== 'prod') {
console.log(`[${m.level}] \t ${m.origin} \t ${m.text}`);
}
if (this.messageStack.length > this.MAX_MESSAGES) {
this.messageStack.pop();
}
}
/**
* 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);
}
/****************************************************************************/
/**
* Integrations
* -------------
*
* Code related to integrations
*
*/
/**
* Calls OSC send message and resolves reply to logger
* @param {string} message
* @param {any} [payload]
*/
async sendOsc(message, payload = undefined) {
// Todo: add disabled osc check
const reply = await this.osc.send(message, payload);
if (!reply.success) {
this.error('TX', reply.message);
}
}
}