import { ArgumentType, Client, Message } from 'node-osc'; import { LogOrigin, MaybeNumber, MaybeString, OSCSettings, OscSubscription } from 'ontime-types'; import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; import { parseTemplateNested } from './integrationUtils.js'; import { logger } from '../../classes/Logger.js'; import { OscServer } from '../../adapters/OscAdapter.js'; import { stringToOSCArgs } from '../../utils/oscArgParser.js'; /** * @description Class contains logic towards outgoing OSC communications * @class */ export class OscIntegration implements IIntegration { protected oscClient: null | Client; protected oscServer: OscServer | null = null; subscriptions: OscSubscription[]; targetIP: MaybeString; portOut: MaybeNumber; portIn: MaybeNumber; enabledOut: boolean; enabledIn: boolean; constructor() { this.oscClient = null; this.subscriptions = []; this.targetIP = null; this.portOut = null; this.portIn = null; this.enabledOut = false; this.enabledIn = false; } /** * Initializes oscClient */ init(config: OSCSettings) { const { targetIP, portOut, subscriptions, enabledOut, enabledIn, portIn } = config; this.initTX(enabledOut, targetIP, portOut, subscriptions); this.initRX(enabledIn, portIn); // return `OSC integration client connected to ${targetIP}:${portOut}`; } private initSubscriptions(subscriptions: OscSubscription[]) { this.subscriptions = subscriptions; } dispatch(action: TimerLifeCycleKey, state?: object) { // noop if (!this.oscClient || !this.enabledOut) { return; } for (let i = 0; i < this.subscriptions.length; i++) { const { cycle, address, payload, enabled } = this.subscriptions[i]; if (cycle !== action || !enabled || !address) { continue; } const parsedAddress = parseTemplateNested(address, state || {}); const parsedPayload = payload ? parseTemplateNested(payload, state || {}) : undefined; const parsedArguments = stringToOSCArgs(parsedPayload); try { this.emit(parsedAddress, parsedArguments); } catch (error) { logger.error(LogOrigin.Tx, `OSC Integration: ${error}`); } } } emit(address: string, args: ArgumentType[]) { if (!this.oscClient) { return; } //TODO: Look into using bundles const message = new Message(address); message.append(args); this.oscClient.send(message); } private initTX(enabledOut: boolean, targetIP: string, portOut: number, subscriptions: OscSubscription[]) { this.initSubscriptions(subscriptions); if (!enabledOut && this.enabledOut) { this.targetIP = targetIP; this.portOut = portOut; this.enabledOut = enabledOut; this.shutdownTX(); return; } if (this.oscClient && targetIP === this.targetIP && portOut === this.portOut) { // nothing changed that would mean we need a new client return; } this.targetIP = targetIP; this.portOut = portOut; this.enabledOut = enabledOut; try { this.oscClient = new Client(targetIP, portOut); } catch (error) { this.oscClient = null; throw new Error(`Failed initialising OSC client: ${error}`); } } private initRX(enabledIn: boolean, portIn: number) { if (!enabledIn && this.enabledIn) { this.shutdownRX(); return; } // Start OSC Server logger.info(LogOrigin.Rx, `Starting OSC Server on port: ${portIn}`); this.oscServer = new OscServer(portIn); } shutdown() { this.shutdownTX(); this.shutdownRX(); } private shutdownTX() { logger.info(LogOrigin.Rx, 'Shutting down OSC integration'); if (this.oscServer) { this.oscServer?.shutdown(); this.oscServer = null; } } private shutdownRX() { logger.info(LogOrigin.Tx, 'Shutting down OSC integration'); if (this.oscClient) { this.oscClient?.close(); this.oscClient = null; } } } export const oscIntegration = new OscIntegration();