Files
pixelpass/src/viewer/mod.rs
T
molluskandClaude Opus 4.8 eb077d81f0 feat(relay): add --relay / PIXELPASS_RELAY override
Both host and viewer hardcoded presets::N0, pinning every session to the
bundled relays (which on iroh rc.0 are the canary-grade defaults). Add a
shared common::endpoint::bind() that keeps N0's DNS discovery + crypto but
swaps in a RelayMode::Custom single-relay map when --relay (or the
PIXELPASS_RELAY env var, so GUI children inherit it) is set.

Lets users point at a self-hosted relay or staging today; the production
relays (*.relay.iroh.network) speak a newer protocol that rc.0 rejects
("invalid iroh-relay version header"), so they only become usable — and
the default — after an iroh GA bump. Verified: override connects cleanly
through staging; bad URLs are rejected before any network work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 15:59:36 -04:00

111 lines
4.7 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");
crate::common::tunnel::bridge(quic_send, quic_recv, tcp).await
}
_ = 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 --untimed --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!();
}