Standby + Playout Policy — fallback selection, last-frame hold, loop, language/time-zone
How to read this page
A Plain and a Clear version of this page have not been written yet. What follows is the document itself.
Precise — the source document
This is the document. Rendered from the repository at the commit above, with nothing rewritten for the web. A gate re-renders it on every deploy and fails the build if a single byte differs.
Part of the UNI Production Platform production/ tree (ADR-PROD-007 scheduler/playout). Authored
strictly against docs/UNI_PRODUCTION_PLATFORM.md: the container/service map (uni-playout #8,
uni-bcast-mixer STANDBY scene #1), the broadcast.json overlay contract, and the GAPS register.
This file fixes how the scheduler picks a standby clip on a source/encoder glitch, the
last-frame-hold -> standby-reel sequence, the loop policy, and language / time-zone selection.
It depends on catalog.json (built by build-catalog.mjs, shaped in CATALOG_SPEC.md) as the
asset layer and on the per-slot run-of-show under production/run-of-show/ as the schedule layer.
All status claims are pending (design). See the honesty footer.
1. What "standby" protects against
A live source or the encoder can drop mid-show: the colony cam (:3020) stalls, a guest's
LiveKit feed freezes, /glass stops repainting, or uni-bcast-mixer (OBS) hiccups on a scene
swap. The audience must never see a frozen frame, a black screen, or an error. The platform's
answer (master design, scheduler section): cut to the STANDBY scene and loop catalog content
until the live path recovers, with a graceful last-frame-hold -> standby-reel transition so the
cut is not jarring.
STANDBY is a first-class OBS scene in the mixer (alongside COLONY / GLASS / GUESTS / CLIP /
NEWSDESK / TITLE / PIP). It layers: the looping standby clip (media source) + the standby.html
overlay (a calm "back shortly" / now-playing card) + the music bed + the ticker/clock overlays.
The producer / playout owns when to enter and leave it.
2. Detection -> entry (the watchdog)
uni-playout runs Restart=always (systemd) and a health probe loop. Entry into STANDBY is
triggered by any of:
- Encoder/mixer fault: obs-websocket unreachable, or the program scene reports no active video
source, or the relay (
uni-bcast-relayMediaMTX API127.0.0.1:9997) shows the ingest path down. - Source fault on the current beat: the live source named by the active run-of-show beat is unhealthy past a debounce window (default 2.5 s, mirroring the glass cockpit's alarm-debounce so a single dropped frame does not trip it).
- Run-of-show gap: the schedule has no live beat ready for the current slot/time (late guest, empty segment) - STANDBY fills the gap rather than dead air.
- Operator command:
cut_to("STANDBY")via the production MCP (session-authed in-show verb).
On entry, playout writes broadcast.json: onAir stays LIVE, nowPlaying.segment="Standby",
and standby.html shows the calm card. The entry reason + timestamp are recorded in the audit log
(the producer cannot self-approve a go-live, but in-show cuts within an open operator session are
audited, not per-call prompted - the gating model in the master design).
3. The transition sequence (last-frame hold -> standby reel)
The cut is staged so it reads as intentional, not as a crash:
- Last-frame hold (0 - ~600 ms). The mixer holds the last good program frame (OBS freeze / the standby clip's first frame pre-rolled) for a short beat. This hides the source-drop instant and gives the standby media source time to start cleanly. Hold is capped (default 600 ms); if the source recovers inside the hold, playout aborts the standby cut and returns to program.
- Crossfade to the standby reel (~300 ms). A short fade (the proven foundation already uses fade transitions) from the held frame into the STANDBY scene with the first selected clip playing. No hard cut.
- Standby reel loop. Playout plays a sequence of standby-eligible clips (Section 4) back to
back, crossfading between them, with the
standby.htmlcard + ticker + clock + music overlaid. - Recovery -> crossfade back. When the live path is healthy again past a recovery debounce
(default 4 s, longer than entry debounce to avoid flapping), playout crossfades from the
current standby clip's nearest clean boundary back to the live program scene and resumes the
run-of-show where it left off.
broadcast.json.nowPlayingis restored.
Anti-flap: if entry/exit would toggle more than 3 times in 60 s, playout latches STANDBY for a cool-down (default 30 s) before re-attempting recovery, so a flapping source does not strobe the audience.
4. Standby clip selection (the ranking)
When playout needs the next standby clip, it queries catalog.json and ranks candidates by a
deterministic, tie-broken preference. Prefer, in order:
aired === true- already public on YouTube, so it is safe, reviewed content. (A not-yet-aired clip could expose unpublished material on the live broadcast.) Non-aired clips are used only as a last resort when no aired clip fits, and only if--allow-unaired-standbypolicy is set by the operator.- On-language -
row.language === slotLanguage(the current slot's language; Section 5). Falls back to the slot's language family, then toenas the universal default. orientation === "vertical"- the pool is 9:16; the STANDBY scene composites vertical clips with the pillarbox / shorts-wall layout (GAP G-9x16). Alandscapeclip would letterbox oddly in that scene, so vertical is preferred for the reel;unknownorientation is treated conservatively as vertical.- Duration-fit - prefer clips whose
durationSecfits the expected hold window. Default fit band 45 - 120 s (the shorts are ~80 - 90 s). A clip withdurationSec === nullis skipped for fit-sensitive selection (it may be a stub or unmeasured) unless forced. - Topical coherence (soft) - prefer the same
campaign(and, if set, the sameseries) as the interrupted segment, so the standby reel feels related to what the show was doing. This is a soft tiebreak, not a hard filter. - Freshness / rotation - among equal-ranked candidates, pick the least-recently-played
(playout keeps an in-memory recently-played ring per session) so the reel does not repeat the
same clip while a long outage runs. Final deterministic tiebreak:
assetIdascending.
Selection is a pure function of (catalog rows, slot context, recently-played set) - reproducible
and auditable. The shortlist (the standby pool for a slot) can be precomputed at slot start so the
glitch-path selection is O(1).
Guard: clips under min-duration (3 s; the content/media/shorts/ 0.07 s stubs) are never in the
catalog (the builder refuses them), so they can never be selected.
5. Language + time-zone selection
The 7-day grid places three 4-hour slots/day across time zones; each slot carries a language
and a local appointment time (the daily engine already staggers languages: en 09:00 CT,
es 10:00, pt 11:00, fr 12:00, it 13:00, hi 14:00 - from uni-channels.json). Playout uses that
registry both for live language and for standby:
- Slot language drives the reel. A Spanish 12:00-16:00 CET slot pulls
language === "es"standby clips; a Hindi slot pullslanguage === "hi"(Devanagari font;hi_IN-priyamvadavoice for any narration overlay). Language falls back en when a language has too few aired standby clips to sustain a long outage. - Time-zone -> which clock zones + which slot.
broadcast.json.clock.zonesshows the relevant zones (UTC + the slot's anchor region + two reference zones). The weekly grid maps wall-clock windows to slots so each major audience band gets a fresh appointment in its own prime time and its own language; the standby reel inherits the active slot's language so a viewer who tuned in for Hindi prime time still sees Hindi content during an outage. - Caption language during standby.
uni-bcast-captionsmay idle during a clip-only reel; thecaption.htmloverlay either hides or shows the clip's burned-in language.broadcast.json. caption.langis set to the slot language. - Brand consistency. Every standby clip carries
brandPack: "uni-solutionwright"; thestandby.htmlcard uses the same UNI x Solution Wright mark as the live lower-thirds, so the channel identity holds across the cut (master-design uniform-brand rule).
The language registry (uni-channels.json) is the single source for locale, default language,
playlist id, publish-local-time, and title-prefix; playout reads it (not a hard-coded table) so a
registry edit reschedules both live and standby language behavior.
6. Loop policy (sustained outage)
For a long outage, the standby reel must stay watchable, honest, and non-repetitive:
- Sequence, do not single-loop. Play a rotation of distinct ranked clips (Section 4), crossfading between them, rather than looping one clip - a single looped clip reads as "frozen / broken" to a returning viewer.
- Rotation window. Cycle through the slot's standby shortlist; only re-show a clip once the shortlist is exhausted (recently-played ring prevents near-term repeats). With ~100 aired clips today the rotation can run a long time before any repeat.
- Honest standby card.
standby.htmlsays, plainly, "Back on air shortly" + the now-playing clip title + a staleness-aware clock (every overlay showsbroadcast.json.updatedUtcage honestly per the contract). It does not claim the live show is running when it is not. - Music continues, ducked appropriately. The music bed (GAP G-MUSIC - must be sourced
CC/royalty-free; none exists today) keeps the reel from feeling dead;
broadcast.json.musicreflects its level. Narration overlays are not triggered during a pure standby reel unless the operator narrates. - Escalation. If the outage exceeds an operator-set ceiling (default 15 min), playout
raises an alert (notify path) and the producer proposes either continuing the reel or a clean
stop_broadcast- which is human-gated + 2-step confirm (the producer agent cannot self-approve ending the public stream).
7. Interaction with the run-of-show
- A scheduled CLIP beat (
roll_clip) is not standby - it is normal programming; it pulls from the same catalog but by the run-of-show's explicitassetId/ filter, not the standby ranking. - Standby is the involuntary path (glitch / gap). When the run-of-show itself schedules a "film / segment playout" template for a slot, that is planned content, and standby only engages if that playout's source faults.
- On recovery, playout resumes the run-of-show at the interrupted beat's boundary, not from the top of the slot, so the schedule does not drift.
Status (honest)
This is a design + policy spec, not a deployed system. No part of the playout / standby path
runs yet; every "playout will / picks / cuts" is a proposal, status pending. The thresholds
(debounce 2.5 s, hold 600 ms, recovery 4 s, fit band 45 - 120 s, escalation 15 min) are honest
defaults to be tuned against captured runs, not measured values.
- No banned-unqualified word is used as a claim (verified / proven / guaranteed / isolated /
secure / 100% / certified / real). Standby behavior is described as it is designed to behave;
recovery/anti-flap correctness is
pendinguntil a captured run (a deliberately killed source) shows the cut-to-STANDBY-and-recover sequence. - Standby selection depends on
catalog.json, whoseairedstate is read from_statussnapshots and may be stale (seeCATALOG_SPEC.md). "Prefer aired" is therefore best-effort: reconcile against the live YouTube library if exposing unpublished content is a concern. - Open GAPs touching this artifact: G-9x16 (vertical pool in a 16:9 STANDBY scene - pillarbox/shorts-wall), G-MUSIC (no music bed asset exists - the reel's audio bed must be sourced CC/royalty-free), G-ENC (the encoder node/GPU is an operator hardware choice; the encoder is never co-located with the ERP appliance).
- Live-appliance safety: the business stack (
solutionwright-*, odoo, jitsi, cloudflared, portainer) is read-only observation, never a mutation target. Standby/playout only reads the catalog + the run-of-show and drives the mixer/overlays; the irreversible verbs (stop_broadcast) remain human-gated + 2-step confirm and the producer agent cannot self-approve.
sha256 b3dd46df86b9383c — of the original file, so what was ingested stays checkable.