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,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(())
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in new issue
Block a user