NetDesk 1.5.0 — source release
This commit is contained in:
commit
40318aafdd
227 files changed
+80798
No files matched your search
@@ -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"
|
||||
@@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user