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!(); }