diff --git a/TASKS.md b/TASKS.md index 8d15308..935702b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -49,7 +49,7 @@ cited first), then 35 / 37 / 40·A/C/D / 55·2–3. TASK 32 is complete, Back on | 40·A/C/D | ☐ | App Settings: active-camera picker; persist output resolution + surface transition defaults + confirm-before-End; accent consolidation | [`task-40`](TASKS/task-40-app-settings-round.md) | | 3·20 | ✅ | Session report — **derived from `superChatEvents.list` on demand, not persisted** (YouTube is the authority; no ledger). Per-currency totals, money off by default (done 2026-11-05) | [`task-03`](TASKS/task-03-capture-pipeline.md) | | 32 | ✅ | Stream resilience: fragmented MP4, blip retry, poll-driven RECONNECTING (no countdown — YouTube's cutoff is not a constant), ingest pre-flight, one-click Back on air (`BackOnAirTests`) | [`task-32`](TASKS/task-32-stream-resilience.md) | -| 33 | ☐ | Bandwidth auto step-down: pure policy over dropped-frame rate, one-way within a broadcast | [`task-33`](TASKS/task-33-bandwidth-step-down.md) | +| 33 | ◇ | Auto step-down, **cause-classified** (machine starvation → fps/resolution; upload congestion → bitrate only), shed our own capture cost first, one-way, hard floors, plain-words toast | [`task-33`](TASKS/task-33-bandwidth-step-down.md) | | 34 | ☐ | Scheduled streams (Text-drawer version) | [`task-34`](TASKS/task-34-scheduled-streams.md) | | 35 | ☐ | Scene-linked audio | [`task-35`](TASKS/task-35-scene-linked-audio.md) | | 37 | ☐ | Defaults vs current layout split (build-id); enables one-click revert to defaults | [`task-37`](TASKS/task-37-defaults-current-split.md) | diff --git a/TASKS/task-33-bandwidth-step-down.md b/TASKS/task-33-bandwidth-step-down.md index 08efa31..bdaad08 100644 --- a/TASKS/task-33-bandwidth-step-down.md +++ b/TASKS/task-33-bandwidth-step-down.md @@ -1,21 +1,68 @@ -# TASK 33 — Bandwidth auto step-down (queued 2026-09-01; v1 per the complete-v1 ruling) +# TASK 33 — Automatic step-down when the show can't hold its tier (queued 2026-09-01; v1) > Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here. +> Design settled with the creator 2026-11-09. **Not a magenta question of taste: this is the app +> admitting it is part of the problem when it is.** +**Goal:** the stream degrades *predictably and honestly* instead of drowning viewers in freeze frames +— and when LlamaCasty itself is the starving process, it says so out loud instead of blaming the +creator's connection. The `variable` reusable stream (shipped) makes every step zero-API: a tier or +bitrate change is just an encoder relaunch at the same URL. -**Goal:** when the upload can't hold the tier, drop it automatically instead of drowning viewers in -freeze frames. The `variable` reusable stream (shipped) makes this zero-API; the governor that decides -WHEN is what TASKS.md:25 always claimed existed and was never built. +## Cause first, lever second -1. ☐ Pure `StepDownPolicy` — sliding window over the encoder's reported dropped-frame rate - (`StreamHealth`, already parsed): sustained overrun → downgrade exactly one 16:9 tier - (1080p60 → 1080p30 → 720p60 → 720p30). Hysteresis cooldown; **one-way within a broadcast** - (no oscillation — a recovered connection doesn't climb back mid-show; the creator may manually - step up). Vertical tier stays a manual choice. -2. ☐ Restart machinery = TASK 32 slice 1 (tier change is an encoder relaunch at new W×H/FPS over the - same pipe/URL; the master canvas is 1920×1080 regardless, so geometry never rewrites). -3. ☐ Surface: the resolution badge + dropdown update to the landed tier; ONE Info toast - ("Quality lowered to hold your stream") — no nagging after that. -4. ☐ Tests: pure-policy units (drop-rate windows, one-way, cooldown) + the ONE integration: fake - health stream with sustained drops → exactly one lower-tier encoder restart. +| Cause | Signal | Lever | +|---|---|---| +| **Machine starvation** — the PC (often the game) can't make the frame | pump stalls, render time over the frame budget, frames never produced | reduce work: **60 → 30 fps**, then **1080 → 720** | +| **Upload congestion** — the socket can't carry the bits | frames produced on schedule but rejected; encoder queue full; **rising submit block time** (pump already logs `avg submit` / `avg wait`) | reduce bits: **8.0 → 6.4 → 5.1 → 4.1 Mbps** (~20% steps), geometry untouched | +Resolution is **not** a bandwidth lever: 1080p30 and 720p60 are both 6 Mbps in the shipped table, so +that step relieves nothing. Fps is **not** a congestion lever: same bitrate, same socket. + +## Shed our own load before asking the viewer to pay + +1. If our capture/convert cadence is what starves, **halve our own capture rate first** — same output + tier, less of the creator's machine. This is the "budget streamer self-streaming gameplay" case. +2. Only then step fps, then resolution. + +## Budgets, floors, terminal state + +| Start | Steps allowed | Worst case it can reach | +|---|---|---| +| 1080p60 | **3, any mix of the two ladders** | 720p30 @ 6.4 Mbps | +| 720p60 | **1** | 720p30 @ 4.8 Mbps | +| Vertical 1080p60 | **1** | Vertical 1080p30 @ 6.4 Mbps | + +- Floors: 16:9 never below **720p30**; vertical never below **Vertical 1080p30**. +- Budget exhausted → **hold**, keep streaming, one persistent warning naming the cause, manual + step-up stays available. Never disconnect, never nag repeatedly. +- Trigger: ≥5% dropped over 15 s, twice, 30 s cooldown between steps. Mixed/ambiguous evidence does + nothing. One-way within a broadcast (no climb-back; revisit with session data). + +## Own the mistake in plain words + +A toast that reports a mechanism is a bug in the toast. Every message names **what happened, who +caused it, and what the app already did** — no exception text, no error codes: + +- ours: *"LlamaCasty couldn't keep up with your scene, so it lowered the frame rate to 30. Your + connection wasn't the problem."* +- theirs: *"Your upload couldn't hold 8 Mbps, so LlamaCasty dropped it to 6.4 to stop the freezing. + It will stay there for this stream."* + +One toast per step, plus one session-report line (`quality stepped down 12:04 — encoder` / `— +upload`) so support answers from data, not memory. Wording rules live in +[`docs/ai/ui-surface.md`](../docs/ai/ui-surface.md) → Toast notifications. + +## Tests + +Pure `StepDownPolicy` units (classifier, both ladders, budgets, floors, cooldown, one-way, ambiguity → +no action) + the ONE integration: a fake health stream with sustained drops of each cause produces the +matching step and exactly one encoder restart. + +## Reference + +OBS Dynamic Bitrate (27.2) is the established answer for the congestion half: bitrate-only, small +steps, hysteresis. **Adopted:** bitrate as the sole congestion lever, stepped with cooldown. +**Discarded for v1:** its automatic climb-back (the creator's one-way ruling). **Ours:** the +machine-side ladder and "shed our own cost first" — OBS has no equivalent, and that is the half that +matters when the game and the app share one machine. diff --git a/docs/ai/ui-surface.md b/docs/ai/ui-surface.md index d841649..d6445c0 100644 --- a/docs/ai/ui-surface.md +++ b/docs/ai/ui-surface.md @@ -214,6 +214,12 @@ MIT — notice #10 in `THIRD-PARTY-NOTICES.txt`). Facts that cost a hunt: - Calls marshal to the captured Dispatcher (`_dispatcher.Invoke`) — encoder/pump events fire off-thread. Migrated MessageBoxes: both webcam-acquire warnings, image-read warning, sign-in unsuccessful/failed. +- **A toast names the cause, the culprit and the remedy (2026-11-09).** A message that reports a + mechanism is a bug in the message: "All pipe instances are busy", an HTTP status, an ffmpeg exit + code are all findings for the log, not for the creator. Every user-facing message says what + happened, **whose fault it was** (the machine, the connection, or LlamaCasty itself), and what the + app already did about it. When it is ours, say so plainly — an app that blames the creator's + connection for its own starvation is lying, and they will believe it and go buy a new router. Promoted from log-only (each keeps its AppLog line): go-live prep failures ×3 (reusable stream, broadcast insert, prep exception), frame-pump death while live, mic-missing at startup, premium lapse, saved-session refresh failure (sign-out notice). Deliberately left log-only: offline license re-validation