Design settled with the creator. Two findings drove it:
- **Resolution is not a bandwidth lever.** 1080p30 and 720p60 are both 6 Mbps in the shipped table,
so "step down to 720p" relieves nothing against congestion; and 60→30 at the same bitrate relieves
nothing either. Congestion therefore steps **bitrate only** (8.0 → 6.4 → 5.1 → 4.1, ~20%), which is
what OBS Dynamic Bitrate does — cited, adopted, with its climb-back discarded for v1.
- **The other half has no OBS equivalent**: machine starvation (the game and the app on one PC) steps
fps, then resolution, and sheds LlamaCasty's own capture cost *first*. That is the app taking
responsibility for a stream it is helping to starve.
Budgets as ruled: 3 steps for a 1080 start, 1 for 720 and vertical, hard floors at 720p30 /
Vertical 1080p30, hold-and-warn at the floor, never disconnect. Trigger ≥5% drops over 15 s twice,
30 s cooldown, ambiguous evidence does nothing.
New rule in `ui-surface.md`: a toast names the cause, the culprit and the remedy. Mechanism strings
("All pipe instances are busy", HTTP codes, exit codes) are log findings, never user-facing copy.
3.8 KiB
TASK 33 — Automatic step-down when the show can't hold its tier (queued 2026-09-01; v1)
Catalog:
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.
Cause first, lever second
| 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
- 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.
- 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 → 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.