host/x11: default to XDamage capture; drop --untimed from viewers
X11 full-desktop capture used `ximagesrc use-damage=false`, which copies the whole root window every frame. On servers without working MIT-SHM (and CPU-bound everywhere else) this collapses to ~1 fps — a field test over an xlibre host played back at roughly one frame per minute. Default to `use-damage=true` (XDamage re-grabs only changed regions); keep `PIXELPASS_X11_NO_DAMAGE=1` as an escape hatch for driver artifacts. Also drop `--untimed` from both mpv invocations (viewer banner + the interactive launcher). `--untimed` displays each frame as it decodes and ignores audio timestamps, which drifts a shared *video* progressively out of sync with its audio. Pacing to the audio clock keeps A/V synced at a negligible latency cost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -280,7 +280,9 @@ impl Drop for Routing {
|
|||||||
/// initial `lost`. In every other mode (whole-desktop, or best-effort app
|
/// initial `lost`. In every other mode (whole-desktop, or best-effort app
|
||||||
/// filtering) the loopback keeps audio flowing from the outset, so there is no
|
/// filtering) the loopback keeps audio flowing from the outset, so there is no
|
||||||
/// initial gap to report. Pure: no I/O, so the emit decision is unit-testable.
|
/// initial gap to report. Pure: no I/O, so the emit decision is unit-testable.
|
||||||
pub(super) fn initial_app_audio_state(opts: &HostOpts) -> Option<crate::common::output::AppAudioState> {
|
pub(super) fn initial_app_audio_state(
|
||||||
|
opts: &HostOpts,
|
||||||
|
) -> Option<crate::common::output::AppAudioState> {
|
||||||
(opts.app.is_some() && opts.strict_audio).then_some(crate::common::output::AppAudioState::Lost)
|
(opts.app.is_some() && opts.strict_audio).then_some(crate::common::output::AppAudioState::Lost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -523,7 +523,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn capture_summary_reflects_audio_mode() {
|
fn capture_summary_reflects_audio_mode() {
|
||||||
assert_eq!(capture_summary(&opts(None, false)), "fullscreen + system-audio");
|
assert_eq!(
|
||||||
|
capture_summary(&opts(None, false)),
|
||||||
|
"fullscreen + system-audio"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
capture_summary(&opts(Some("Firefox"), false)),
|
capture_summary(&opts(Some("Firefox"), false)),
|
||||||
"fullscreen + app-audio=Firefox"
|
"fullscreen + app-audio=Firefox"
|
||||||
@@ -533,7 +536,10 @@ mod tests {
|
|||||||
capture_summary(&opts(Some("Firefox"), true)),
|
capture_summary(&opts(Some("Firefox"), true)),
|
||||||
"fullscreen + app-audio=Firefox (strict)"
|
"fullscreen + app-audio=Firefox (strict)"
|
||||||
);
|
);
|
||||||
assert_eq!(capture_summary(&opts(None, true)), "fullscreen + system-audio");
|
assert_eq!(
|
||||||
|
capture_summary(&opts(None, true)),
|
||||||
|
"fullscreen + system-audio"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+14
-4
@@ -37,12 +37,22 @@ pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result<Captur
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// XDamage capture (`use-damage=true`) only re-grabs changed screen
|
||||||
|
// regions instead of copying the whole root window every frame. On a busy
|
||||||
|
// desktop that is the difference between a usable framerate and ~1 fps —
|
||||||
|
// `use-damage=false` does a full XGetImage per frame, which collapses on
|
||||||
|
// servers without working MIT-SHM (and pins the CPU everywhere else).
|
||||||
|
// Kept as the default; `PIXELPASS_X11_NO_DAMAGE=1` restores full-frame
|
||||||
|
// capture if a driver produces partial-update artifacts with damage on.
|
||||||
|
let use_damage = if std::env::var_os("PIXELPASS_X11_NO_DAMAGE").is_some() {
|
||||||
|
"use-damage=false"
|
||||||
|
} else {
|
||||||
|
"use-damage=true"
|
||||||
|
};
|
||||||
let mut source_args = vec![
|
let mut source_args = vec![
|
||||||
"ximagesrc".to_string(),
|
"ximagesrc".to_string(),
|
||||||
// Full frames (no damage regions) to avoid partial-update artifacts;
|
// show-pointer matches Wayland's CursorMode::Embedded.
|
||||||
// use-damage=true is a later CPU optimization. show-pointer matches
|
use_damage.to_string(),
|
||||||
// Wayland's CursorMode::Embedded.
|
|
||||||
"use-damage=false".to_string(),
|
|
||||||
"show-pointer=true".to_string(),
|
"show-pointer=true".to_string(),
|
||||||
];
|
];
|
||||||
if let Some(xid) = xid {
|
if let Some(xid) = xid {
|
||||||
|
|||||||
+2
-1
@@ -279,8 +279,9 @@ impl Player {
|
|||||||
Player::Mpv => crate::common::process::spawn_detached(
|
Player::Mpv => crate::common::process::spawn_detached(
|
||||||
"mpv",
|
"mpv",
|
||||||
&[
|
&[
|
||||||
|
// No `--untimed`: it ignores audio timestamps and drifts a
|
||||||
|
// shared video out of sync. Pacing to audio keeps A/V synced.
|
||||||
"--profile=low-latency",
|
"--profile=low-latency",
|
||||||
"--untimed",
|
|
||||||
"--hwdec=auto",
|
"--hwdec=auto",
|
||||||
"--audio-buffer=0.2",
|
"--audio-buffer=0.2",
|
||||||
"--demuxer-max-bytes=2M",
|
"--demuxer-max-bytes=2M",
|
||||||
|
|||||||
+3
-1
@@ -172,7 +172,9 @@ fn loopback_capture_pid(args: &str) -> Option<u32> {
|
|||||||
let from_source = extract_kv(args, "source")
|
let from_source = extract_kv(args, "source")
|
||||||
.and_then(|v| v.strip_prefix(SINK_NAME_PREFIX))
|
.and_then(|v| v.strip_prefix(SINK_NAME_PREFIX))
|
||||||
.and_then(|rest| rest.strip_suffix(".monitor"));
|
.and_then(|rest| rest.strip_suffix(".monitor"));
|
||||||
from_sink.or(from_source).and_then(|pid| pid.parse::<u32>().ok())
|
from_sink
|
||||||
|
.or(from_source)
|
||||||
|
.and_then(|pid| pid.parse::<u32>().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_kv<'a>(args: &'a str, key: &str) -> Option<&'a str> {
|
fn extract_kv<'a>(args: &'a str, key: &str) -> Option<&'a str> {
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ fn print_viewer_banner(url: &str) {
|
|||||||
eprintln!("│ Connected to host. Open the stream in your player:");
|
eprintln!("│ Connected to host. Open the stream in your player:");
|
||||||
eprintln!("│");
|
eprintln!("│");
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"│ mpv --profile=low-latency --untimed --hwdec=auto --audio-buffer=0.2 --demuxer-max-bytes=2M --demuxer-readahead-secs=0.5 {url}"
|
"│ mpv --profile=low-latency --hwdec=auto --audio-buffer=0.2 --demuxer-max-bytes=2M --demuxer-readahead-secs=0.5 {url}"
|
||||||
);
|
);
|
||||||
eprintln!("│ vlc --network-caching=200 --live-caching=200 {url}");
|
eprintln!("│ vlc --network-caching=200 --live-caching=200 {url}");
|
||||||
eprintln!("│");
|
eprintln!("│");
|
||||||
|
|||||||
Reference in New Issue
Block a user