NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 02:02:46 +00:00
commit 40318aafdd
227 files changed
+80798

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
[package]
name = "netdesk-viewer"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
description = "NetDesk native viewer window — GPU-filtered (bilinear) remote-screen rendering with a full TeamViewer/RustDesk-style toolbar, built on eframe + egui (winit + glow). Developed by Akronlogic."
[[bin]]
name = "netdesk-viewer"
path = "src/main.rs"
[dependencies]
netdesk-core = { path = "../core" }
tokio = { workspace = true }
anyhow = { workspace = true }
# control-protocol payloads (session.info / sys.res) arrive as JSON strings from
# the core; parse them in the net thread into clean Rust before the UI sees them.
serde_json = { workspace = true }
# host-control module (host_ctl.rs) models the shared config.json with derive.
serde = { workspace = true }
# host-control module: blocking HTTPS (rustls) to the relay /api — account
# sign-in / sign-up / sign-out, device list, and /health ping. Enable the
# "json" feature for send_json / into_json (tls + gzip are on by default).
ureq = { version = "2.10", features = ["json"] }
# eframe bundles winit + the GPU backend (glow/OpenGL) + the event loop, and egui
# gives us a real toolbar/menus over the remote image. egui TextureOptions::LINEAR
# = bilinear GPU filtering = smooth + clear scaling of the decoded frame.
eframe = { version = "0.29", default-features = false, features = ["glow", "default_fonts"] }
egui = "0.29"
# OS clipboard read/write for RustDesk-style clipboard sync (egui can set but not
# read the clipboard; arboard does both). Already in the tree via eframe.
arboard = "3"
# decode the embedded NetDesk logo PNG into pixels (window icon + in-app logo
# texture) and encode the live frame to PNG for "Save screenshot".
image = { version = "0.25", default-features = false, features = ["png"] }
# native OS file-picker for the File transfer panel's "Upload…" button (pick a
# local file to send to the host). Sync FileDialog only, so no async runtime
# feature is needed; the Linux-backend features are irrelevant on Windows.
rfd = { version = "0.15", default-features = false }
# pure-Rust QR encoder (Nayuki) for in-app TOTP 2FA enrollment — render the
# host's otpauth:// secret as a QR the user scans with an authenticator app.
qrcodegen = "1.8"
# crisp, consistent icon set (web / copy / folder / lock / power / arrows / …) so
# every button shows a real icon instead of a missing-glyph box. Pure-Rust
# embedded font (Phosphor), no C build; matches egui 0.29.
egui-phosphor = "0.7"
# logging for the headless --host diagnostics
env_logger = "0.11"
# NOTE: remote audio playback ("Play remote computer sound") is implemented in
# src/audio.rs (cpal + Opus) but not yet wired in — the libopus build on this
# windows-gnu toolchain needs a reliable Opus source (audiopus_sys 0.1.8's build
# script mangles extended-length paths). Re-enable cpal + an Opus decoder here
# and `mod audio;` in main.rs once that is sorted.
# Unified app: the native Rust host engine (capture+encode+SendInput) runs
# IN-PROCESS when the user clicks Start Service, so one NetDesk.exe both views
# and hosts — no Python. Windows-only (DXGI/MediaFoundation/SendInput live here).
[target.'cfg(windows)'.dependencies]
netdesk-host = { path = "../host" }
# Remote audio playback (src/audio.rs): Opus decode via the vendored libopus
# + cpal (WASAPI) output.
netdesk-opus = { path = "../opus" }
cpal = "0.15"
+74
View File
@@ -0,0 +1,74 @@
//! Build script for the NetDesk native viewer.
//!
//! Embeds the NetDesk application icon (`assets/icon.ico`, multi-size) into the
//! Windows executable so Explorer, the taskbar and Alt-Tab all show the branded
//! green NetDesk mark. On the `x86_64-pc-windows-gnu` toolchain used for the
//! portable build, the resource is compiled with `windres` (shipped in the
//! MinGW bin dir that is on `PATH` during the workflow build) into a COFF object
//! which is then forced onto the final binary link line. No extra crates are
//! required — this keeps the build hermetic and offline-safe.
//!
//! If `windres` is unavailable the build still succeeds (a warning is emitted);
//! the runtime `ViewportBuilder::with_icon` call in `main.rs` still gives the
//! window + taskbar the icon, only the on-disk `.exe` file icon is skipped.
use std::path::PathBuf;
use std::process::Command;
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
// The .ico resource only applies to Windows PE binaries.
if target_os != "windows" {
return;
}
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let ico = manifest.join("..").join("assets").join("icon.ico");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", ico.display());
if !ico.exists() {
println!(
"cargo:warning=NetDesk icon not found at {} — exe icon not embedded",
ico.display()
);
return;
}
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let rc_path = out.join("netdesk.rc");
let obj_path = out.join("netdesk_icon.o");
// windres wants forward slashes inside the quoted path (backslashes are
// treated as escapes in the .rc grammar). Resource id 1 => the app's
// default icon group, which is what Explorer/Shell pick up.
let ico_fwd = ico.display().to_string().replace('\\', "/");
if std::fs::write(&rc_path, format!("1 ICON \"{ico_fwd}\"\n")).is_err() {
println!("cargo:warning=could not write {} — exe icon not embedded", rc_path.display());
return;
}
match Command::new("windres")
.arg(&rc_path)
.args(["-O", "coff", "-o"])
.arg(&obj_path)
.status()
{
Ok(status) if status.success() => {
// -bins: attach the resource object only to binary targets, and force
// it onto the link line (resources have no referenced symbols, so a
// plain static lib could be dropped — a direct link-arg cannot).
println!("cargo:rustc-link-arg-bins={}", obj_path.display());
}
Ok(status) => {
println!(
"cargo:warning=windres exited with {status} — exe icon not embedded (window icon still set at runtime)"
);
}
Err(e) => {
println!(
"cargo:warning=windres not found ({e}) — exe icon not embedded (window icon still set at runtime)"
);
}
}
}
+94
View File
@@ -0,0 +1,94 @@
//! Remote audio playback — "Play remote computer sound".
//!
//! The host streams its system audio as Opus (CHAN_AUDIO `0xa1`), one 20 ms
//! frame at 48 kHz stereo per packet — exactly what the web viewer decodes with
//! WebCodecs. Here we decode each Opus packet with libopus (via the vendored
//! `netdesk-opus` crate) and play the PCM through the default output device with
//! `cpal` (WASAPI on Windows). A short ring buffer bridges the decode side (net
//! thread) and the audio callback (cpal's own thread) and caps latency.
//!
//! Developed by Akronlogic.
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use netdesk_opus::Decoder;
const SR: u32 = 48_000;
const CH: usize = 2;
/// Decodes the host's Opus audio and plays it. Keep the instance alive for the
/// whole session — dropping it stops the output stream.
pub struct AudioPlayer {
_stream: cpal::Stream, // keep alive: dropping stops playback
decoder: Decoder,
ring: Arc<Mutex<VecDeque<f32>>>,
}
impl AudioPlayer {
/// Open the default output device at 48 kHz stereo and start playing silence.
pub fn new() -> anyhow::Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow::anyhow!("no default audio output device"))?;
let config = cpal::StreamConfig {
channels: CH as u16,
sample_rate: cpal::SampleRate(SR),
buffer_size: cpal::BufferSize::Default,
};
let ring: Arc<Mutex<VecDeque<f32>>> =
Arc::new(Mutex::new(VecDeque::with_capacity(SR as usize)));
let ring_cb = ring.clone();
let stream = device.build_output_stream(
&config,
move |out: &mut [f32], _: &cpal::OutputCallbackInfo| match ring_cb.lock() {
Ok(mut r) => {
for s in out.iter_mut() {
*s = r.pop_front().unwrap_or(0.0);
}
}
Err(_) => {
for s in out.iter_mut() {
*s = 0.0;
}
}
},
move |err| eprintln!("[audio] output stream error: {err}"),
None,
)?;
stream.play()?;
let decoder = Decoder::new(SR, CH as u32)?;
Ok(Self {
_stream: stream,
decoder,
ring,
})
}
/// Decode one Opus packet and enqueue the resulting PCM for playback.
/// Silently drops malformed/undecodable packets (best-effort, like the web
/// viewer).
pub fn push_opus(&mut self, data: &[u8]) {
let pcm = match self.decoder.decode(data) {
Ok(p) => p, // interleaved i16, stereo
Err(_) => return,
};
if let Ok(mut r) = self.ring.lock() {
for &s in &pcm {
r.push_back(s as f32 / 32768.0);
}
// Cap buffered audio to ~400 ms so playback can't drift far behind
// the video (drop the oldest samples on overrun).
let cap = SR as usize * CH * 2 / 5;
while r.len() > cap {
r.pop_front();
}
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff