Commit Graph
5 Commits
Author SHA1 Message Date
gramps f2cab5ca0f feat(quality): the creator sets the rate — per tier, persisted, clamped only at YouTube's floor
The tier table cannot know a creator's connection, so it should not pretend to. The
Mbps box under the picker is theirs: the tier chooses geometry and frame rate, the
box chooses bits. Stored per tier (`Video.Rate.<tier>`), so tuning 1080p60 does not
touch 720p30, and none means "the shipped default" rather than zero.

Clamped at the tier's YouTube minimum (`RateLadder`), because a target under it
cannot hold the resolution; no ceiling otherwise, because a wrong guess is exactly
what live step-down exists to catch, and a creator who types 25 on a fibre line
should be allowed to find out. The tooltip carries YouTube's number for the tier.

`MainViewModel.EffectiveQuality` is the single substitution point, and the encoder
options, the step-down policy and the clip buffer all read it — so a rated tier can
never stream at the shipped default because one call site was missed.

One ordering trap found by the test: the constructor resolves the default tier
before the layout store is open, so a read at that point must not cache, or the
shipped default shadows the creator's stored rate for the whole run.

670 pass, 0 warnings.
2026-11-12 08:11:13 -08:00
gramps 93719a286d feat(step-down): the bitrate floor is the tier's YouTube minimum
Step-down used a flat 3 Mbps (4 for vertical) as its floor — a number that had
nothing to do with the tier being streamed. With CBR holding the target, that
floor is a real picture-quality floor, so it has to follow YouTube's published
H.264 table (support.google.com/youtube/answer/2853702): 6 at 1080p60, 5 at
1080p30, 3 at 720p.

Models/RateLadder is that table as pure policy. Vertical is not a separate row:
1080x1920 and 1920x1080 are the same pixel count, so the frame rate alone picks
the band. StepDownPolicy takes the floor as an optional constructor argument and
refuses one above the starting rate, so an unusual start cannot begin under its
own floor. Omitted, the historical 3/4 floor still applies, which keeps the
existing policy tests meaningful and lets a caller opt in.

One consequence worth knowing: the three-step budget can stop above the floor
(12 → 9.6 → 7.7 → 6.2 from a 20% loss), so the floor is a limit, not a target.

669 pass, 0 warnings.
2026-11-12 08:05:55 -08:00
gramps 24077e58cf fix(stepdown): restart the audio pipe around encoder changes
A quality step-down now closes the named audio pipe before stopping/flushing the
pump, then opens a fresh server before restarting the pump. This prevents ffmpeg
from waiting on the old audio input and avoids reconnecting to a broken pipe.

EncoderRestartTests pins the audio-stop → video-stop → audio-start → video-start
order. 616 tests green, clean build with 0 warnings.
2026-11-11 09:52:27 -08:00
gramps 0fa3006da7 feat(33): shed our own capture cost first; the task is done
Last slice. 590 green, 0 warnings.

- **Shed before you charge the viewer.** On machine evidence the policy's first act is
  `ShedCaptureCadence`: `ScreenCaptureFrameSource` widens its conversion interval 10 ms → 33 ms (≈100
  → 30 conversions/s) through a new `ScreenCaptureManager.ShedLoad`. The output tier, the declared fps
  and the viewer's picture are untouched — we stop paying for readbacks we were throwing away. It
  costs no step budget; if the starvation persists, the fps ladder runs on the next clean window.
- The shed is reset on every stop path, so a cheap session cannot leave a later one capped.
- **The session line is a Logs-tab line, not a session-report line.** The session report is the money
  summary (Super Chats, per currency); quality chatter there would bury the numbers it exists for. The
  cause-tagged line already lands in the log at the moment of the step.

TASK 33 is complete: two ladders, budgets, floors, ambiguity, one-way with recovery behind a flag, the
relaunch, the copy, and the cost shed. 18 tests pin the ladders, the copy and the restart.
2026-11-09 13:05:44 -08:00
gramps 134103c08b feat(33): the step-down acts — one encoder relaunch, one toast, same URL
Slice 3. 589 green, 0 warnings.

- **`FramePump.ChangeEncoderAsync`** is a full pump stop/start, not an encoder swap. A swap leaves an
  in-flight submit pointing at a disposed process, and that faults the loop — which ends the
  broadcast, the one thing a quality step-down must never do. `StopAsync` already awaits the loop and
  flushes, so this reuses the tested path and costs one short freeze, which the ladder accepts.
- **`MainViewModel.StepDown`**: one planner per broadcast, subscribed to `StatsReported`, unsubscribed on
  every stop path. The pump thread only decides; the restart is posted to the dispatcher, because the
  act stops the loop that raised the event.
- **The creator's choice is not rewritten**: `_encoderTierOverride` changes what the encoder emits while
  the dropdown keeps showing what they picked, and the badge adds `· lowered` so the two never disagree
  silently.
- Copy comes from `StepDownPlanner.Describe` (machine cause owns it, upload cause blames the upload);
  the log gets the cause-tagged line for support.

Test: `EncoderRestartTests` runs a real pump with a fake encoder, changes the tier, and asserts a NEW
encoder started at the new fps/bitrate and produced a frame — the regression that would otherwise ship
as "it lowered the badge and the stream died".

Remaining for 33: shed our own capture cost before the fps step, and the session-report line.
2026-11-09 12:50:53 -08:00