//! NetDesk native CLI — a headless end-to-end test harness for the Rust core. //! Connects to a host through the real signaling server, completes the E2E //! handshake, and streams video, reporting fps / keyframes / bitrate. This is //! how the core is tested on CI and during the build-out, before the UI exists. //! //! Usage: //! netdesk [ws-url] [seconds] //! Example: //! netdesk 123456789 'secret' wss://netdesk.space/ws 10 use netdesk_core::decode::best_decoder_for; use netdesk_core::{connect, ConnectConfig, SessionEvent}; use std::time::{Duration, Instant}; use tokio::sync::mpsc; #[tokio::main] async fn main() -> anyhow::Result<()> { let args: Vec = std::env::args().collect(); if args.len() < 3 { eprintln!("usage: netdesk [ws-url] [seconds]"); std::process::exit(2); } let host_id = args[1].clone(); let password = args[2].clone(); let ws_url = args .get(3) .cloned() .unwrap_or_else(|| "wss://netdesk.space/ws".to_string()); let secs: u64 = args.get(4).and_then(|s| s.parse().ok()).unwrap_or(10); println!("NetDesk native core v{} (by {})", netdesk_core::VERSION, netdesk_core::VENDOR); println!("connecting to host {host_id} via {ws_url} ..."); let (tx, mut rx) = mpsc::unbounded_channel::(); let cfg = ConnectConfig { ws_url, host_id, password, totp: String::new(), takeover: false, }; let session = match connect(cfg, tx).await { Ok(s) => s, Err(e) => { eprintln!("connect failed: {e}"); std::process::exit(1); } }; // ask for an immediate keyframe so we start decoding fast session.request_keyframe(); // Report the monitors the host advertised (this is what drives the viewer's // monitor-switch UI). With `switch` as the 5th arg, flip to another monitor // mid-session to prove monitor.switch works end to end. let mons: Vec<_> = session.monitors().to_vec(); let cur = session.current_monitor(); println!( " monitors: {} [{}] current={cur}", mons.len(), mons.iter() .map(|m| format!("#{} {}x{}", m.i, m.w, m.h)) .collect::>() .join(", ") ); let do_switch = args.get(5).map(|s| s == "switch").unwrap_or(false); let mut switched = false; let start = Instant::now(); let mut frames = 0u64; let mut keyframes = 0u64; let mut bytes = 0u64; let mut connected = false; let mut last_report = Instant::now(); // Decode the stream to prove the pixels come through. Codec-aware: follows // the host's per-frame codec tag (H.264 / VP9), swapping on a keyframe. let mut cur_codec: u8 = 0; let mut decoder = best_decoder_for(cur_codec); let mut decoded = 0u64; let mut audio_pkts = 0u64; let mut audio_bytes = 0u64; let mut last_decoded_dim = (0usize, 0usize); let mut last_nonblack = 0u64; // non-black pixels in the last decoded frame loop { let remaining = Duration::from_secs(secs).checked_sub(start.elapsed()); let timeout = match remaining { Some(r) if !r.is_zero() => r, _ => break, }; match tokio::time::timeout(timeout, rx.recv()).await { Ok(Some(ev)) => match ev { SessionEvent::Connecting => println!(" . connecting"), SessionEvent::Challenged { e2e } => println!(" . challenged (e2e={e2e})"), SessionEvent::Connected { host_name, e2e, w, h } => { connected = true; println!(" ✓ CONNECTED to \"{host_name}\" {w}x{h} e2e={e2e}"); } SessionEvent::Video { keyframe, au, nals, codec } => { frames += 1; bytes += au.len() as u64; if keyframe { keyframes += 1; } // Live monitor-switch test: after a few frames, flip to a // different monitor and watch the stream follow. if do_switch && !switched && frames == 30 && mons.len() > 1 { let other = mons.iter().map(|m| m.i).find(|&i| i != cur).unwrap_or(cur); println!(" --> monitor.switch to #{other}"); session.monitor_switch(other); switched = true; } if codec != cur_codec && keyframe { println!(" codec switch -> {}", if codec == 1 { "VP9" } else { "H.264" }); decoder = best_decoder_for(codec); cur_codec = codec; } if codec == cur_codec { match decoder.decode(&au) { Ok(Some(frame)) => { decoded += 1; last_decoded_dim = (frame.w, frame.h); last_nonblack = frame.rgb.chunks_exact(3).filter(|p| p[0] | p[1] | p[2] != 0).count() as u64; } Ok(None) => {} Err(e) => { if decoded == 0 && frames <= 3 { eprintln!(" decode error: {e}"); } } } } if last_report.elapsed() >= Duration::from_secs(1) { let secs_f = start.elapsed().as_secs_f64().max(0.001); println!( " {:>4} rx | {:>4} decoded | {:>3} key | {:>5.1} fps | {:>6.2} Mbps | last {}x{} ({} NAL, {} lit px)", frames, decoded, keyframes, decoded as f64 / secs_f, (bytes as f64 * 8.0) / secs_f / 1_000_000.0, last_decoded_dim.0, last_decoded_dim.1, nals, last_nonblack ); last_report = Instant::now(); } } SessionEvent::Audio { data } => { audio_pkts += 1; audio_bytes += data.len() as u64; } SessionEvent::Denied { reason } => { eprintln!(" ✗ DENIED: {reason}"); std::process::exit(1); } SessionEvent::Ended { reason } => { println!(" session ended: {reason}"); break; } // New control-protocol events are not exercised by this probe. _ => {} }, Ok(None) => break, Err(_) => break, // overall timeout } } session.close(); let secs_f = start.elapsed().as_secs_f64().max(0.001); println!("\n==== RESULT ===="); println!("connected : {connected}"); println!("audio : {audio_pkts} Opus pkts ({audio_bytes} B)"); println!("frames rx : {frames} ({:.1} fps)", frames as f64 / secs_f); println!("decoded : {decoded} frames (last {}x{}, {} lit px)", last_decoded_dim.0, last_decoded_dim.1, last_nonblack); println!("keyframes : {keyframes}"); println!("video : {:.2} Mbps avg", (bytes as f64 * 8.0) / secs_f / 1_000_000.0); if connected && decoded > 0 && last_nonblack > 0 { println!("STATUS : OK — native core decoded live H.264 to real pixels ({}x{})", last_decoded_dim.0, last_decoded_dim.1); } else if connected && frames > 0 { println!("STATUS : stream received but not decoded (decoder issue)"); } else if connected { println!("STATUS : CONNECTED but no frames (host idle? request keyframe?)"); } else { println!("STATUS : FAILED to establish a session"); std::process::exit(1); } Ok(()) }