Files
status-indicators/atem_watch.js
T

115 lines
4.9 KiB
JavaScript
Raw Normal View History

// 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 <switcher ip> <path to atem-connection> <reference.json>
//
// 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)