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:
+26
-7
@@ -1,6 +1,7 @@
|
||||
pub mod audio;
|
||||
mod capture;
|
||||
mod pipeline;
|
||||
mod quality;
|
||||
mod serve;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
@@ -20,6 +21,7 @@ use crate::common::{
|
||||
};
|
||||
|
||||
use self::pipeline::CaptureHandle;
|
||||
use self::quality::EffectiveQuality;
|
||||
|
||||
/// Messages from per-viewer tasks to the capture supervisor.
|
||||
enum SupervisorMsg {
|
||||
@@ -43,7 +45,15 @@ pub async fn run(opts: HostOpts) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let resolution = resolve_max_viewers(&opts);
|
||||
// Resolve quality first: Auto sizes its bandwidth budget against the viewer
|
||||
// cap the host will honor. To avoid a circular dependency (the auto-derived
|
||||
// cap itself depends on bitrate), Auto sizes against the user's explicit
|
||||
// --max-viewers when given, else a single viewer. The resulting effective
|
||||
// bitrate then feeds the cap resolution below.
|
||||
let sizing_viewers = opts.max_viewers.filter(|&n| n > 0).unwrap_or(1);
|
||||
let quality = quality::resolve(&opts, sizing_viewers);
|
||||
|
||||
let resolution = resolve_max_viewers(&opts, quality.bitrate);
|
||||
if resolution.value == 0 {
|
||||
bail!("--max-viewers must be at least 1");
|
||||
}
|
||||
@@ -76,10 +86,16 @@ pub async fn run(opts: HostOpts) -> Result<()> {
|
||||
EndpointAddr::new(addr.id).with_addrs(addr.addrs.iter().filter(|a| a.is_relay()).cloned());
|
||||
let ticket = EndpointTicket::new(relay_only);
|
||||
let clipboard_ok = opts.interactive && copy_to_clipboard(&ticket.to_string());
|
||||
print_host_banner(&ticket, display, &opts, &resolution, clipboard_ok);
|
||||
print_host_banner(&ticket, display, &opts, &quality, &resolution, clipboard_ok);
|
||||
|
||||
let (sup_tx, sup_rx) = mpsc::channel::<SupervisorMsg>(16);
|
||||
let supervisor = tokio::spawn(supervise(opts.clone(), display, resolution.value, sup_rx));
|
||||
let supervisor = tokio::spawn(supervise(
|
||||
opts.clone(),
|
||||
quality,
|
||||
display,
|
||||
resolution.value,
|
||||
sup_rx,
|
||||
));
|
||||
|
||||
accept_loop(&endpoint, sup_tx.clone(), cancel.clone()).await;
|
||||
|
||||
@@ -187,6 +203,7 @@ async fn handle_peer(
|
||||
/// the count is already at the cap.
|
||||
async fn supervise(
|
||||
opts: HostOpts,
|
||||
quality: EffectiveQuality,
|
||||
display: DisplayServer,
|
||||
max_viewers: u32,
|
||||
mut rx: mpsc::Receiver<SupervisorMsg>,
|
||||
@@ -206,7 +223,7 @@ async fn supervise(
|
||||
|
||||
if handle.is_none() {
|
||||
tracing::info!("first viewer arriving — spawning capture");
|
||||
match capture::spawn(display, &opts).await {
|
||||
match capture::spawn(display, &opts, &quality).await {
|
||||
Ok(h) => handle = Some(h),
|
||||
Err(e) => {
|
||||
let _ = reply.send(Err(format!("capture spawn failed: {e:#}")));
|
||||
@@ -243,6 +260,7 @@ fn print_host_banner(
|
||||
ticket: &EndpointTicket,
|
||||
display: DisplayServer,
|
||||
opts: &HostOpts,
|
||||
quality: &EffectiveQuality,
|
||||
resolution: &MaxViewersResolution,
|
||||
clipboard_ok: bool,
|
||||
) {
|
||||
@@ -250,7 +268,8 @@ fn print_host_banner(
|
||||
eprintln!("┌─ PixelPass · host ─────────────────────────────────────────");
|
||||
eprintln!("│ display server : {display:?}");
|
||||
eprintln!("│ capture : {}", capture_summary(opts));
|
||||
eprintln!("│ bitrate / fps : {} kbps @ {} fps", opts.bitrate, opts.framerate);
|
||||
eprintln!("│ quality : {} — {}", quality.label, quality.dimensions_summary());
|
||||
eprintln!("│ ({})", quality.note);
|
||||
eprintln!("│ hw encode : {}", if opts.no_hwencode { "off (software x264)" } else { "on (VAAPI H.264)" });
|
||||
eprintln!("│ max viewers : {} ({})", resolution.value, resolution.source.label());
|
||||
eprintln!("│");
|
||||
@@ -302,7 +321,7 @@ impl MaxViewersSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_max_viewers(opts: &HostOpts) -> MaxViewersResolution {
|
||||
fn resolve_max_viewers(opts: &HostOpts, effective_bitrate: u32) -> MaxViewersResolution {
|
||||
if let Some(n) = opts.max_viewers {
|
||||
return MaxViewersResolution {
|
||||
value: n,
|
||||
@@ -313,7 +332,7 @@ fn resolve_max_viewers(opts: &HostOpts) -> MaxViewersResolution {
|
||||
&& cfg.bandwidth.status == BandwidthStatus::Measured
|
||||
&& let Some(upstream) = cfg.bandwidth.upstream_mbps
|
||||
{
|
||||
let n = bandwidth::recommended_max_viewers(upstream, opts.bitrate);
|
||||
let n = bandwidth::recommended_max_viewers(upstream, effective_bitrate);
|
||||
return MaxViewersResolution {
|
||||
value: n,
|
||||
source: MaxViewersSource::BandwidthMeasurement { safe_mbps: upstream },
|
||||
|
||||
Reference in New Issue
Block a user