NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 00:47:49 +00:00
commit 21dea33089
227 files changed
+80714

No files matched your search

+203
View File
@@ -0,0 +1,203 @@
//! Encode correctness + honest timing on synthetic MOVING content (independent
//! of whether the live desktop is active). Encodes N frames, decodes them back
//! with the real viewer decoder, and asserts the roundtrip reproduces the right
//! dimensions — proving the Annex-B the host emits is valid for the viewer.
//!
//! Usage: netdesk-host-encbench [sw|mf] (default sw = openh264 software)
use std::time::Instant;
fn make_frame(w: usize, h: usize, t: usize) -> Vec<u8> {
let mut buf = vec![0u8; w * h * 4];
let bx = (t * 13) % (w.saturating_sub(120).max(1));
let by = (t * 7) % (h.saturating_sub(120).max(1));
for y in 0..h {
for x in 0..w {
let i = (y * w + x) * 4;
let inbox = x >= bx && x < bx + 120 && y >= by && y < by + 120;
if inbox {
buf[i] = 255;
buf[i + 1] = 255;
buf[i + 2] = 255;
buf[i + 3] = 255;
} else {
buf[i] = ((x + t * 3) & 0xff) as u8;
buf[i + 1] = ((y + t * 2) & 0xff) as u8;
buf[i + 2] = ((x + y + t) & 0xff) as u8;
buf[i + 3] = 255;
}
}
}
buf
}
/// Mean (R,G,B) of a decoded row.
fn row_means(rgb: &[u8], w: usize, row: usize) -> (f64, f64, f64) {
let base = row * w * 3;
let (mut r, mut g, mut b) = (0u64, 0u64, 0u64);
for col in 0..w {
let o = base + col * 3;
r += rgb[o] as u64;
g += rgb[o + 1] as u64;
b += rgb[o + 2] as u64;
}
let n = w as f64;
(r as f64 / n, g as f64 / n, b as f64 / n)
}
/// Solid gray frame (128,128,128) with a moving white box, so a correct decode
/// is gray everywhere and any green row stands out immediately.
fn gray_frame(w: usize, h: usize, t: usize) -> Vec<u8> {
let mut buf = vec![128u8; w * h * 4];
for p in buf.chunks_exact_mut(4) {
p[3] = 255;
}
let bx = (t * 11) % (w.saturating_sub(100).max(1));
for y in 0..100.min(h) {
for x in bx..(bx + 100).min(w) {
let i = (y * w + x) * 4;
buf[i] = 255;
buf[i + 1] = 255;
buf[i + 2] = 255;
}
}
buf
}
fn main() -> anyhow::Result<()> {
let mode = std::env::args().nth(1).unwrap_or_else(|| "sw".into());
let (w, h) = (1920usize, 1080usize);
let n = 120usize;
println!("encode roundtrip [{}]: {}x{}, {} frames", mode, w, h, n);
// Green-line hunt: QSV-encode gray frames, decode through the HARDWARE
// decoder (best_decoder), and scan every decoded row for anomalous green.
#[cfg(windows)]
if mode == "mfdec" {
let _ = tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).try_init();
let w = std::env::var("ND_W").ok().and_then(|s| s.parse().ok()).unwrap_or(w);
let h = std::env::var("ND_H").ok().and_then(|s| s.parse().ok()).unwrap_or(h);
println!(" mfdec dims: {}x{}", w, h);
let mut enc = netdesk_host::encode_mf::MfEncoder::new(w, h, 60, 8_000_000)?;
let mut dec = netdesk_core::decode::best_decoder();
println!("encoder: {}", enc.name());
let mut aus: Vec<Vec<u8>> = Vec::new();
for t in 0..30 {
for ef in enc.encode(&gray_frame(w, h, t))? {
aus.push(ef.data);
}
}
for ef in enc.drain()? {
aus.push(ef.data);
}
let mut frames_checked = 0usize;
let mut green_rows_total = 0usize;
let mut first_report = String::new();
for au in &aus {
if let Some(fr) = dec.decode(au)? {
frames_checked += 1;
let mut green_here = 0usize;
let mut first = None;
for row in 0..fr.h {
let (r, g, b) = row_means(&fr.rgb, fr.w, row);
// "green" = G clearly above both R and B (gray/white have R≈G≈B).
if g - r > 40.0 && g - b > 40.0 {
green_here += 1;
if first.is_none() {
first = Some((row, r, g, b));
}
}
}
green_rows_total += green_here;
if green_here > 0 && first_report.is_empty() {
let (row, r, g, b) = first.unwrap();
first_report = format!(
"frame {}x{}: {} green rows; first row {} rgb=({:.0},{:.0},{:.0})",
fr.w, fr.h, green_here, row, r, g, b
);
}
}
}
println!("decoded {} frames; total green rows: {}", frames_checked, green_rows_total);
if green_rows_total == 0 {
println!("NO GREEN LINES via hardware decode");
} else {
println!("GREEN DETECTED: {}", first_report);
}
return Ok(());
}
let mut dec = netdesk_core::decode::H264Decoder::new()?;
let mut keyframes = 0usize;
let mut decoded_ok = 0usize;
let mut total_bytes = 0usize;
let mut enc_ns = 0u128;
let mut emitted = 0usize;
// Collect all AUs (with timing) then decode, so the two encoders share one path.
let mut aus: Vec<(Vec<u8>, bool)> = Vec::new();
#[cfg(windows)]
if mode == "mf" {
let mut enc = match netdesk_host::encode_mf::MfEncoder::new(w, h, 60, 8_000_000) {
Ok(e) => e,
Err(e) => {
println!("no hardware encoder ({e}); skipping mf test");
return Ok(());
}
};
println!("hardware encoder: {} (hw={})", enc.name(), enc.is_hardware());
for t in 0..n {
let frame = make_frame(w, h, t);
let t0 = Instant::now();
let outs = enc.encode(&frame)?;
enc_ns += t0.elapsed().as_nanos();
for ef in outs {
aus.push((ef.data, ef.keyframe));
}
}
for ef in enc.drain()? {
aus.push((ef.data, ef.keyframe));
}
}
if mode != "mf" {
let mut enc = netdesk_host::encode::H264Encoder::new(w, h, 60, 8_000_000)?;
for t in 0..n {
let frame = make_frame(w, h, t);
let t0 = Instant::now();
let out = enc.encode(&frame)?;
enc_ns += t0.elapsed().as_nanos();
if let Some(ef) = out {
aus.push((ef.data, ef.keyframe));
}
}
}
for (data, keyframe) in &aus {
emitted += 1;
total_bytes += data.len();
if *keyframe {
keyframes += 1;
}
if let Some(fr) = dec.decode(data)? {
if fr.w == w && fr.h == h {
decoded_ok += 1;
}
}
}
let enc_ms = enc_ns as f64 / n as f64 / 1e6;
println!(" encode avg: {:.2} ms/frame (~{:.0} fps)", enc_ms, 1000.0 / enc_ms);
println!(" AUs emitted: {} keyframes: {} decoded_ok: {}", emitted, keyframes, decoded_ok);
println!(" avg AU: {:.1} KB", total_bytes as f64 / emitted.max(1) as f64 / 1024.0);
if keyframes == 0 {
anyhow::bail!("no keyframe emitted");
}
if decoded_ok < emitted.saturating_sub(2) {
anyhow::bail!("roundtrip decode failed: {}/{} decoded", decoded_ok, emitted);
}
println!("ROUNDTRIP OK");
Ok(())
}
+76
View File
@@ -0,0 +1,76 @@
//! Standalone NetDesk native host engine. Captures the screen and serves
//! viewers over the NetDesk protocol with the best available H.264 encoder and
//! SendInput injection. Configured via environment:
//! ND_HOST_ID (required) the id viewers connect to
//! ND_PASSWORD (required) session password (never leaves this machine)
//! ND_WS_URL default wss://netdesk.space/ws
//! ND_NAME default "NetDesk (Rust)"
//! ND_MONITOR default 0
//! ND_FPS default 60
//! ND_BITRATE default 8000000
fn main() -> anyhow::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
#[cfg(windows)]
{
let host_id = std::env::var("ND_HOST_ID").map_err(|_| anyhow::anyhow!("set ND_HOST_ID"))?;
let password =
std::env::var("ND_PASSWORD").map_err(|_| anyhow::anyhow!("set ND_PASSWORD"))?;
let cfg = netdesk_host::run::EngineConfig {
ws_url: std::env::var("ND_WS_URL").unwrap_or_else(|_| "wss://netdesk.space/ws".into()),
host_id,
name: std::env::var("ND_NAME").unwrap_or_else(|_| "NetDesk (Rust)".into()),
password,
monitor: envnum("ND_MONITOR", 0),
fps: envnum("ND_FPS", 60),
bitrate: envnum("ND_BITRATE", 8_000_000),
// Host-enforced permissions + approve-before-connect (see run.rs):
// ND_PERM_INPUT / ND_PERM_CLIPBOARD / ND_PERM_FILES / ND_PERM_AUDIO
// (default 1) and ND_APPROVE (default 0).
perms: netdesk_core::host::HostPerms {
input: envflag("ND_PERM_INPUT", true),
clipboard: envflag("ND_PERM_CLIPBOARD", true),
files: envflag("ND_PERM_FILES", true),
audio: envflag("ND_PERM_AUDIO", true),
},
approve: envflag("ND_APPROVE", false),
totp_secret: std::env::var("ND_TOTP").unwrap_or_default(),
// Encrypted direct-IP: ND_DIRECT_PORT makes the host listen on that
// TCP port for a viewer that dials it directly (no relay);
// ND_ALLOW_IPS is an optional CIDR allow-list.
direct_port: std::env::var("ND_DIRECT_PORT").ok().and_then(|s| s.parse().ok()),
allow_ips: std::env::var("ND_ALLOW_IPS").unwrap_or_default(),
// ND_MAX_VIEWERS>1 allows multiple simultaneous direct-IP viewers
// (1 controller + N observers of the one capture).
max_viewers: std::env::var("ND_MAX_VIEWERS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1)
.max(1),
};
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let handle = netdesk_host::run::EngineHandle::new();
rt.block_on(netdesk_host::run::run_engine(cfg, handle))?;
}
#[cfg(not(windows))]
println!("engine is Windows-only");
Ok(())
}
#[cfg(windows)]
fn envnum<T: std::str::FromStr>(key: &str, default: T) -> T {
std::env::var(key).ok().and_then(|s| s.parse().ok()).unwrap_or(default)
}
/// "1"/"true"/"yes" = true, "0"/"false"/"no" = false, unset = `default`.
#[cfg(windows)]
fn envflag(key: &str, default: bool) -> bool {
match std::env::var(key).as_deref().map(str::trim) {
Ok("1") | Ok("true") | Ok("yes") => true,
Ok("0") | Ok("false") | Ok("no") => false,
_ => default,
}
}
+146
View File
@@ -0,0 +1,146 @@
//! Privacy-mode feasibility probe. Determines whether a window marked
//! `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)` is hidden from THIS host's
//! own screen capture — both DXGI Desktop Duplication (`grab`) and the GDI
//! BitBlt fallback (`grab_seed`). If it is, a fullscreen black excluded window
//! is a real, driver-free privacy mode: the local monitor shows black while the
//! remote operator still captures the desktop behind it.
//!
//! Method: show a small solid-magenta popup at a known spot, capture with the
//! affinity OFF (control — magenta must appear) then ON (magenta must vanish),
//! and count magenta pixels in each frame. Safe: the window is small and shown
//! for ~2s, then destroyed. Developed by Akronlogic.
#![cfg(windows)]
use std::time::{Duration, Instant};
use windows::core::w;
use windows::Win32::Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, PeekMessageW, RegisterClassW,
SetWindowDisplayAffinity, ShowWindow, TranslateMessage, MSG, PM_REMOVE, SW_SHOW,
WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WNDCLASSW, WS_EX_TOPMOST, WS_POPUP, WS_VISIBLE,
};
use netdesk_host::capture::DxgiCapturer;
unsafe extern "system" fn wndproc(h: HWND, m: u32, w: WPARAM, l: LPARAM) -> LRESULT {
DefWindowProcW(h, m, w, l)
}
fn pump(ms: u64) {
let end = Instant::now() + Duration::from_millis(ms);
while Instant::now() < end {
unsafe {
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
std::thread::sleep(Duration::from_millis(15));
}
}
/// Count magenta (B=255,G=0,R=255 with tolerance) pixels in a BGRA buffer.
fn count_magenta(buf: &[u8]) -> usize {
buf.chunks_exact(4)
.filter(|p| p[0] >= 250 && p[1] <= 6 && p[2] >= 250)
.count()
}
/// Freshest DXGI frame over `ms` (None if the duplication reported no change).
fn freshest_dxgi(cap: &mut DxgiCapturer, ms: u64) -> Option<Vec<u8>> {
let end = Instant::now() + Duration::from_millis(ms);
let mut last = None;
while Instant::now() < end {
if let Ok(Some(f)) = cap.grab(80) {
last = Some(f);
}
}
last
}
fn main() {
unsafe {
let hinst = HINSTANCE::default();
let brush = CreateSolidBrush(COLORREF(0x00FF_00FF)); // B=FF,G=00,R=FF = magenta
let class = w!("NdPrivacyProbe");
let wc = WNDCLASSW {
lpfnWndProc: Some(wndproc),
hInstance: hinst,
lpszClassName: class,
hbrBackground: brush,
..Default::default()
};
let atom = RegisterClassW(&wc);
if atom == 0 {
eprintln!("RegisterClassW failed");
std::process::exit(1);
}
let hwnd = match CreateWindowExW(
WS_EX_TOPMOST,
class,
w!("nd-privacy-probe"),
WS_POPUP | WS_VISIBLE,
120,
120,
520,
380,
None,
None,
hinst,
None,
) {
Ok(h) => h,
Err(e) => {
eprintln!("CreateWindowExW failed: {e}");
std::process::exit(1);
}
};
let _ = ShowWindow(hwnd, SW_SHOW);
pump(400);
let mut cap = match DxgiCapturer::new(0) {
Ok(c) => c,
Err(e) => {
eprintln!("DxgiCapturer::new failed: {e}");
let _ = DestroyWindow(hwnd);
std::process::exit(1);
}
};
// CONTROL: affinity OFF — the window must be visible to both captures.
let _ = SetWindowDisplayAffinity(hwnd, WDA_NONE);
pump(300);
let ctl_gdi = cap.grab_seed().map(|b| count_magenta(&b)).unwrap_or(usize::MAX);
let ctl_dxgi = freshest_dxgi(&mut cap, 700).map(|b| count_magenta(&b));
// EXCLUDE: affinity ON — the window must vanish from capture.
let _ = SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
pump(400);
let exc_gdi = cap.grab_seed().map(|b| count_magenta(&b)).unwrap_or(usize::MAX);
let exc_dxgi = freshest_dxgi(&mut cap, 900).map(|b| count_magenta(&b));
let _ = DestroyWindow(hwnd);
println!("PRIVACY_PROBE control_gdi_magenta={ctl_gdi} exclude_gdi_magenta={exc_gdi}");
println!(
"PRIVACY_PROBE control_dxgi_magenta={:?} exclude_dxgi_magenta={:?}",
ctl_dxgi, exc_dxgi
);
let gdi_ok = ctl_gdi > 1000 && exc_gdi == 0;
// DXGI: control should show magenta; exclude should show none. If the
// duplication reported no change in a phase (None), note it.
let dxgi_ok = matches!((ctl_dxgi, exc_dxgi), (Some(c), Some(e)) if c > 1000 && e == 0);
if gdi_ok && dxgi_ok {
println!("VERDICT: PRIVACY_WDA_WORKS — WDA_EXCLUDEFROMCAPTURE hides the window from BOTH DXGI and GDI capture");
} else if gdi_ok {
println!("VERDICT: PRIVACY_WDA_GDI_ONLY — GDI excludes it; DXGI result {:?}/{:?} (inconclusive or not excluded)", ctl_dxgi, exc_dxgi);
} else {
println!("VERDICT: PRIVACY_WDA_FAILS — the window was still captured; privacy needs an IDD virtual display");
}
}
}
+75
View File
@@ -0,0 +1,75 @@
//! Capture + encode benchmark: open DXGI duplication on monitor 0, encode each
//! frame to H.264, and report per-stage timing + bitstream sizes. Proves the
//! native capture->encode pipeline works and shows where we stand on latency.
#[cfg(windows)]
use std::time::Instant;
fn main() -> anyhow::Result<()> {
#[cfg(windows)]
{
netdesk_host::set_dpi_aware();
let mut cap = netdesk_host::capture::DxgiCapturer::new(0)?;
let (w, h) = (cap.width() as usize, cap.height() as usize);
println!("capture: {}x{}", w, h);
let mut enc = netdesk_host::encode::H264Encoder::new(w, h, 60, 8_000_000)?;
let mut last: Option<Vec<u8>> = None;
let mut frames = 0usize;
let mut keyframes = 0usize;
let mut cap_ns = 0u128;
let mut enc_ns = 0u128;
let mut total_bytes = 0usize;
let mut encoded = 0usize;
// warm up + run ~150 iterations
for i in 0..150 {
let t0 = Instant::now();
let frame = match cap.grab(50)? {
Some(buf) => {
last = Some(buf);
last.as_ref().unwrap()
}
None => match last.as_ref() {
Some(b) => b, // screen unchanged: re-encode last (keeps fps up)
None => continue,
},
};
let t1 = Instant::now();
let out = enc.encode(frame)?;
let t2 = Instant::now();
if i >= 10 {
// skip warmup
frames += 1;
cap_ns += (t1 - t0).as_nanos();
enc_ns += (t2 - t1).as_nanos();
if let Some(ef) = &out {
encoded += 1;
total_bytes += ef.data.len();
if ef.keyframe {
keyframes += 1;
}
}
}
}
if frames == 0 {
anyhow::bail!("no frames");
}
let cap_ms = cap_ns as f64 / frames as f64 / 1e6;
let enc_ms = enc_ns as f64 / frames as f64 / 1e6;
println!("measured over {} frames ({} produced output, {} keyframes)", frames, encoded, keyframes);
println!(" capture avg: {:.2} ms", cap_ms);
println!(" encode avg: {:.2} ms", enc_ms);
println!(" pipeline: {:.2} ms (~{:.0} fps ceiling)", cap_ms + enc_ms, 1000.0 / (cap_ms + enc_ms));
if encoded > 0 {
let avg_kb = total_bytes as f64 / encoded as f64 / 1024.0;
println!(" avg AU size: {:.1} KB (~{:.1} Mbit/s @60fps)", avg_kb, avg_kb * 1024.0 * 8.0 * 60.0 / 1e6);
}
}
#[cfg(not(windows))]
println!("probe is Windows-only");
Ok(())
}