NetDesk 1.5.0 — source release
This commit is contained in:
commit
21dea33089
227 files changed
+80714
No files matched your search
@@ -0,0 +1,192 @@
|
||||
//! 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 <host-id> <password> [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<String> = std::env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("usage: netdesk <host-id> <password> [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::<SessionEvent>();
|
||||
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::<Vec<_>>()
|
||||
.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(())
|
||||
}
|
||||
Reference in new issue
Block a user