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
+19 -3
View File
@@ -1407,7 +1407,7 @@ class Engine:
def evaluate(self, snap, now, service_on):
ctx = {"now": now, "service": service_on, "any_recording": False, "dante_all": {},
"ready_due": in_windows(self.ready_cfg.get("due", []), now), "ready_done": self.ready_done(now),
"bypassed": self.bypass_names()[0]}
"bypassed": self.bypass_names()[0], "autofix_windows": (self.cfg.get("auto_fix") or {}).get("windows", [])}
for t in all_tiles(self.cfg):
for chk in t.get("checks", []):
if chk["type"] == "dante":
@@ -3354,6 +3354,12 @@ def atem_reference(xml_path, compare=None):
return {"audio": audio, "camera": camera}
def atem_levels(sil):
"""The inputs whose audio is metered for silence ("silence" in config "atem"), for atem_watch.js."""
sil = sil or {}
return {"levels": {str(k): v for k, v in (sil.get("inputs") or {}).items()}, "silence_seconds": sil.get("seconds", 30)}
def newest(pattern):
"""The path matching a pattern like ".../bmd-atem-*/..." with the highest version number."""
import glob
@@ -3380,7 +3386,8 @@ class AtemWatcher:
if not os.path.isabs(xml):
xml = os.path.join(here, xml)
with open(self.ref_path, "w") as f:
json.dump(atem_reference(xml, a.get("compare")), f)
json.dump(dict(atem_reference(xml, a.get("compare")), **atem_levels(self.atem.get("silence"))), f)
self.said = {} # what each part last said, so a change is logged once
def run(self):
while not self.stop.is_set():
@@ -3396,6 +3403,11 @@ class AtemWatcher:
data = json.loads(line[5:])
data["time"] = time.time()
self.snap.atem = data
for part in ("audio", "camera", "silence"): # say what differs, not only that it does
now_says = data.get(part) or []
if data.get("connected") and now_says != self.said.get(part, []):
log.info("ATEM %s: %s", part, "; ".join(now_says) or "as it should be")
self.said[part] = now_says
elif line.startswith("FIXED "):
r = json.loads(line[6:])
self.fixed[r.get("part")] = r
@@ -3436,11 +3448,15 @@ ATEM_WATCHER = None
def check_atem(chk, snap, ctx):
"""chk["part"] "audio" or "camera": the switcher must match the restore file."""
"""chk["part"] "audio" or "camera": the switcher must match the restore file. "silence": a metered input
(Resi) has had no audio at all for a while; only in auto-fix's hours or while a
HyperDeck records - at 4 AM the console may simply be off."""
a = snap.atem
if not a or not a.get("connected") or ctx["now"] - a.get("time", 0) > 30:
return [(chk["part"], Result(OK))] # watcher not reporting: no verdict
diffs = a.get(chk["part"]) or []
if chk["part"] == "silence" and diffs and not (ctx.get("any_recording") or in_windows(ctx.get("autofix_windows") or [], ctx["now"])):
return [(chk["part"], Result(OK))]
if diffs:
return [(chk["part"], Result(ERR, chk["msg"], info="; ".join(diffs[:3]), named=True))]
return [(chk["part"], Result(OK))]