UNPKG

timeline-state-resolver

Version:
252 lines • 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OscDevice = void 0; const timeline_state_resolver_types_1 = require("timeline-state-resolver-types"); const device_1 = require("../../service/device"); const osc = require("osc"); const debug_1 = require("debug"); const _ = require("underscore"); const easings_1 = require("../../devices/transitions/easings"); const lib_1 = require("../../lib"); const debug = (0, debug_1.default)('timeline-state-resolver:osc'); class OscDevice extends device_1.Device { constructor() { super(...arguments); this._oscClientStatus = 'disconnected'; this.transitions = {}; this.actions = {}; } async init(options) { this.options = options; if (options.type === timeline_state_resolver_types_1.OSCDeviceType.TCP) { debug('Creating TCP OSC device'); const client = new osc.TCPSocketPort({ address: options.host, port: options.port, metadata: true, }); this._oscClient = client; client.open(); // creates client.socket let firstConnect = true; client.socket.on('connect', () => { this._oscClientStatus = 'connected'; if (firstConnect) { // note - perhaps we could resend the commands every time we reconnect? or that could be a device option firstConnect = false; this.context.connectionChanged(this.getStatus()); this.context .resetToState({}) .catch((e) => this.context.logger.warning('Failed to reset to state after first connection, device may be in unknown state (reason: ' + e + ')')); } }); client.socket.on('close', () => { this._oscClientStatus = 'disconnected'; this.context.connectionChanged(this.getStatus()); }); } else if (options.type === timeline_state_resolver_types_1.OSCDeviceType.UDP) { debug('Creating UDP OSC device'); this._oscClient = new osc.UDPPort({ localAddress: '0.0.0.0', localPort: 0, remoteAddress: options.host, remotePort: options.port, metadata: true, }); this._oscClient.once('ready', () => { this.context .resetToState({}) .catch((e) => this.context.logger.warning('Failed to reset to state after first connection, device may be in unknown state (reason: ' + e + ')')); }); this._oscClient.open(); } else { (0, lib_1.assertNever)(options.type); throw new Error(`Unknown device transport type: ${options.type}`); } return Promise.resolve(true); // This device doesn't have any initialization procedure } async terminate() { this._oscClient.close(); this._oscClient.removeAllListeners(); } convertTimelineStateToDeviceState(state) { const addrToOSCMessage = {}; const addrToPriority = {}; Object.values(state.layers).forEach((layer) => { if (layer.content.deviceType === timeline_state_resolver_types_1.DeviceType.OSC) { const content = { ...layer.content, fromTlObject: layer.id, }; if ((addrToOSCMessage[content.path] && addrToPriority[content.path] <= (layer.priority || 0)) || !addrToOSCMessage[content.path]) { addrToOSCMessage[content.path] = content; addrToPriority[content.path] = layer.priority || 0; } } }); return addrToOSCMessage; } diffStates(oldState, newState) { const commands = []; Object.entries(newState).forEach(([address, newCommandContent]) => { const oldLayer = oldState?.[address]; if (!oldLayer) { // added! commands.push({ context: `added: ${newCommandContent.fromTlObject}`, timelineObjId: newCommandContent.fromTlObject, command: newCommandContent, }); } else { // changed? if (!_.isEqual(oldLayer, newCommandContent)) { // changed! commands.push({ context: `changed: ${newCommandContent.fromTlObject}`, timelineObjId: newCommandContent.fromTlObject, command: newCommandContent, }); } } }); return commands; } async sendCommand({ command, context, timelineObjId }) { const cwc = { context: context, command: command, timelineObjId, }; this.context.logger.debug(cwc); debug(command); try { if (command.transition && command.from) { const easingType = easings_1.Easing[command.transition.type]; const easing = (easingType || {})[command.transition.direction]; if (!easing) throw new Error(`Easing "${command.transition.type}.${command.transition.direction}" not found`); for (let i = 0; i < Math.max(command.from.length, command.values.length); i++) { if (command.from[i] && command.values[i] && 'value' in command.from[i] && 'value' in command.values[i]) { if (command.from[i].value !== command.values[i].value && command.from[i].type !== command.values[i].type) { throw new Error('Cannot interpolate between values of different types'); } } } this.transitions[command.path] = { // push the tween started: this.getMonotonicTime(), ...command, }; this._oscSender({ // send first parameters address: command.path, args: [...command.values].map((o, i) => command.from[i] || o), }); // trigger loop: if (!this.transitionInterval) this.transitionInterval = setInterval(() => this.runAnimation(), 40); } else { this._oscSender({ address: command.path, args: command.values, }); } return Promise.resolve(); } catch (e) { this.context.commandError(e, cwc); return Promise.resolve(); } } get connected() { return this._oscClientStatus === 'connected'; } getStatus() { if (this.options?.type === timeline_state_resolver_types_1.OSCDeviceType.TCP) { return { statusCode: this._oscClientStatus === 'disconnected' ? timeline_state_resolver_types_1.StatusCode.BAD : timeline_state_resolver_types_1.StatusCode.GOOD, messages: this._oscClientStatus === 'disconnected' ? ['Disconnected'] : [], }; } return { statusCode: timeline_state_resolver_types_1.StatusCode.GOOD, messages: [], }; } _oscSender(msg, address, port) { this.context.logger.debug('sending ' + msg.address); this._oscClient.send(msg, address, port); } runAnimation() { const t = this.getMonotonicTime(); for (const addr in this.transitions) { // delete old tweens if (this.transitions[addr].started + this.transitions[addr].transition.duration < t) { delete this.transitions[addr]; } } for (const addr in this.transitions) { const tween = this.transitions[addr]; // check if easing exists: const easingType = easings_1.Easing[tween.transition.type]; const easing = (easingType || {})[tween.transition.direction]; if (easing) { // scale time in range 0...1, then calculate progress in range 0..1 const deltaTime = t - tween.started; const progress = deltaTime / tween.transition.duration; const fraction = easing(progress); // calculate individual values: const values = []; for (let i = 0; i < Math.max(tween.from.length, tween.values.length); i++) { if (!tween.from[i]) { values[i] = tween.values[i]; } else if (!tween.values[i]) { values[i] = tween.from[i]; } else { if (tween.from[i].type === timeline_state_resolver_types_1.OSCValueType.FLOAT && tween.values[i].type === timeline_state_resolver_types_1.OSCValueType.FLOAT) { const oldVal = tween.from[i].value; const newVal = tween.values[i].value; values[i] = { type: timeline_state_resolver_types_1.OSCValueType.FLOAT, value: oldVal + (newVal - oldVal) * fraction, }; } else if (tween.from[i].type === timeline_state_resolver_types_1.OSCValueType.INT && tween.values[i].type === timeline_state_resolver_types_1.OSCValueType.INT) { const oldVal = tween.from[i].value; const newVal = tween.values[i].value; values[i] = { type: timeline_state_resolver_types_1.OSCValueType.INT, value: oldVal + Math.round((newVal - oldVal) * fraction), }; } else { values[i] = tween.values[i]; } } } this._oscSender({ address: tween.path, args: values, }); } } if (Object.keys(this.transitions).length === 0) { if (this.transitionInterval) { clearInterval(this.transitionInterval); } this.transitionInterval = undefined; } } getMonotonicTime() { const hrTime = process.hrtime(); return hrTime[0] * 1000 + hrTime[1] / 1000000; } } exports.OscDevice = OscDevice; //# sourceMappingURL=index.js.map