ATEM: RESI AUDIO SILENT when input 16 has no audio for 30 s; log what the switcher has wrong

2026-10-25 the Resi AJA passed no Dante audio and the panel stayed green (DANTE only sees the card on the
network). atem_watch.js now asks the switcher for its Fairlight meters and reports a metered input
stuck at the floor (-100 dB); the ATEM key turns red in auto-fix's hours or while a deck records.
Differences (audio, camera, silence) are logged when they change, so a fault says what it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 06:18:52 -04:00
co-authored by Claude Opus 5.5
parent 0d655bff3c
commit e5587cb109
6 changed files with 86 additions and 12 deletions
+33 -5
View File
@@ -5,11 +5,14 @@
//
// 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
// "camera": {"2/1/2": [4500, 0], ...}, // camera/category/parameter -> values
// "levels": {"16": "Resi - RX1"}, "silence_seconds": 30} // inputs whose audio must never go silent
// Prints a line "ATEM {json}" when the differences change (and every 10 s):
// {"connected": true, "audio": ["16 Resi..: Off, should be On", ...], "camera": [...]}
// {"connected": true, "audio": ["16 Resi..: Off, should be On", ...], "camera": [...],
// "silence": ["input 16 Resi - RX1: no audio for 30 s"]}
// Reads lines on stdin: "fix audio" / "fix camera" sends back only the settings that differ.
// Nothing else is ever sent to the switcher.
// Besides those fixes, the only thing ever sent is the request for the audio meters (as ATEM Software
// Control's mixer does); nothing on the switcher changes.
const [ip, lib, refPath] = process.argv.slice(2)
const fs = require('fs')
const readline = require('readline')
@@ -18,6 +21,13 @@ 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}
// Audio meters: the switcher reads -100.00 dB (-10000) on an input with nothing on it at all; a quiet room, a
// pause or an open mic is far above that. So an input stuck at the floor means its audio is not arriving
// (2026-10-25: the Resi AJA stopped passing Dante audio and nothing said so).
const LEVELS = ref.levels || {}
const FLOOR = -9000
const heard = {} // input -> when it last had any audio
let metersAt = 0 // when the switcher last sent meters (none: no verdict)
const atem = new Atem({ disableMultithreaded: true })
let connected = false
@@ -32,7 +42,17 @@ atem.on('receivedCommands', (cmds) => {
}
}
})
atem.on('connected', () => { connected = true })
atem.on('levelChanged', (l) => {
if (l.system !== 'fairlight' || l.type !== 'source') return
metersAt = Date.now()
const k = String(l.index)
if (k in LEVELS && Math.max(l.levels.inputLeftLevel, l.levels.inputRightLevel) > FLOOR) heard[k] = Date.now()
})
atem.on('connected', () => {
connected = true
for (const k of Object.keys(LEVELS)) heard[k] = Date.now() // a fresh start gets the full grace time
if (Object.keys(LEVELS).length) atem.startFairlightMixerSendLevels().catch(() => {})
})
atem.on('disconnected', () => { connected = false })
atem.on('error', () => {})
@@ -68,10 +88,18 @@ function cameraDiffs() {
return out
}
function silence() {
const now = Date.now()
if (!connected || now - metersAt > 5000) return []
const secs = ref.silence_seconds || 30
return Object.entries(LEVELS).filter(([k]) => now - (heard[k] || now) >= secs * 1000)
.map(([k, name]) => `input ${k} ${name}: no audio for ${secs} s or more`)
}
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 }
camerasSeen: Object.keys(ccu).length, silence: silence() }
const s = JSON.stringify(state)
if (s !== last || Date.now() - lastAt > 10000) {
process.stdout.write('ATEM ' + s + '\n')