// Status panel: keep a connection to the ATEM switcher and compare its audio (Fairlight) and camera // control settings with the restore file, every second. Run by status_panel.py with Companion's node: // // node atem_watch.js // // reference.json (made by status_panel.py from the ATEM Software Control restore .xml): // {"audio": {"16/-65280": {"mixOption": 2, "faderGain": 0}, ...}, // "camera": {"2/1/2": [4500, 0], ...}} // camera/category/parameter -> values // Prints a line "ATEM {json}" when the differences change (and every 10 s): // {"connected": true, "audio": ["16 Resi..: Off, should be On", ...], "camera": [...]} // Reads lines on stdin: "fix audio" / "fix camera" sends back only the settings that differ. // Nothing else is ever sent to the switcher. const [ip, lib, refPath] = process.argv.slice(2) const fs = require('fs') const readline = require('readline') const { Atem, Commands } = require(lib) const ref = JSON.parse(fs.readFileSync(refPath, 'utf8')) const MIX = { 1: 'Off', 2: 'On', 4: 'AFV' } const ccu = {} // "camera/category/parameter" -> {type, values} const atem = new Atem({ disableMultithreaded: true }) let connected = false atem.on('receivedCommands', (cmds) => { for (const c of cmds) { if (c.constructor && c.constructor.rawName === 'CCdP') { const p = c.properties const values = p.numberData && p.numberData.length ? p.numberData : p.bigintData && p.bigintData.length ? p.bigintData.map(Number) : p.boolData && p.boolData.length ? p.boolData.map(Number) : [] ccu[`${c.source}/${c.category}/${c.parameter}`] = { type: p.type, values } } } }) atem.on('connected', () => { connected = true }) atem.on('disconnected', () => { connected = false }) atem.on('error', () => {}) const close = (a, b) => Math.abs(a - b) < 0.002 function audioDiffs() { const out = [] const fl = (atem.state && atem.state.fairlight && atem.state.fairlight.inputs) || {} for (const [key, want] of Object.entries(ref.audio || {})) { const [input, source] = key.split('/') const src = fl[input] && fl[input].sources && fl[input].sources[source] if (!src || !src.properties) continue const have = src.properties if (have.mixOption !== want.mixOption || (want.faderGain !== undefined && have.faderGain !== want.faderGain)) { out.push({ key, input: Number(input), source, want, have: { mixOption: have.mixOption, faderGain: have.faderGain }, text: `input ${input}: ${MIX[have.mixOption] || have.mixOption}, should be ${MIX[want.mixOption] || want.mixOption}` + (have.faderGain !== want.faderGain ? ` (fader ${have.faderGain / 100} dB, should be ${want.faderGain / 100})` : '') }) } } return out } function cameraDiffs() { const out = [] for (const [key, want] of Object.entries(ref.camera || {})) { const have = ccu[key] if (!have || have.values.length !== want.length) continue if (!want.every((w, i) => close(w, have.values[i]))) { const [cam] = key.split('/') out.push({ key, want, have: have.values, type: have.type, text: `camera ${cam} ${key}: ${have.values.join(',')} should be ${want.join(',')}` }) } } return out } let last = '', lastAt = 0 function report() { const state = { connected, audio: connected ? audioDiffs().map((d) => d.text) : [], camera: connected ? cameraDiffs().map((d) => d.text) : [], camerasSeen: Object.keys(ccu).length } const s = JSON.stringify(state) if (s !== last || Date.now() - lastAt > 10000) { process.stdout.write('ATEM ' + s + '\n') last = s lastAt = Date.now() } } async function fix(part) { if (!connected) return process.stdout.write('FIXED ' + JSON.stringify({ part, sent: 0, error: 'not connected' }) + '\n') let sent = 0 if (part === 'audio') { for (const d of audioDiffs()) { const props = { mixOption: d.want.mixOption } if (d.want.faderGain !== undefined) props.faderGain = d.want.faderGain await atem.setFairlightAudioMixerSourceProps(d.input, BigInt(d.source), props) sent++ } } else if (part === 'camera') { for (const d of cameraDiffs()) { const [source, category, parameter] = d.key.split('/').map(Number) const isFloat = d.type === 128 const cmd = new Commands.CameraControlCommand(source, category, parameter, { type: d.type, relative: false, boolData: [], stringData: '', bigintData: [], numberData: d.want.map((v) => (isFloat ? v : Math.round(v))), }) await atem.sendCommand(cmd) sent++ } } process.stdout.write('FIXED ' + JSON.stringify({ part, sent }) + '\n') } readline.createInterface({ input: process.stdin }).on('line', (line) => { const [verb, part] = line.trim().split(/\s+/) if (verb === 'fix') fix(part).catch((e) => process.stdout.write('FIXED ' + JSON.stringify({ part, error: String(e) }) + '\n')) }).on('close', () => { atem.disconnect().finally(() => process.exit(0)); setTimeout(() => process.exit(0), 2000) }) setInterval(report, 1000) atem.connect(ip)