fix(alerts): silent decoder falls back to the six animations after a 1s no-frame grace

Routing and ruling both good: 'procs in the chat windows but nothing in the alert box'
with zero exceptions — every failure stage was silent by design (Debug.WriteLine-only
preview catch, bare catch{} in RunAsync firing Completed regardless). A decode run that
starts but yields neither a frame nor an audio chunk left _clip != null with
_latestClipFrame == null, so RenderClipFrame returned null forever: the box stayed
transparent for the whole alert and the six-animation fallback (which only ran on
setup-time throws) never fired.

Fix: AlertOverlayLayer.Advance gives a started clip a NoFrameFallbackSeconds (1.0s)
grace — produce no frame AND no audio inside it and the clip is torn down, _elapsed
resets, and the SAME alert continues as the animation branch (never blank, never drained
early). Diagnostics stop the lying silence: BeginClip logs box id/path/size + setup
failures; AlertClipDecoder.RunAsync logs frame/chunk end-state and the exception it used
to swallow.

Good Dog test: SilentDecoder_FallsBackToTheAnimationAfterTheNoFrameGrace (transparent
inside grace, disposed past it, animation renders, drains to idle).

Reference: alert playback must degrade to its fallback on dead-air, never vanish —
same contract as every player/booth failure budget (e.g. OBS source fallbacks).
https://obsproject.com/forum/threads/source-visibility-fallback.187383/
This commit is contained in:
2026-09-26 12:08:27 -07:00
parent 7b940b6a5a
commit 80038ff152
6 changed files with 231 additions and 60 deletions
+27
View File
@@ -881,3 +881,30 @@ Red pre-fix (it was the poller's), green post-fix.
Grep-ahead: a `DispatcherTimer` ctor guarded by `if (Application.Current != null)` marks a class
with UI-thread affinity — audit every production ingress point for the marshal when adding one.
---
## A decoder that "starts" but yields nothing is DEAD AIR — fall back on a no-frame grace, never trust silent catches (2026-09-26)
Second live alert failure of the day, a different void than the crash: creator's repro — On-Air →
test → remove and re-add the Stream Alerts layer → TEST event → procs in chat but the alert box
stayed transparent, and startup.log 11:44's `peakMix 0.105` proved the alert audio never reached the
mix (vs 11:21's 0.375–0.891). ZERO exceptions anywhere; every failure stage was silent BY DESIGN:
`UpdatePreview`'s catch was `Debug.WriteLine`-only (nothing in release), and `AlertClipDecoder.RunAsync`
had a bare `catch { }` around the whole run (fires `Completed` regardless). A decode run that fails
AFTER `BeginClip` succeeds leaves `_clip != null` with `_latestClipFrame == null` → `RenderClipFrame`
returns null forever → box transparent FOREVER, and the six-animation fallback only ever ran on
setup-time throws. Even frame pacing was silently off (no frame-rate probe → `frameDuration = 0` →
video dumps instantly while 50 ms audio chunks pace the whole clip).
The established answer (derivative: any video-player/booth failure budget has the same contract):
**an alert that plays is a promise — the moment data stops arriving, degrade, don't vanish.** Fixed
in `AlertOverlayLayer.Advance`: a started clip that produced neither a video frame NOR an audio chunk
inside a `NoFrameFallbackSeconds` (1.0 s) grace is torn down, `_elapsed` resets to 0, and the SAME
alert continues as the six-animation branch — never drained early, never blank. And the silence is
gone: `BeginClip` writes the box id / resolved path / size + setup failure; `RunAsync` now logs the
frame/chunk end-state AND the exception it swallowed. Rules for every future gamble:
(1) a `catch` that hides WHY the output vanished is a lie — log the exception and the counters;
(2) any producer that yields zero output inside a grace becomes a fallback trigger, not a wait;
(3) one integration test per rescue: `SilentDecoder_FallsBackToTheAnimationAfterTheNoFrameGrace`
pins transparent→fell-back→renders→drains.