mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-24 08:29:14 +00:00
refactor: pass HTTP to integration controller (#652)
* refactor: pass HTTP to integration controller * refactor: deprecate onair control * refactor: remove playback router
This commit is contained in:
@@ -6,24 +6,9 @@
|
|||||||
gap: $section-spacing;
|
gap: $section-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onAirSection {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $element-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonSection {
|
.buttonSection {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: $element-spacing;
|
gap: $element-spacing;
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
color: $label-gray;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: $action-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
|
||||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
|
||||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||||
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline';
|
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline';
|
||||||
|
|
||||||
@@ -72,18 +70,6 @@ export default function MessageControl() {
|
|||||||
changeHandler={() => undefined}
|
changeHandler={() => undefined}
|
||||||
actionHandler={() => undefined}
|
actionHandler={() => undefined}
|
||||||
/>
|
/>
|
||||||
<div className={style.onAirSection}>
|
|
||||||
<label className={style.label}>Toggle On Air state</label>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
|
|
||||||
leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
|
||||||
onClick={() => setMessage.onAir(!data.onAir)}
|
|
||||||
data-testid='toggle on air'
|
|
||||||
>
|
|
||||||
{data?.onAir ? 'Ontime is On Air' : 'Ontime is Off Air'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,17 +62,14 @@ export class OscServer implements IAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reply = dispatchFromAdapter(
|
// we dont reply on OSC
|
||||||
|
dispatchFromAdapter(
|
||||||
path,
|
path,
|
||||||
{
|
{
|
||||||
payload: transformedPayload,
|
payload: transformedPayload,
|
||||||
},
|
},
|
||||||
'osc',
|
'osc',
|
||||||
);
|
);
|
||||||
if (reply) {
|
|
||||||
const { topic, payload } = reply;
|
|
||||||
this.osc.emit(topic, payload);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(LogOrigin.Rx, `OSC IN: ${error}`);
|
logger.error(LogOrigin.Rx, `OSC IN: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ export class SocketServer implements IAdapter {
|
|||||||
'ws',
|
'ws',
|
||||||
);
|
);
|
||||||
if (reply) {
|
if (reply) {
|
||||||
const { topic, payload } = reply;
|
const { payload } = reply;
|
||||||
ws.send(topic, payload);
|
ws.send(type, payload);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
|
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
|||||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
import { router as rundownRouter } from './routes/rundownRouter.js';
|
||||||
import { router as projectRouter } from './routes/projectRouter.js';
|
import { router as projectRouter } from './routes/projectRouter.js';
|
||||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||||
import { router as playbackRouter } from './routes/playbackRouter.js';
|
import { router as apiRouter } from './routes/apiRouter.js';
|
||||||
|
|
||||||
// Import adapters
|
// Import adapters
|
||||||
import { OscServer } from './adapters/OscAdapter.js';
|
import { OscServer } from './adapters/OscAdapter.js';
|
||||||
@@ -61,7 +61,7 @@ app.use(express.json({ limit: '1mb' }));
|
|||||||
app.use('/events', rundownRouter);
|
app.use('/events', rundownRouter);
|
||||||
app.use('/project', projectRouter);
|
app.use('/project', projectRouter);
|
||||||
app.use('/ontime', ontimeRouter);
|
app.use('/ontime', ontimeRouter);
|
||||||
app.use('/playback', playbackRouter);
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
// serve static - css
|
// serve static - css
|
||||||
app.use('/external', express.static(externalsStartDirectory));
|
app.use('/external', express.static(externalsStartDirectory));
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class EventLoader {
|
|||||||
*/
|
*/
|
||||||
static getEventWithCue(cue) {
|
static getEventWithCue(cue) {
|
||||||
const timedEvents = EventLoader.getTimedEvents();
|
const timedEvents = EventLoader.getTimedEvents();
|
||||||
return timedEvents.find((event) => event.cue === cue);
|
return timedEvents.find((event) => event.cue.toLowerCase() === cue.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { messageService } from '../services/message-service/MessageService.js';
|
// skipcq: JS-C1003 - we like the API
|
||||||
|
import * as assert from '../utils/assert.js';
|
||||||
|
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
|
||||||
|
|
||||||
|
import { isPartialTimerMessage, messageService } from '../services/message-service/MessageService.js';
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { PlaybackService } from '../services/PlaybackService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { parse, updateEvent } from './integrationController.config.js';
|
import { parse, updateEvent } from './integrationController.config.js';
|
||||||
@@ -9,273 +13,205 @@ export type ChangeOptions = {
|
|||||||
value: unknown;
|
value: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO: re-throwing the error does not add any extra information or value
|
|
||||||
export function dispatchFromAdapter(
|
export function dispatchFromAdapter(
|
||||||
type: string,
|
type: string,
|
||||||
args: {
|
args: {
|
||||||
payload: unknown;
|
payload: unknown;
|
||||||
},
|
},
|
||||||
_source?: 'osc' | 'ws',
|
_source?: 'osc' | 'ws' | 'http',
|
||||||
) {
|
) {
|
||||||
const payload = args.payload;
|
const payload = args.payload;
|
||||||
const typeComponents = type.toLowerCase().split('/');
|
const action = type.toLowerCase();
|
||||||
const mainType = typeComponents[0];
|
const handler = actionHandlers[action];
|
||||||
|
if (handler) {
|
||||||
switch (mainType) {
|
return handler(payload);
|
||||||
case 'test-ontime': {
|
} else {
|
||||||
return { topic: 'hello' };
|
throw new Error(`Unhandled message ${type}`);
|
||||||
}
|
|
||||||
|
|
||||||
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-blink': {
|
|
||||||
if (typeof payload !== 'undefined') {
|
|
||||||
messageService.setTimerBlink(Boolean(payload));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'set-timer-blackout': {
|
|
||||||
if (typeof payload !== 'undefined') {
|
|
||||||
messageService.setTimerBlackout(Boolean(payload));
|
|
||||||
}
|
|
||||||
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 'set-external-message-text': {
|
|
||||||
if (typeof payload !== 'string') {
|
|
||||||
throw new Error(`Unable to parse payload: ${payload}`);
|
|
||||||
}
|
|
||||||
messageService.setExternalText(payload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case 'set-external-message-visible': {
|
|
||||||
if (typeof payload === 'undefined') {
|
|
||||||
throw new Error(`Unable to parse payload: ${payload}`);
|
|
||||||
}
|
|
||||||
messageService.setExternalVisibility(Boolean(payload));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'start': {
|
|
||||||
PlaybackService.start();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'start-next': {
|
|
||||||
PlaybackService.startNext();
|
|
||||||
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 || typeof payload !== 'string') {
|
|
||||||
throw new Error(`Event ID not recognised: ${payload}`);
|
|
||||||
}
|
|
||||||
PlaybackService.startById(payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'startcue': {
|
|
||||||
if (!payload || typeof payload !== 'string') {
|
|
||||||
throw new Error(`Event cue not recognised: ${payload}`);
|
|
||||||
}
|
|
||||||
PlaybackService.startByCue(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 'addtime': {
|
|
||||||
const time = Number(payload);
|
|
||||||
if (isNaN(time)) {
|
|
||||||
throw new Error(`Time not recognised ${payload}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
PlaybackService.addTime(time);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Could not add time: ${error}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
//deprecated
|
|
||||||
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 'gotocue':
|
|
||||||
case 'loadcue': {
|
|
||||||
if (!payload || typeof payload !== 'string') {
|
|
||||||
throw new Error(`Event cue not recognised: ${payload}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
PlaybackService.loadByCue(payload);
|
|
||||||
} 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// WS: {type: 'change', payload: { eventId, property, value } }
|
|
||||||
case 'change': {
|
|
||||||
const { eventId, property, value } = payload as ChangeOptions;
|
|
||||||
const { parsedPayload, parsedProperty } = parse(property, value);
|
|
||||||
return updateEvent(eventId, parsedProperty, parsedPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
throw new Error(`Unhandled message ${type}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActionHandler = (payload: unknown) => { payload: unknown };
|
||||||
|
|
||||||
|
// TODO: add data to missing returns once available
|
||||||
|
const actionHandlers: Record<string, ActionHandler> = {
|
||||||
|
/* General */
|
||||||
|
'test-ontime': () => ({ payload: `Hello from Ontime version ${ONTIME_VERSION}` }),
|
||||||
|
'ontime-poll': () => ({
|
||||||
|
payload: eventStore.poll(),
|
||||||
|
}),
|
||||||
|
change: (payload) => {
|
||||||
|
// WS: {type: 'change', payload: { eventId, property, value } }
|
||||||
|
const { eventId, property, value } = payload as ChangeOptions;
|
||||||
|
const { parsedPayload, parsedProperty } = parse(property, value);
|
||||||
|
|
||||||
|
const updatedEvent = updateEvent(eventId, parsedProperty, parsedPayload);
|
||||||
|
return { payload: updatedEvent };
|
||||||
|
},
|
||||||
|
/* Message Service */
|
||||||
|
'set-timer-message': (payload) => {
|
||||||
|
if (!isPartialTimerMessage(payload)) {
|
||||||
|
throw new Error('Payload is not a valid timer message');
|
||||||
|
}
|
||||||
|
const newState = messageService.setTimerMessage(payload);
|
||||||
|
return { payload: newState.timerMessage };
|
||||||
|
},
|
||||||
|
'set-timer-blink': (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
const newState = messageService.setTimerBlink(Boolean(payload));
|
||||||
|
return { payload: newState.timerMessage };
|
||||||
|
},
|
||||||
|
'set-timer-blackout': (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
const newState = messageService.setTimerBlackout(Boolean(payload));
|
||||||
|
return { payload: newState.timerMessage };
|
||||||
|
},
|
||||||
|
'set-timer-message-text': (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
const newState = messageService.setTimerText(payload);
|
||||||
|
return { payload: newState.timerMessage };
|
||||||
|
},
|
||||||
|
'set-timer-message-visible': (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
const newState = messageService.setTimerVisibility(Boolean(payload));
|
||||||
|
return { payload: newState.timerMessage };
|
||||||
|
},
|
||||||
|
'set-public-message-text': (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
const newState = messageService.setPublicText(payload);
|
||||||
|
return { payload: newState.publicMessage };
|
||||||
|
},
|
||||||
|
'set-public-message-visible': (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
const newState = messageService.setPublicVisibility(Boolean(payload));
|
||||||
|
return { payload: newState.publicMessage };
|
||||||
|
},
|
||||||
|
'set-lower-message-text': (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
const newState = messageService.setLowerText(payload);
|
||||||
|
return { payload: newState.lowerMessage };
|
||||||
|
},
|
||||||
|
'set-lower-message-visible': (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
const newState = messageService.setLowerVisibility(Boolean(payload));
|
||||||
|
return { payload: newState.lowerMessage };
|
||||||
|
},
|
||||||
|
'set-external-message-text': (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
const newState = messageService.setExternalText(payload);
|
||||||
|
return { payload: newState.externalMessage };
|
||||||
|
},
|
||||||
|
'set-external-message-visible': (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
const newState = messageService.setExternalVisibility(Boolean(payload));
|
||||||
|
return { payload: newState.externalMessage };
|
||||||
|
},
|
||||||
|
/* Playback */
|
||||||
|
start: (payload) => {
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
if ('index' in payload) {
|
||||||
|
const reply = actionHandlers.startindex(payload.index);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
if ('id' in payload) {
|
||||||
|
const reply = actionHandlers.startid(payload.id);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
if ('cue' in payload) {
|
||||||
|
const reply = actionHandlers.startcue(payload.cue);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackService.start();
|
||||||
|
|
||||||
|
return { payload: 'start' };
|
||||||
|
},
|
||||||
|
'start-next': () => {
|
||||||
|
PlaybackService.startNext();
|
||||||
|
return { payload: 'start' };
|
||||||
|
},
|
||||||
|
startindex: (payload) => {
|
||||||
|
const eventIndex = numberOrError(payload);
|
||||||
|
if (eventIndex <= 0) {
|
||||||
|
throw new Error(`Event index out of range ${eventIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexes in frontend are 1 based
|
||||||
|
PlaybackService.startByIndex(eventIndex - 1);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
startid: (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
PlaybackService.startById(payload);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
startcue: (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
PlaybackService.startByCue(payload);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
pause: () => {
|
||||||
|
PlaybackService.pause();
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
previous: () => {
|
||||||
|
PlaybackService.loadPrevious();
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
next: () => {
|
||||||
|
PlaybackService.loadNext();
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
stop: () => {
|
||||||
|
PlaybackService.stop();
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
reload: () => {
|
||||||
|
PlaybackService.reload();
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
roll: () => {
|
||||||
|
PlaybackService.roll();
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
loadindex: (payload) => {
|
||||||
|
const eventIndex = numberOrError(payload);
|
||||||
|
if (eventIndex <= 0) {
|
||||||
|
throw new Error(`Event index out of range ${eventIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexes in frontend are 1 based
|
||||||
|
PlaybackService.loadByIndex(eventIndex - 1);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
loadid: (payload) => {
|
||||||
|
assert.isDefined(payload);
|
||||||
|
PlaybackService.loadById(payload.toString().toLowerCase());
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
loadcue: (payload) => {
|
||||||
|
assert.isString(payload);
|
||||||
|
PlaybackService.loadByCue(payload);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
addtime: (payload) => {
|
||||||
|
const time = numberOrError(payload);
|
||||||
|
PlaybackService.addTime(time);
|
||||||
|
return { payload: 'success' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a value of type number, converting if necessary
|
||||||
|
* Otherwise throws
|
||||||
|
* @param value
|
||||||
|
* @returns number
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
function numberOrError(value: unknown) {
|
||||||
|
const converted = Number(value);
|
||||||
|
if (isNaN(converted)) {
|
||||||
|
throw new Error('Payload is not a valid number');
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
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: eventStore.get('playback') });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/start'
|
|
||||||
// Starts timer object
|
|
||||||
export const pbStart = async (req, res) => {
|
|
||||||
const { eventId, eventIndex } = req.query;
|
|
||||||
if (eventId) {
|
|
||||||
const success = PlaybackService.startById(eventId);
|
|
||||||
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event ID' });
|
|
||||||
} else if (eventIndex) {
|
|
||||||
const index = Number(eventIndex);
|
|
||||||
if (!isNaN(index)) {
|
|
||||||
const success = PlaybackService.startByIndex(eventIndex - 1);
|
|
||||||
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event index' });
|
|
||||||
} else {
|
|
||||||
res.status(400).send({ message: 'Invalid event index' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PlaybackService.start();
|
|
||||||
res.sendStatus(202);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/pause'
|
|
||||||
// Pauses timer object
|
|
||||||
export const pbPause = async (req, res) => {
|
|
||||||
PlaybackService.pause();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/stop'
|
|
||||||
// Stops timer object
|
|
||||||
export const pbStop = async (req, res) => {
|
|
||||||
PlaybackService.stop();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/roll'
|
|
||||||
// Sets timer object to roll mode
|
|
||||||
export const pbRoll = async (req, res) => {
|
|
||||||
PlaybackService.roll();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/previous'
|
|
||||||
// Loads previous event
|
|
||||||
export const pbPrevious = async (req, res) => {
|
|
||||||
PlaybackService.loadPrevious();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/next'
|
|
||||||
// Loads Next event
|
|
||||||
export const pbNext = async (req, res) => {
|
|
||||||
PlaybackService.loadNext();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/load'
|
|
||||||
// Load requested event
|
|
||||||
export const pbLoad = async (req, res) => {
|
|
||||||
const { eventId, eventIndex } = req.query;
|
|
||||||
if (eventId) {
|
|
||||||
const success = PlaybackService.loadById(eventId);
|
|
||||||
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event ID' });
|
|
||||||
} else if (eventIndex) {
|
|
||||||
const index = Number(eventIndex);
|
|
||||||
if (!isNaN(index)) {
|
|
||||||
const success = PlaybackService.loadByIndex(eventIndex - 1);
|
|
||||||
success ? res.sendStatus(202) : res.status(400).send({ message: 'Invalid event index' });
|
|
||||||
} else {
|
|
||||||
res.status(400).send({ message: 'Invalid event index' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status(400).send({ message: 'No event given' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/unload'
|
|
||||||
// Unloads any events
|
|
||||||
export const pbUnload = async (req, res) => {
|
|
||||||
PlaybackService.stop();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/reload'
|
|
||||||
// Reloads current event
|
|
||||||
export const pbReload = async (req, res) => {
|
|
||||||
PlaybackService.reload();
|
|
||||||
res.sendStatus(202);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||||
|
import { logger } from '../classes/Logger.js';
|
||||||
|
import { LogOrigin } from 'ontime-types';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
const helloMessage = 'You have reached Ontime API server';
|
||||||
|
|
||||||
|
// create route between controller and '/api/' endpoint
|
||||||
|
router.get('/', (_req, res) => {
|
||||||
|
res.status(200).json({ message: helloMessage });
|
||||||
|
});
|
||||||
|
|
||||||
|
// any GET request in /api is sent to the integration controller
|
||||||
|
router.get('/*', (req, res) => {
|
||||||
|
const action = req.path.substring(1);
|
||||||
|
const params = { payload: req.query };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reply = dispatchFromAdapter(action, params, 'http');
|
||||||
|
res.status(202).json(reply);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(LogOrigin.Rx, `HTTP IN: ${error}`);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import {
|
|
||||||
pbGet,
|
|
||||||
pbLoad,
|
|
||||||
pbNext,
|
|
||||||
pbPause,
|
|
||||||
pbPrevious,
|
|
||||||
pbReload,
|
|
||||||
pbRoll,
|
|
||||||
pbStart,
|
|
||||||
pbStop,
|
|
||||||
pbUnload,
|
|
||||||
} from '../controllers/playbackController.js';
|
|
||||||
|
|
||||||
export const router = express.Router();
|
|
||||||
|
|
||||||
// create route between controller and '/playback/' endpoint
|
|
||||||
router.get('/', pbGet);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/start' endpoint
|
|
||||||
router.post('/start', pbStart);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/pause' endpoint
|
|
||||||
router.post('/pause', pbPause);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/stop' endpoint
|
|
||||||
router.post('/stop', pbStop);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/roll' endpoint
|
|
||||||
router.post('/roll', pbRoll);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/previous' endpoint
|
|
||||||
router.post('/previous', pbPrevious);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/next' endpoint
|
|
||||||
router.post('/next', pbNext);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/load' endpoint
|
|
||||||
router.post('/load', pbLoad);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/unload' endpoint
|
|
||||||
router.post('/unload', pbUnload);
|
|
||||||
|
|
||||||
// create route between controller and '/playback/reload' endpoint
|
|
||||||
router.post('/reload', pbReload);
|
|
||||||
|
|
||||||
// router.post('*', (req, res) => res.return(404))
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { Message } from 'ontime-types';
|
import { Message, TimerMessage } from 'ontime-types';
|
||||||
|
|
||||||
import { TimerMessage } from 'ontime-types/src/definitions/runtime/MessageControl.type.js';
|
|
||||||
import { throttle } from '../../utils/throttle.js';
|
import { throttle } from '../../utils/throttle.js';
|
||||||
|
|
||||||
import type { PublishFn } from '../../stores/EventStore.js';
|
import type { PublishFn } from '../../stores/EventStore.js';
|
||||||
@@ -12,7 +10,6 @@ class MessageService {
|
|||||||
publicMessage: Message;
|
publicMessage: Message;
|
||||||
lowerMessage: Message;
|
lowerMessage: Message;
|
||||||
externalMessage: Message;
|
externalMessage: Message;
|
||||||
onAir: boolean;
|
|
||||||
|
|
||||||
private throttledSet: PublishFn;
|
private throttledSet: PublishFn;
|
||||||
private publish: PublishFn | null;
|
private publish: PublishFn | null;
|
||||||
@@ -47,7 +44,6 @@ class MessageService {
|
|||||||
visible: false,
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onAir = false;
|
|
||||||
this.throttledSet = () => {
|
this.throttledSet = () => {
|
||||||
throw new Error('Published called before initialisation');
|
throw new Error('Published called before initialisation');
|
||||||
};
|
};
|
||||||
@@ -78,6 +74,15 @@ class MessageService {
|
|||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description patches the TimerMessage object
|
||||||
|
*/
|
||||||
|
setTimerMessage(payload: Partial<TimerMessage>) {
|
||||||
|
this.timerMessage = { ...this.timerMessage, ...payload };
|
||||||
|
this.throttledSet('timerMessage', this.timerMessage);
|
||||||
|
return this.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description sets message on stage timer screen
|
* @description sets message on stage timer screen
|
||||||
*/
|
*/
|
||||||
@@ -132,19 +137,6 @@ class MessageService {
|
|||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description set state of onAir, toggles if parameters are offered
|
|
||||||
*/
|
|
||||||
setOnAir(status?: boolean) {
|
|
||||||
if (typeof status === 'undefined') {
|
|
||||||
this.onAir = !this.onAir;
|
|
||||||
} else {
|
|
||||||
this.onAir = status;
|
|
||||||
}
|
|
||||||
this.throttledSet('onAir', this.onAir);
|
|
||||||
return this.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description set state of timer blink, toggles if parameters are offered
|
* @description set state of timer blink, toggles if parameters are offered
|
||||||
*/
|
*/
|
||||||
@@ -181,9 +173,25 @@ class MessageService {
|
|||||||
timerMessage: this.timerMessage,
|
timerMessage: this.timerMessage,
|
||||||
publicMessage: this.publicMessage,
|
publicMessage: this.publicMessage,
|
||||||
lowerMessage: this.lowerMessage,
|
lowerMessage: this.lowerMessage,
|
||||||
onAir: this.onAir,
|
externalMessage: this.externalMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const messageService = new MessageService();
|
export const messageService = new MessageService();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts whether an object is a valid TimerMessage patch
|
||||||
|
* @param obj - object to evaluate
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export function isPartialTimerMessage(obj: any): obj is Partial<TimerMessage> {
|
||||||
|
return (
|
||||||
|
obj &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
(typeof obj.text === 'string' || obj.text === undefined) &&
|
||||||
|
(typeof obj.visible === 'boolean' || obj.visible === undefined) &&
|
||||||
|
(typeof obj.timerBlink === 'boolean' || obj.timerBlink === undefined) &&
|
||||||
|
(typeof obj.timerBlackout === 'boolean' || obj.timerBlackout === undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RuntimeStore } from 'ontime-types';
|
import { Playback, RuntimeStore } from 'ontime-types';
|
||||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
import { eventTimer } from '../services/TimerService.js';
|
import { eventTimer } from '../services/TimerService.js';
|
||||||
import { messageService } from '../services/message-service/MessageService.js';
|
import { messageService } from '../services/message-service/MessageService.js';
|
||||||
@@ -53,10 +53,10 @@ export const eventStore = {
|
|||||||
* Currently registered objects in store
|
* Currently registered objects in store
|
||||||
* - Timer Service timer
|
* - Timer Service timer
|
||||||
* - Timer Service playback
|
* - Timer Service playback
|
||||||
|
* - Timer Service onAir
|
||||||
* - Message Service timerMessage
|
* - Message Service timerMessage
|
||||||
* - Message Service publicMessage
|
* - Message Service publicMessage
|
||||||
* - Message Service lowerMessage
|
* - Message Service lowerMessage
|
||||||
* - Message Service onAir
|
|
||||||
* - Event Loader loaded
|
* - Event Loader loaded
|
||||||
* - Event Loader eventNow
|
* - Event Loader eventNow
|
||||||
* - Event Loader publicEventNow
|
* - Event Loader publicEventNow
|
||||||
@@ -67,11 +67,11 @@ export const eventStore = {
|
|||||||
export const getInitialPayload = () => ({
|
export const getInitialPayload = () => ({
|
||||||
timer: eventTimer.timer,
|
timer: eventTimer.timer,
|
||||||
playback: eventTimer.playback,
|
playback: eventTimer.playback,
|
||||||
|
onAir: eventTimer.playback !== Playback.Stop,
|
||||||
timerMessage: messageService.timerMessage,
|
timerMessage: messageService.timerMessage,
|
||||||
publicMessage: messageService.publicMessage,
|
publicMessage: messageService.publicMessage,
|
||||||
lowerMessage: messageService.lowerMessage,
|
lowerMessage: messageService.lowerMessage,
|
||||||
externalMessage: messageService.externalMessage,
|
externalMessage: messageService.externalMessage,
|
||||||
onAir: messageService.onAir,
|
|
||||||
loaded: eventLoader.loaded,
|
loaded: eventLoader.loaded,
|
||||||
eventNow: eventLoader.eventNow,
|
eventNow: eventLoader.eventNow,
|
||||||
publicEventNow: eventLoader.publicEventNow,
|
publicEventNow: eventLoader.publicEventNow,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function isString(value: unknown): asserts value is string {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`Unexpected payload type: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDefined<T>(value: T | undefined): asserts value is T {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error('Payload not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNumber(value: unknown): asserts value is number {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`Unexpected payload type: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
|
|
||||||
test('message control sends messages to screens', async ({ context }) => {
|
test('message control sends messages to screens', async ({ context }) => {
|
||||||
const editorPage = await context.newPage();
|
const editorPage = await context.newPage();
|
||||||
@@ -32,12 +32,4 @@ test('message control sends messages to screens', async ({ context }) => {
|
|||||||
await featurePage.goto('http://localhost:4001/timer');
|
await featurePage.goto('http://localhost:4001/timer');
|
||||||
await featurePage.waitForLoadState('load', { timeout: 5000 });
|
await featurePage.waitForLoadState('load', { timeout: 5000 });
|
||||||
await featurePage.getByText('testing stage').click();
|
await featurePage.getByText('testing stage').click();
|
||||||
|
|
||||||
// on air state
|
|
||||||
await editorPage.getByTestId('toggle on air').click();
|
|
||||||
|
|
||||||
await featurePage.goto('http://localhost:4001/studio');
|
|
||||||
await featurePage.getByText('ON AIR').click();
|
|
||||||
const onAirActive = await featurePage.locator('data-testid=on-air-enabled');
|
|
||||||
await expect(onAirActive).toHaveText(/on air/i);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user