UNPKG

timeline-state-resolver

Version:
341 lines • 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionManager = void 0; const timeline_state_resolver_types_1 = require("timeline-state-resolver-types"); const remoteDeviceInstance_1 = require("./remoteDeviceInstance"); const _ = require("underscore"); const deviceContainer_1 = require("..//devices/deviceContainer"); const atemUtil_1 = require("atem-connection/dist/lib/atemUtil"); const eventemitter3_1 = require("eventemitter3"); const lib_1 = require("../lib"); const FREEZE_LIMIT = 5000; // how long to wait before considering the child to be unresponsive class ConnectionManager extends eventemitter3_1.EventEmitter { constructor() { super(...arguments); this._config = new Map(); this._connections = new Map(); this._updating = false; this._connectionAttempts = new Map(); } /** * Set the config options for all connections */ setConnections(connectionsConfig) { // run through and see if we need to reset any of the counters this._config.forEach((conf, id) => { const newConf = connectionsConfig[id]; if (newConf && configHasChanged(conf, newConf)) { // new conf warrants an immediate retry this._connectionAttempts.delete(id); } }); this._config = new Map(Object.entries(connectionsConfig)); this._updateConnections(); } getConnections(includeUninitialized = false) { if (includeUninitialized) { return Array.from(this._connections.values()); } else { return Array.from(this._connections.values()).filter((conn) => conn.initialized === true); } } getConnection(connectionId, includeUninitialized = false) { if (includeUninitialized) { return this._connections.get(connectionId); } else { const connection = this._connections.get(connectionId); if (connection?.initialized === true) { return connection; } else { return undefined; } } } /** * Iterate over config and check that the existing connection has the right config, if * not... recreate it */ _updateConnections() { if (this._updating) return; this._updating = true; if (this._nextAttempt) { clearTimeout(this._nextAttempt); this._nextAttempt = undefined; } const operations = []; for (const [deviceId, config] of this._config.entries()) { // find connection const connection = this._connections.get(deviceId); if (connection) { // see if it should be restarted because of an update if (connectionConfigHasChanged(connection, config)) { operations.push({ operation: 'update', id: deviceId }); } else if (connection.deviceOptions.debug !== config.debug || connection.deviceOptions.debugState !== config.debugState) { // see if we should set the debug params operations.push({ operation: 'setDebug', id: deviceId }); } } else { // create operations.push({ operation: 'create', id: deviceId }); } } for (const deviceId of this._connections.keys()) { // find if still in config const config = this._config.get(deviceId); if (!config) { // not found, so it should be closed operations.push({ operation: 'delete', id: deviceId }); } } const isAllowedOp = (op) => { if (op.operation !== 'create') return true; // allow non-create ops const nextCreate = this._connectionAttempts.get(op.id); if (!nextCreate || nextCreate.next < Date.now()) return true; return false; }; const allowedOperations = operations.filter(isAllowedOp); if (operations.length === 0) { // no operations needed, so stop the loop this._updating = false; return; } else if (allowedOperations.length === 0) { this._updating = false; // wait until next const nextTime = Array.from(this._connectionAttempts.values()).reduce((a, b) => (a.next < b.next ? a : b), { last: Date.now(), next: Date.now() + 4000, // in 4 seconds }); this._nextAttempt = setTimeout(() => { this._updateConnections(); }, nextTime.next - Date.now()); // there's nothing to execute right now so return return; } Promise.allSettled(allowedOperations.map(async (op) => this.executeOperation(op))) .then(() => { this._updating = false; // rerun the algorithm once to make sure we have no missed operations in the meanwhile this._updateConnections(); }) .catch((e) => { this.emit('warning', 'Error encountered while updating connections: ' + e); }); } async executeOperation({ operation, id }) { try { switch (operation) { case 'create': await this.createConnection(id); break; case 'delete': await this.deleteConnection(id); break; case 'update': await this.deleteConnection(id); await this.createConnection(id); break; case 'setDebug': await this.setDebugForConnection(id); break; } } catch { this.emit('warning', `Failed to execute "${operation} for ${id}"`); } } async createConnection(id) { const deviceOptions = this._config.get(id); if (!deviceOptions) return; // has been removed since, so do not create const lastAttempt = this._connectionAttempts.get(id); const last = lastAttempt?.last ?? Date.now(); this._connectionAttempts.set(id, { last: Date.now(), next: Date.now() + Math.min(Math.max(Date.now() - last, 2000) * 2, 60 * 1000), }); // first retry after 4secs, double it every try, max 60s const threadedClassOptions = { threadUsage: deviceOptions.threadUsage || 1, autoRestart: false, disableMultithreading: !deviceOptions.isMultiThreaded, instanceName: id, freezeLimit: FREEZE_LIMIT, }; const container = await createContainer(deviceOptions, id, () => Date.now(), threadedClassOptions); // we rely on threadedclass to timeout if this fails if (!container) { this.emit('warning', 'Failed to create container for ' + id); return; } // set up event handlers await this._setupDeviceListeners(id, container); container.onChildClose = () => { this.emit('error', 'Connection ' + id + ' closed'); this._connections.delete(id); this.emit('connectionRemoved', id); container .terminate() .catch((e) => this.emit('warning', `Failed to initialise ${id} (${e})`)) .finally(() => { this._updateConnections(); }); }; this._connections.set(id, container); this.emit('connectionAdded', id, container); // trigger connection init this._handleConnectionInitialisation(id, container) .then(() => { this._connectionAttempts.delete(id); this.emit('connectionInitialised', id); }) .catch((e) => { this.emit('error', 'Connection ' + id + ' failed to initialise', e); this._connections.delete(id); this.emit('connectionRemoved', id); container .terminate() .catch((e) => this.emit('warning', `Failed to initialise ${id} (${e})`)) .finally(() => { this._updateConnections(); }); }); } async deleteConnection(id) { const connection = this._connections.get(id); if (!connection) return Promise.resolve(); // already removed / never existed this._connections.delete(id); this.emit('connectionRemoved', id); return new Promise((resolve) => (0, lib_1.deferAsync)(async () => { let finished = false; setTimeout(() => { if (!finished) { resolve(); this.emit('warning', 'Failed to delete connection in time'); connection.terminate().catch((e) => this.emit('error', 'Failed to terminate connection: ' + e)); } }, 30000); try { await connection.device.terminate(); await connection.device.removeAllListeners(); await connection.terminate(); } catch { await connection.terminate(); } finished = true; resolve(); }, (e) => { this.emit('warning', 'Error encountered trying to delete connection: ' + e); })); } async setDebugForConnection(id) { const config = this._config.get(id); const connection = this._connections.get(id); if (!connection || !config) return; try { await connection.device.setDebugLogging(config.debug ?? false); await connection.device.setDebugState(config.debugState ?? false); } catch { this.emit('warning', 'Failed to update debug values for ' + id); } } async _handleConnectionInitialisation(id, container) { const deviceOptions = this._config.get(id); if (!deviceOptions) return; // if the config has been removed, the connection should be removed as well so no need to init this.emit('info', `Initializing connection ${id} (${container.instanceId}) of type ${timeline_state_resolver_types_1.DeviceType[deviceOptions.type]}...`); await container.init(deviceOptions.options, undefined); await container.reloadProps(); this.emit('info', `Connection ${id} (${container.instanceId}) initialized!`); } async _setupDeviceListeners(id, container) { const passEvent = (ev) => { const evHandler = (...args) => this.emit(('connectionEvent:' + ev), id, ...args); container.device .on(ev, evHandler) .catch((e) => this.emit('error', 'Failed to attach listener for device: ' + id + ' ' + ev, e)); }; passEvent('info'); passEvent('warning'); passEvent('error'); passEvent('debug'); passEvent('debugState'); passEvent('connectionChanged'); passEvent('resetResolver'); passEvent('slowCommand'); passEvent('slowSentCommand'); passEvent('slowFulfilledCommand'); passEvent('commandReport'); passEvent('commandError'); passEvent('updateMediaObject'); passEvent('clearMediaObjects'); passEvent('timeTrace'); } } exports.ConnectionManager = ConnectionManager; /** * A config has changed if any of the options are no longer the same, taking default values into * consideration. In addition, the debug logging flag should be ignored as that can be changed at runtime. */ function connectionConfigHasChanged(connection, config) { const oldConfig = connection.deviceOptions; // now check device specific options return configHasChanged(oldConfig, config); } function configHasChanged(oldConfig, config) { // now check device specific options return !_.isEqual(_.omit(oldConfig, 'debug', 'debugState'), _.omit(config, 'debug', 'debugState')); } function createContainer(deviceOptions, deviceId, getCurrentTime, threadedClassOptions) { switch (deviceOptions.type) { case timeline_state_resolver_types_1.DeviceType.CASPARCG: return deviceContainer_1.DeviceContainer.create('../../dist/integrations/casparCG/index.js', 'CasparCGDevice', deviceId, deviceOptions, getCurrentTime, threadedClassOptions); case timeline_state_resolver_types_1.DeviceType.SISYFOS: return deviceContainer_1.DeviceContainer.create('../../dist/integrations/sisyfos/index.js', 'SisyfosMessageDevice', deviceId, deviceOptions, getCurrentTime, threadedClassOptions); case timeline_state_resolver_types_1.DeviceType.VIZMSE: return deviceContainer_1.DeviceContainer.create('../../dist/integrations/vizMSE/index.js', 'VizMSEDevice', deviceId, deviceOptions, getCurrentTime, threadedClassOptions); case timeline_state_resolver_types_1.DeviceType.VMIX: return deviceContainer_1.DeviceContainer.create('../../dist/integrations/vmix/index.js', 'VMixDevice', deviceId, deviceOptions, getCurrentTime, threadedClassOptions); case timeline_state_resolver_types_1.DeviceType.SINGULAR_LIVE: case timeline_state_resolver_types_1.DeviceType.TELEMETRICS: case timeline_state_resolver_types_1.DeviceType.PHAROS: case timeline_state_resolver_types_1.DeviceType.ABSTRACT: case timeline_state_resolver_types_1.DeviceType.ATEM: case timeline_state_resolver_types_1.DeviceType.HTTPSEND: case timeline_state_resolver_types_1.DeviceType.HTTPWATCHER: case timeline_state_resolver_types_1.DeviceType.HYPERDECK: case timeline_state_resolver_types_1.DeviceType.LAWO: case timeline_state_resolver_types_1.DeviceType.MULTI_OSC: case timeline_state_resolver_types_1.DeviceType.OBS: case timeline_state_resolver_types_1.DeviceType.OSC: case timeline_state_resolver_types_1.DeviceType.PANASONIC_PTZ: case timeline_state_resolver_types_1.DeviceType.SHOTOKU: case timeline_state_resolver_types_1.DeviceType.SOFIE_CHEF: case timeline_state_resolver_types_1.DeviceType.TCPSEND: case timeline_state_resolver_types_1.DeviceType.TRICASTER: case timeline_state_resolver_types_1.DeviceType.VISCA_OVER_IP: case timeline_state_resolver_types_1.DeviceType.QUANTEL: { ensureIsImplementedAsService(deviceOptions.type); // presumably this device is implemented in the new service handler return remoteDeviceInstance_1.RemoteDeviceInstance.create(deviceId, deviceOptions, getCurrentTime, threadedClassOptions); } default: (0, atemUtil_1.assertNever)(deviceOptions); return null; } } function ensureIsImplementedAsService(_type) { // This is a type check } //# sourceMappingURL=ConnectionManager.js.map