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
+48 -8
View File
@@ -25,13 +25,25 @@ pub struct Cli {
#[arg(long, value_enum)]
pub display_server: Option<DisplayServerArg>,
/// Encode bitrate in kbps.
#[arg(long, default_value_t = 6000)]
pub bitrate: u32,
/// Quality preset. Bundles a max video height, bitrate, and framerate.
/// `auto` derives them from the saved bandwidth pre-flight (falls back to
/// `medium` when no measurement exists). Defaults to `auto`; in the
/// interactive menu, omitting this shows a picker instead.
#[arg(long, value_enum)]
pub quality: Option<Quality>,
/// Capture framerate.
#[arg(long, default_value_t = 30)]
pub framerate: u32,
/// Cap the encoded video height (px); width follows the source aspect.
/// Power-user override — takes precedence over the preset's height.
#[arg(long, value_name = "N")]
pub max_height: Option<u32>,
/// Encode bitrate in kbps. Overrides the quality preset's bitrate.
#[arg(long)]
pub bitrate: Option<u32>,
/// Capture framerate. Overrides the quality preset's framerate.
#[arg(long)]
pub framerate: Option<u32>,
/// Disable VAAPI HW encode; force software x264.
#[arg(long)]
@@ -71,13 +83,37 @@ pub enum DisplayServerArg {
X11,
}
/// Quality preset. Each fixed preset bundles a (max-height, bitrate, fps)
/// tuple — resolution is a quality-per-bitrate knob, so the three only make
/// sense together. `Auto` has no fixed tuple; it picks one of the others from
/// the bandwidth pre-flight at host startup. See `host::quality`.
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum Quality {
/// Native source resolution, 6000 kbps, 30 fps (no downscale).
Source,
/// Up to 1080p, 4000 kbps, 30 fps.
High,
/// Up to 720p, 2500 kbps, 30 fps.
Medium,
/// Up to 480p, 1000 kbps, 30 fps.
Low,
/// Derive from the measured upstream; falls back to `medium` when unmeasured.
Auto,
}
#[derive(Debug, Clone)]
pub struct HostOpts {
pub window: bool,
pub app: Option<String>,
pub display_server: Option<DisplayServerArg>,
pub bitrate: u32,
pub framerate: u32,
/// Chosen preset (Auto = derive at startup). Defaults to Auto.
pub quality: Quality,
/// Raw `--bitrate` override (kbps); None = use the preset's bitrate.
pub bitrate: Option<u32>,
/// Raw `--framerate` override; None = use the preset's framerate.
pub framerate: Option<u32>,
/// Raw `--max-height` override (px); None = use the preset's height.
pub max_height: Option<u32>,
pub no_hwencode: bool,
pub max_viewers: Option<u32>,
pub interactive: bool,
@@ -95,8 +131,12 @@ impl Cli {
window: self.window,
app: self.app,
display_server: self.display_server,
// No `--quality` and nothing picked interactively → the documented
// default, Auto.
quality: self.quality.unwrap_or(Quality::Auto),
bitrate: self.bitrate,
framerate: self.framerate,
max_height: self.max_height,
no_hwencode: self.no_hwencode,
max_viewers: self.max_viewers,
interactive,