feat(quality): resolution/quality presets + Auto from pre-flight

Add a host-global quality knob (Discord-style) so the sharer can trade
resolution + bitrate for upload bandwidth. Quality is host-global by
design: one encode pipeline fans out to every viewer, so per-viewer
quality is out of scope (it would kill the broadcast fanout).

- New `--quality source|high|medium|low|auto` (ValueEnum) bundling a
  (max-height, bitrate, fps) tuple per preset; `auto` derives the preset
  from the saved bandwidth pre-flight (safe_mbps / viewer cap), falling
  back to `medium` when unmeasured. Default is auto; the interactive
  Host branch shows a picker when --quality is omitted (mirrors pick_app).
- `--max-height N` raw override; `--bitrate`/`--framerate` changed to
  Option so an explicit flag overrides just that field of the preset
  (precedence rule), leaving the rest of the preset intact.
- host/quality.rs: Preset table + resolve(); pure resolve_auto() split
  from the config read for testability. 5 unit tests lock preset
  pass-through, the Auto ladder, the unmeasured fallback, and override
  precedence.
- pipeline::build_args inserts `videoscale ! video/x-raw,height=N,
  pixel-aspect-ratio=1/1,width=[2,8192,2]` only for non-Source presets.
  PAR 1/1 forces a proportional downscale (without it videoscale keeps
  full width and squashes PAR — no bandwidth win); the even-stepped width
  range + even-rounded height satisfy H.264 4:2:0. EffectiveQuality is
  threaded capture -> wayland/x11 -> pipeline; max_viewers is now sized
  against the effective (post-preset) bitrate.
- Banner gains a quality line (preset label + ≤Np/kbps/fps + provenance).
- deps.rs checks `videoscale`; smoke-pipeline.sh adds a 1080->480
  downscale check asserting an even width below source.
- README: --quality preset table, Auto behavior, host-global note,
  --max-height/--bitrate/--framerate override precedence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:03:14 -04:00
parent 45e5d7ef37
commit 7483b9aae8
11 changed files with 503 additions and 33 deletions
+3 -2
View File
@@ -16,9 +16,10 @@ use nix::unistd::close;
use std::os::fd::{AsFd, IntoRawFd, OwnedFd, RawFd};
use super::pipeline::{self, CaptureHandle};
use super::quality::EffectiveQuality;
use crate::cli::HostOpts;
pub async fn start(opts: &HostOpts) -> Result<CaptureHandle> {
pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result<CaptureHandle> {
// 1. Negotiate the screencast session with the portal.
let proxy = Screencast::new()
.await
@@ -69,7 +70,7 @@ pub async fn start(opts: &HostOpts) -> Result<CaptureHandle> {
"do-timestamp=true".to_string(),
];
pipeline::spawn(opts, source_args, move || {
pipeline::spawn(opts, quality, source_args, move || {
// Parent no longer needs the pipewire fd — gst inherited its own copy.
let _ = close(raw_fd);
})