b0ff20fe3f
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>
122 lines
5.4 KiB
Rust
122 lines
5.4 KiB
Rust
use anyhow::{Context, Result, bail};
|
|
use iroh_tickets::endpoint::EndpointTicket;
|
|
use std::time::Duration;
|
|
use tokio::net::TcpListener;
|
|
|
|
use crate::cli::ViewerOpts;
|
|
use crate::common::{alpn::ALPN, endpoint, output, signal};
|
|
|
|
/// Cap on the initial QUIC connect. `endpoint.connect()` has no built-in
|
|
/// deadline, so an offline host / stale code / unreachable relay otherwise
|
|
/// hangs forever with no feedback (the silent "connecting…" failure mode).
|
|
/// Matches the host's 15s `online()` cap.
|
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
|
|
|
|
pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
|
|
let cancel = signal::install_ctrl_c();
|
|
|
|
let endpoint = endpoint::bind(opts.relay.as_deref()).await?;
|
|
|
|
let addr = ticket.endpoint_addr().clone();
|
|
tracing::info!(remote = %addr.id, "connecting to host");
|
|
|
|
// Bound the connect attempt and let ctrl-c abort it, so the viewer fails
|
|
// loud (and the GUI surfaces the error) instead of spinning indefinitely.
|
|
let conn = tokio::select! {
|
|
_ = cancel.cancelled() => {
|
|
tracing::info!("ctrl-c received before the connection was established");
|
|
endpoint.close().await;
|
|
return Ok(());
|
|
}
|
|
result = tokio::time::timeout(CONNECT_TIMEOUT, endpoint.connect(addr, ALPN)) => match result {
|
|
Ok(Ok(conn)) => conn,
|
|
Ok(Err(e)) => {
|
|
endpoint.close().await;
|
|
bail!("failed to connect to the host: {e:#}");
|
|
}
|
|
Err(_) => {
|
|
endpoint.close().await;
|
|
bail!(
|
|
"couldn't reach the host within {}s — it may be offline, the share \
|
|
code may be stale, or the relay may be unreachable. Check that the \
|
|
host is running, then re-copy the code and try again.",
|
|
CONNECT_TIMEOUT.as_secs()
|
|
);
|
|
}
|
|
},
|
|
};
|
|
// Everything past the established connection runs in one block so any error
|
|
// (open_bi, bind, local_addr, accept) is captured rather than `?`-propagated
|
|
// straight out of the function — that would skip the close below and leak the
|
|
// endpoint. The connect-phase arms above close explicitly for the same reason.
|
|
let result = async {
|
|
let (quic_send, quic_recv) = conn.open_bi().await?;
|
|
|
|
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
|
let port = listener.local_addr()?.port();
|
|
let url = format!("http://127.0.0.1:{port}");
|
|
output::emit(output::Event::Connected { url: &url });
|
|
|
|
if opts.interactive {
|
|
let player = crate::interactive::prompt_player()?;
|
|
player
|
|
.spawn(&url)
|
|
.with_context(|| "failed to launch player")?;
|
|
print_viewer_banner_interactive();
|
|
} else {
|
|
print_viewer_banner(&url);
|
|
}
|
|
|
|
tokio::select! {
|
|
accepted = listener.accept() => {
|
|
let (tcp, peer) = accepted?;
|
|
tracing::info!(%peer, "local viewer connected");
|
|
// Race the bridge against ctrl-c so a disconnect lands promptly
|
|
// mid-stream (mirrors the host's handle_peer). Without this, the
|
|
// cancel token is set but nothing checks it once the player has
|
|
// connected — ctrl-c is ignored until a second press, and a GUI
|
|
// "Disconnect" only takes effect via the child's SIGKILL backstop.
|
|
tokio::select! {
|
|
res = crate::common::tunnel::bridge(quic_send, quic_recv, tcp) => res,
|
|
_ = cancel.cancelled() => {
|
|
tracing::info!("ctrl-c received during stream — disconnecting");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
_ = cancel.cancelled() => {
|
|
tracing::info!("ctrl-c received before local viewer connected");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
.await;
|
|
|
|
endpoint.close().await;
|
|
result
|
|
}
|
|
|
|
fn print_viewer_banner(url: &str) {
|
|
eprintln!();
|
|
eprintln!("┌─ PixelPass · viewer ───────────────────────────────────────");
|
|
eprintln!("│ Connected to host. Open the stream in your player:");
|
|
eprintln!("│");
|
|
eprintln!(
|
|
"│ 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!("│");
|
|
eprintln!("│ Press Ctrl+C to disconnect.");
|
|
eprintln!("└────────────────────────────────────────────────────────────");
|
|
eprintln!();
|
|
}
|
|
|
|
fn print_viewer_banner_interactive() {
|
|
eprintln!();
|
|
eprintln!("┌─ PixelPass · viewer ───────────────────────────────────────");
|
|
eprintln!("│ Player launched. Close it (or press Ctrl+C here) to");
|
|
eprintln!("│ disconnect.");
|
|
eprintln!("└────────────────────────────────────────────────────────────");
|
|
eprintln!();
|
|
}
|