UNPKG

vtally

Version:

An affordable and reliable Tally Light that works via WiFi based on NodeMCU / ESP8266. Supports multiple video mixers.

204 lines (203 loc) 8.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const net_1 = __importDefault(require("net")); const xml2js_1 = __importDefault(require("xml2js")); const VmixConfiguration_1 = __importDefault(require("./VmixConfiguration")); // @see https://www.vmix.com/help20/index.htm?TCPAPI.html class VmixConnector { constructor(configuration, communicator) { this.configuration = configuration; this.communicator = communicator; this.wasHelloReceived = false; this.wasSubcribeOkReceived = false; this.xmlQueryInterval = 5000; this.waitForHelloPeriod = 5000; } connect() { const client = new net_1.default.Socket(); this.client = client; const connectClient = () => { this.wasHelloReceived = false; this.wasSubcribeOkReceived = false; console.log(`Connecting to Vmix at ${this.configuration.getIp().toString()}:${this.configuration.getPort().toNumber()}`); client.connect(this.configuration.getPort().toNumber(), this.configuration.getIp().toString()); }; const reconnectClient = () => { this.disconnect().then(() => this.reconnectTimeout = setTimeout(() => { if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); } client.connect(this.configuration.getPort().toNumber(), this.configuration.getIp().toString()); }, 200)); }; const queryXml = () => { if (!client.connecting && !client.destroyed) { client.write("XML\r\n"); } }; connectClient(); client.on("connect", () => { console.debug(`TCP connection to ${this.configuration.getIp().toString()}:${this.configuration.getPort().toNumber()} established`); this.waitForHelloTimeout = setTimeout(() => { if (this.waitForHelloTimeout) { clearTimeout(this.waitForHelloTimeout); } if (!this.wasHelloReceived || !this.wasSubcribeOkReceived) { reconnectClient(); console.error(`The remote at ${this.configuration.getIp().toString()}:${this.configuration.getPort().toNumber()} did not identify as vMix TCPAPI. Is this the correct port for the TCPAPI? (default ${VmixConfiguration_1.default.defaultPort})`); } }, this.waitForHelloPeriod); }); client.on("ready", () => { client.write("SUBSCRIBE TALLY\r\n"); // @TODO: we need to poll for new channels or renames. Is there a way to subscribe to those? this.intervalHandle = setInterval(queryXml, this.xmlQueryInterval); queryXml(); }); client.on("timeout", () => { console.error("Connection to vMix timed out"); }); client.on("error", error => { console.error(`${error.name}: ${error.message}`); }); client.on('data', this.onData.bind(this)); client.on('close', (hadError) => { this.communicator.notifyMixerIsDisconnected(); console.log("Connection to vMix closed"); if (this.intervalHandle) { clearInterval(this.intervalHandle); this.intervalHandle = undefined; } if (hadError) { console.debug("Connection to vMix is reconnected after an error"); reconnectClient(); } }); } onConnectionComplete() { console.log("Connection to vMix complete"); this.communicator.notifyMixerIsConnected(); } onData(data) { data.toString().replace(/[\r\n]*$/, "").split("\r\n").forEach(command => { console.debug(`> ${command}`); if (command.startsWith("VERSION OK")) { this.wasHelloReceived = true; console.debug("Connection to vMix established"); if (this.wasHelloReceived && this.wasSubcribeOkReceived) { this.onConnectionComplete(); } } else if (command.startsWith("SUBSCRIBE OK TALLY")) { this.wasSubcribeOkReceived = true; console.debug("Successfully subscribed to tally updates from vMix"); if (this.wasHelloReceived && this.wasSubcribeOkReceived) { this.onConnectionComplete(); } } else if (command.startsWith("TALLY OK")) { this.handleTallyCommand(command); } else if (command.startsWith("XML ")) { // @TODO: it would be better to detect the "XML" response itself, not the payload } else if (command.startsWith("<vmix>")) { this.handleXmlCommand(command); } else { console.debug("Ignoring unkown command from vmix"); } }, this); } handleTallyCommand(command) { const result = command.match(/^TALLY OK (\d*)$/); if (result === null) { console.error("Tally OK command was ill formed"); } else { const state = result[1]; let programs = []; let previews = []; // vMix encodes tally states as numbers: // @see https://www.vmix.com/help20/index.htm?TCPAPI.html // 0 = off // 1 = program // 2 = preview state.split('').forEach((val, idx) => { if (val === "1") { programs.push(`${idx + 1}`); } else if (val === "2") { previews.push(`${idx + 1}`); } }); this.communicator.notifyProgramPreviewChanged(programs, previews); } } handleXmlCommand(command) { xml2js_1.default.parseString(command, (error, result) => { if (error) { console.error(`Error parsing XML response from vMix: ${error}`); } else { const inputs = (result.vmix || {}).inputs; if (inputs === undefined) { console.log("XML from vMix looks faulty. Could not find inputs."); } else { const count = inputs[0].input.length; const names = inputs[0].input.reduce((map, input, idx) => { map[idx + 1] = input.$.shortTitle; return map; }, {}); this.communicator.notifyChannelNames(count, names); } } }); } disconnect() { this.wasHelloReceived = false; this.wasSubcribeOkReceived = false; const promise = new Promise(resolve => { if (this.intervalHandle) { clearInterval(this.intervalHandle); this.intervalHandle = undefined; } if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = undefined; } if (this.waitForHelloTimeout) { clearTimeout(this.waitForHelloTimeout); this.waitForHelloTimeout = undefined; } if (this.client && !this.client.destroyed) { // @TODO: check if client is still connected and disconnect gracefully // if (this.client.isConnected) { // // if we are connected: try to be nice // this.client.end(() => { // console.log("Disconnected from vMix") // resolve(null) // }) // } else { // if not: be rude this.client.destroy(); resolve(null); // } } else { resolve(null); } }); this.client = undefined; return promise; } isConnected() { return this.client !== undefined && !this.client.destroyed && this.wasHelloReceived && this.wasSubcribeOkReceived; } } VmixConnector.ID = "vmix"; exports.default = VmixConnector;