docs(queue): six veggies, then the new work; scheduling and publishing retired

Creator ruling 2026-11-09: finish the queue before the new features, gold pass always last.

**Retired (platform's surface, not ours):**
- 34 scheduled streams — YouTube owns scheduling *and* the reminder that makes it worth anything.
- 50 publish recording — Studio owns upload (resumable, quota-aware, explains rejections), and an
  in-app uploader would widen the OAuth consent to whole-channel video management for one convenience.
  The app adds "Show in folder" instead.

**New tasks:** 65 replay clips (the creator-side buffer YouTube does not give you), 66 timed ticker
messages, 67 per-event alert videos, 68 edit-while-live + first-run On-Air.

**TASK 65 spec** is the interesting one: 15 s is a clip (10/15/20, default 15); the ring holds encrypted
*encoded* fragments in RAM — raw 1080p frames would be ~7.4 GB for 15 s — so there is no steady disk
write and no crash debris; F10 saves; the replay layer plays once then hides in Live and loops the last
1–3 elsewhere; silent by default; three kept; the layer is excluded from its own clip (hall of mirrors).

**TASK 14 gains Branch 3:** mic input gain ahead of the gate (table boom mics fall under it), webcam
blank hotkey, opacity/geometry parity with a collapsed Geometry disclosure, Show-in-folder.

**Decisions recorded:** the replay-buffer reversal (it was retired in 2022-era reasoning as "different
product"; the case for it won), the platform-surface rule, and voice triggering deferred — always-
listening in a streaming app hears the game, so the content is its own false-positive source.

Queue refactored to 56 lines by archiving completed rows and the superseded surge plan; HANDOFF at 40.
This commit is contained in:
2026-11-09 14:27:07 -08:00
parent 42228c7aab
commit ec6a5ca466
11 changed files with 254 additions and 97 deletions
+52
View File
@@ -474,3 +474,55 @@ These were explicitly ruled out:
default. Tests: `BrandLogoTests`. On-screen check pending.
- ☐ **Cube** — GPU path gated on capability; CPU path only if it holds 60fps.
- Gear: right-click opens App Settings (built, on-screen check pending). Left-click menu stays for now.
---
# 2026-11-09 trim — completed rows and the retired surge plan
Moved out of `TASKS.md` when the queue was refactored (creator ruling: finish the queue, then the new
features). Nothing here is active; the reasons for the two `❌` rows live in `docs/ai/decisions.md`.
## Completed
| Task | Scope |
|---|---|
| 53 | Output-format contract — six size/FPS modes, vertical YouTube playback, recordings visually accepted |
| 3·17 | Text layer — content, font, colour, alignment, drag/resize, persisted (2026-11-04) |
| 21 | Media source — video file decodes, composites, plays audio, loops, stops with last reference (2026-11-05) |
| 64 | Green/Blue Screen — detect + key from the decode pipe, opt-in auto-apply, alert clips included (2026-11-05) |
| 3·20 | Session report — derived from `superChatEvents.list` on demand, never persisted (2026-11-05) |
| 39·2 | YPP slice 2 — three eligibility bars, dated thresholds, "x of 3 criteria met" (2026-11-05) |
| 32 | Stream resilience — fragmented MP4, blip retry, poll-driven RECONNECTING, ingest pre-flight, one-click Back on air |
| 33 | Auto step-down — cause-classified (machine → shed, fps, resolution; upload → bitrate), one-way, floors |
| — | Preview undo (Ctrl+Z / Ctrl+Shift+Z) |
| — | OutputProfile gate + Demo conversion UI |
| — | System-message strip replaced by the Logs tab |
## Retired rows
| Task | Why |
|---|---|
| 49 | Chat profanity filter — YouTube owns moderation (2026-11-05) |
| 10 | Polar subscription path — superseded by the one-price model (2026-11-01) |
| — | `StreamConfig` defaults — no production call sites |
| 34 | Scheduled streams — retired 2026-11-09, platform's surface |
| 50 | Publish recording — retired 2026-11-09, platform's surface |
## The surge plan (2026-11-02 → 11-07) — superseded
Six days of assigned work, written before the queue was re-ordered. Kept for the record only; the live
order is in `TASKS.md`:
| Day | Units |
|---|---|
| 1 | Licensing: ownership check, greyed tiers, About conversion, Logs tab |
| 2 | Release blockers: visibility flow, 30/60-fps gate, vertical resolution, webcam identity, Move |
| 3 | Resilience: TASK 32 + 33 |
| 4 | Content & media: text layer, media wiring, profanity filter (retired), scheduled streams (retired) |
| 5 | Data & settings: reward persistence, YPP slice 2, defaults split, scene audio, settings round, disk hygiene |
| 6 | Finish: publish recording (retired), undo, feedback batch, gold pass, sweep |
## Creator hands — cleared 2026-11-09
- Vertical-tier camera check: 1–6 pass (Live tile inside the band, Chat rect-only, 16:9 restored exactly).
- Webcam cadence, vertical geometry: 29.8 distinct images/s — the 25.6/s failure does not reproduce.
+12
View File
@@ -22,3 +22,15 @@
13. ✅ Web source URI hidden for webcam — URI section was showing for webcam due to WPF binding path `SelectedElement.Type` not resolving on `WebcamSceneConfig`; replaced with `IsWebSource` virtual property on `SceneElement` (mirrors existing `IsWebcam` pattern), overridden in `Source` to return `Type == SourceType.WebSource`, bound via `BoolToVis`
14. ✅ Check/revert consistent order + accept — all three element sections (webcam border, countdown timer, web URI) now use consistent [✕][✓] button order; green check buttons enabled with click handlers that save current values as snapshot baseline (so revert undoes to last accepted state, not initial selection)
### Branch 3 — queued 2026-11-09 (creator testing notes)
| # | Slice | Why |
|---|---|---|
| 15 | ☐ **Mic input gain** — a dB stage *ahead of* the gate, meter reads post-gain | A headset mic sits close enough to clear the gate; a table-mounted boom is quieter and the gate eats it. Not AGC — automatic gain pumps on breath and silence. Clipping is already the limiter's job |
| 16 | ☐ **Webcam blank / standby** — a visibility hotkey for the webcam layer (tenth registration), optional standby media while hidden | "My webcam has to go away *now*." The privacy blur covers the screen case; this covers the camera in one keystroke |
| 17 | ☐ **Opacity + geometry consistency** — every layer type exposes the same opacity control and the same position/size affordances | Parity across layer types. Geometry fields stay, but collapse behind a "Geometry" disclosure: keyboard-only and screen-reader users cannot drag a layer, and precise alignment between two monitors is otherwise guesswork |
| 18 | ☐ **Show in folder** — on the stop notification, reveal the finished recording | Publishing is retired to YouTube (TASK 50); the app's remaining job is "where did my file go?" |
One Good Dog test per slice. If any of them turns out to be a redesign rather than a fix, it gets its own
task number instead of hiding in this batch.
+65
View File
@@ -0,0 +1,65 @@
# TASK 65 — Replay clips: buffer the last 15 seconds, show it, save it
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
> Spec settled with the creator 2026-11-09. **Not a Twitch-clip clone:** the platform owns viewer
> clips; this is the creator-side replay buffer YouTube does not give you.
## The rule
**15 seconds is a clip.** Options 10 / 15 / 20, default **15**, no slider, no 30. A clip is a moment,
not a segment — 3-minute Shorts are the platform's problem, not ours.
## Why the ring holds encoded bytes, not frames
A 1080p BGRA composite frame is 8.3 MB; fifteen seconds at 60 fps is **~7.4 GB** of raw frames. The
15 MB figure is *after* compression. So the ring keeps **fragmented MP4** (`frag_keyframe+empty_moov`)
that ffmpeg writes to a pipe, in a **byte ring in RAM** — never on disk, never raw.
- Fragments are **1 s** (not 2): the ring can only hand back whole fragments, so a longer fragment
means the save can miss the punchline.
- The buffer is capped **by bytes**, not seconds — bitrate moves. When 15 s would exceed the cap, the
clip is simply shorter.
- The ring is a member of the process: it dies with the app, so a crash leaves no debris and needs no
sweep. **Nothing touches the disk until F10.**
## Trigger
**F10** saves the clip (F1–F9 are taken; F11 is the fallback if a game steals F10). No focus switch, no
mouse. Deferred deliberately: **voice triggering** (always-listening in a streaming app is a privacy
liability, and the game's own audio produces false positives) and **`!clip`** (only for viewers/mods,
never the creator, who cannot reach a keyboard mid-moment).
## The replay layer
One layer type, added by the creator to the scenes they want; behaviour comes from the scene:
| Scene | Behaviour |
|---|---|
| **Live** | plays the new clip **once**, then **hides** — you're there to see it or you're not |
| Every other scene | loops the last **1–3** saved clips **in order** (highlight reel), configurable |
- **Audio off by default**, toggleable per layer. The show's sound design is the creator's; a replay
should not re-mix it uninvited.
- **The replay layer is excluded from the clip's own composition.** Otherwise saving while a clip plays
records a video of a video (hall of mirrors). Viewers still see it live; the clip simply doesn't.
## Storage and retention
- Saved to `clips\` beside recordings, as a normal `.mp4`.
- **The three most recent are kept**; the fourth deletes the oldest.
- **Auto-clip on monetization events** (off by default): a Super Chat / membership / raid saves the
preceding 15 s hands-free. Same retention, so it cannot accumulate.
- **Disk discipline:** no steady writes while live (the ring is RAM); the free-space guard that already
protects REC also disables the ring when space is low — **the recording always wins**.
## Privacy
Named in `PRIVACY.md`: clips are local only, the three most recent are kept, older ones are deleted
automatically, and nothing is uploaded. The first save says the same in one line, so the policy is not
only a document.
## Tests
- Ring: fragment boundaries respected, byte cap enforced, save emits a playable MP4, oldest pruned.
- Layer: once-then-hide in Live, in-order loop elsewhere, silent by default, layer excluded from the clip.
- Failure: save with an empty ring (just started) says so instead of writing a 0-byte file.
+17
View File
@@ -0,0 +1,17 @@
# TASK 66 — Timed ticker messages (the ticker's real job)
> Catalog: [`TASKS.md`](../TASKS.md). Creator idea 2026-11-09.
**Problem.** The ticker today is wired to YouTube events that a new streamer will rarely see — no
memberships, no Super Chats, nothing to tick. A feature that fires twice a year is furniture.
**Idea.** Give the ticker its own clock: a list of creator-written messages plus an interval.
- Config: interval (minutes) and a list of lines; rotate in order, newest config wins on restart.
- Example the creator gave: *every 20 minutes, promote the merch channel*.
- Yields to an alert: an event's own ticker beats a scheduled one, and the clock resumes after.
- Off by default, one message to start, no randomisation and no scheduling-per-day until asked.
**Where:** the ticker section of the alert box properties.
**Tests:** interval firing, rotation order, alert pre-emption, persistence across restart.
+16
View File
@@ -0,0 +1,16 @@
# TASK 67 — Per-event alert videos
> Catalog: [`TASKS.md`](../TASKS.md). Creator idea 2026-11-09.
**Problem.** The app plays one alert clip for every event. That single clip was always a placeholder.
**Idea.** A clip per event kind — Super Chat, Super Sticker, membership, subscription, gift, raid —
falling back to the default when a kind has none assigned.
- Each slot accepts a local file; the existing picker and the decode/keying pipe are reused unchanged
(TASK 21 / 64), including green/blue screen — a clip is a clip.
- Missing slot → default clip → nothing, in that order, and never an error for the creator who only
filled one.
- The alert box's display method, sound byte and ticker stay per-event as they already are.
**Tests:** slot selection per event kind, fallback chain, a keyed per-event clip composites correctly.
+28
View File
@@ -0,0 +1,28 @@
# TASK 68 — Editing while live, and the first-run On-Air state
> Catalog: [`TASKS.md`](../TASKS.md). Creator rulings 2026-11-09.
## Part 1 — edit while live
Creators need to change a layout mid-show: hide a layer, nudge a camera, swap a video. Today editing is
gated to "not live", so the only way is to stop the show.
- Allow the layer panel and geometry edits while streaming/recording.
- An edit becomes program immediately — one canvas, no preview/program split — so an accidental drag is
visible to viewers. Undo (Ctrl+Z) must therefore be live too, and the UI must say when editing is live.
- Preview-mode gestures stay as they are: left = zoom while live, right = menu; **no chord while live**.
Editing live is an explicit mode the creator turns on, not something a stray click does.
- Never allow: removing the Background, deleting a scene that is on air, changing the output tier's
composition mid-frame.
## Part 2 — first-run state
The app should open in **On-Air mode** to match the 1-2-3 screen, so the first thing the creator sees
is the state they will stream from.
Risk to settle before building: an armed ON-AIR pill changes what Start does. The 1-2-3 screen must
*explain* the state rather than let the app default into the riskier action silently — a creator who
expects a recording must not go live by pressing Start.
**Tests:** edit during a session changes the composite; undo restores; a blocked edit action reports and
does nothing; first-run state matches the guide's copy.