From b7d58bbf3248160512c14fcafc1c36aca8660158 Mon Sep 17 00:00:00 2001 From: Timothy Date: Wed, 22 Jul 2026 22:09:37 +0200 Subject: [PATCH] docs(74): tighten overlay suppression rule (Merge-during-filler also clears) Folds whole-branch review finding #2. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/channels.md | 4 +++- docs/decisions.md | 5 ++++- docs/domain-model.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/channels.md b/docs/channels.md index aef6a42e2..f5c813b81 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -238,4 +238,6 @@ element" and `docs/decisions.md` → `graphics.channel-level-attachment`. explanatory caption when the channel is in HLS-Direct. - **A deco can suppress it.** A `Deco` in `Override` or `Disable` mode for its graphics-elements section takes precedence over the channel-level overlay (channel elements are a base layer, not - the final word) — see `docs/domain-model.md` → "Graphics element". + the final word). Additionally, on a **filler** item a deco whose graphics-elements section is not + set to run during filler (`UseGraphicsElementsDuringFiller` false) also clears the overlay, for + `Merge` and `Override` alike — see `docs/domain-model.md` → "Graphics element". diff --git a/docs/decisions.md b/docs/decisions.md index 52be260db..1f28ff282 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -2735,7 +2735,10 @@ four joins (composite key `{ChannelId, GraphicsElementId}`), added via a dual-pr - `GraphicsElementSelector.SelectGraphicsElements` appends channel-level elements at the **final fall-through**, alongside `playoutItem.PlayoutItemGraphicsElements` — a **base layer**. A deco in `Merge` mode composes with it; a deco in `Override`/`Disable` mode returns earlier and so - suppresses it (decos are allowed to override channel defaults, a deliberate rule). + suppresses it (decos are allowed to override channel defaults, a deliberate rule). One more + suppression path: on a **filler** item, a deco whose graphics-elements section is not set to run + during filler (`UseGraphicsElementsDuringFiller` false) clears the result and returns for `Merge` + and `Override` alike, so the channel base layer is dropped there too. `HttpLiveStreamingDirect` continues to return empty (ErsatzTV isn't transcoding, so there is no frame pipeline to draw into). - **Seeded built-in element**, mirroring the `iptv.logo-drives-bug-preset` (#67) pattern: diff --git a/docs/domain-model.md b/docs/domain-model.md index 341de1def..42ae9a278 100644 --- a/docs/domain-model.md +++ b/docs/domain-model.md @@ -68,7 +68,7 @@ Channel (1) ──< Playout (0..N per channel; ChannelPlayoutSource distinguishe | **Seasonal / date-conditional scheduling** (#73) | Holiday/seasonal channels are **not a separate feature** — they are the existing date predicate on `IAlternateScheduleItem`, implemented by `ProgramScheduleAlternate` (Classic) and `PlayoutTemplate` (Block), evaluated by `AlternateScheduleSelector.GetScheduleForDate` (first match by `Index`, catch-all last). **Leaving `StartYear`/`EndYear` empty makes the range repeat every year** — the "set once, works every December" switch; explicit years (required in pairs) mean a one-off window and disable wrap-around detection. Wrap-around (Nov→Feb) and invalid/leap dates (Feb 31) are handled. No *soft* prioritization primitive exists (binary first-match-wins); that ask belongs to #70's weighting work. See `channels.md` → "Recipe: seasonal / holiday programming" and `decisions.md` 2026-07-17. | `IAlternateScheduleItem`, `ProgramScheduleAlternate`, `PlayoutTemplate` | `/app/playouts/{id}/alternate-schedules`, `/app/playouts/{id}/templates` | | **Playback order** | How a schedule item's source(s) are sequenced (`PlaybackOrder`). Note three that are easily confused: **`Shuffle`** is Fisher–Yates over the flattened items, so airtime is implicitly proportional to collection size (a 200-episode show swamps a 20-episode one). **`ShuffleInOrder`** is a balanced shuffle (keyj) that pads sources to equal length with non-emitting spacers — it plays every item exactly once per cycle, so it prevents *clumping* but leaves airtime proportional to size; it is **not** fair-share. **`WeightedShuffle`** (#70) picks a *source* by smooth weighted round-robin then takes its next item, so each source's `Weight` is its share of airtime — equal weights (the default) mean equal airtime regardless of library size, with small sources looping. Classic engine only; rejected at the write path for playlist/block items. See `decisions.md` 2026-07-17. | `PlaybackOrder`, `MultiCollectionItem.Weight`, `MultiCollectionSmartItem.Weight` | `WeightedShuffle` is offered as a Playback Order **only** on classic schedule items whose source is a MultiCollection (`web/src/schedules/itemRules.ts`, #404); the per-source weights themselves are edited at `/app/multi-collections` | | **Watermark** | `ChannelWatermark` image overlay; attached at channel, schedule-item, block-item, deco, or playout-item level with position/size/opacity. | `ChannelWatermark`, `DecoWatermark`, `BlockItemWatermark`, `ProgramScheduleItemWatermark` | `/app/watermarks` | -| **Graphics element** | YAML-authored (`Text`/`Image`/`Subtitle`/`Motion`/`Script`) render-engine overlay, distinct from the image-only `ChannelWatermark` system. Attaches via 5 parallel join tables: `PlayoutItemGraphicsElement`, `ProgramScheduleItemGraphicsElement`, `BlockItemGraphicsElement`, `DecoGraphicsElement`, and (#74) **`ChannelGraphicsElement`** — a direct `Channel`-level attachment that did not exist before #74. `GraphicsElementSelector.SelectGraphicsElements` treats channel-level elements as the final fall-through **base layer**: they merge with `Merge`-mode deco elements and per-playout-item elements, but a deco in `Override`/`Disable` mode returns before that fall-through and so **suppresses** the channel overlay; `HttpLiveStreamingDirect` always returns empty (ErsatzTV isn't transcoding, so nothing can be burned in). A built-in seeded text element, `on-now-next.yml` (`GraphicsElementDefaults.OnNowNextFileName`), is written once (`GraphicsElementSeeder.SeedOnNowNext`, guarded by the `graphics.on_now_next_seeded` ConfigElement marker, adopt-not-clobber like the #67 watermark seed) and identified to API clients via a server-derived `GraphicsElementResponseModel.builtIn` flag (path-name comparison, not name matching). Edited per-channel at Channel editor → Branding → "Show On Now / Next overlay". See `decisions.md` → `graphics.channel-level-attachment` (#74). | `GraphicsElement`, `ChannelGraphicsElement` | `/app/edit-channel/{id}` (Branding tab); YAML files under `GraphicsElementsTextTemplatesFolder` etc. are not directly SPA-edited | +| **Graphics element** | YAML-authored (`Text`/`Image`/`Subtitle`/`Motion`/`Script`) render-engine overlay, distinct from the image-only `ChannelWatermark` system. Attaches via 5 parallel join tables: `PlayoutItemGraphicsElement`, `ProgramScheduleItemGraphicsElement`, `BlockItemGraphicsElement`, `DecoGraphicsElement`, and (#74) **`ChannelGraphicsElement`** — a direct `Channel`-level attachment that did not exist before #74. `GraphicsElementSelector.SelectGraphicsElements` treats channel-level elements as the final fall-through **base layer**: they merge with `Merge`-mode deco elements and per-playout-item elements, but a deco in `Override`/`Disable` mode returns before that fall-through and so **suppresses** the channel overlay (and on a **filler** item, a deco whose graphics-elements section is not set to run during filler — `UseGraphicsElementsDuringFiller` false — clears it too, for `Merge` and `Override` alike); `HttpLiveStreamingDirect` always returns empty (ErsatzTV isn't transcoding, so nothing can be burned in). A built-in seeded text element, `on-now-next.yml` (`GraphicsElementDefaults.OnNowNextFileName`), is written once (`GraphicsElementSeeder.SeedOnNowNext`, guarded by the `graphics.on_now_next_seeded` ConfigElement marker, adopt-not-clobber like the #67 watermark seed) and identified to API clients via a server-derived `GraphicsElementResponseModel.builtIn` flag (path-name comparison, not name matching). Edited per-channel at Channel editor → Branding → "Show On Now / Next overlay". See `decisions.md` → `graphics.channel-level-attachment` (#74). | `GraphicsElement`, `ChannelGraphicsElement` | `/app/edit-channel/{id}` (Branding tab); YAML files under `GraphicsElementsTextTemplatesFolder` etc. are not directly SPA-edited | | **Collection** | Manual list of media items (`CollectionItem`). | `Collection` | `/app/collections` | | **SmartCollection** | Saved search — a `Query` string, no static item list. | `SmartCollection` | `/app/collections` | | **MultiCollection** | Combines multiple `Collection`s and/or `SmartCollection`s (with grouping via `MultiCollectionItem`/`MultiCollectionSmartItem`). Both join entities carry a per-source `Weight` (default 1) used by `PlaybackOrder.WeightedShuffle` (#70) and ignored by every other order — the two are mirrors, so a change to one belongs on the other. The editor exposes a per-source weight input (1..1000, mirroring the API validator) with a computed % share and a "Reset to fair share" action; it round-trips `weight` from the GET because the PUT replaces the item list (#404). | `MultiCollection` | `/app/multi-collections` (#151, weight UI #404) |