commit 61e8a75cdd5d07ccdb32431fc673ed38a1344ffd Author: dev-team Date: Fri Oct 23 09:17:55 2026 +0800 init: MVP remote desktop (agent/viewer/common) with H.264 pipeline diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..106ab9e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +# openh264 为 C++ 代码,默认动态链接 libstdc++/libgcc;改为静态链接,交付单文件 exe +[target.x86_64-pc-windows-gnu] +rustflags = ["-C", "link-arg=-static-libstdc++", "-C", "link-arg=-static-libgcc"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef8220a --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# ===== Rust / Cargo ===== +/target +**/*.rs.bk + +# ===== 构建产物 ===== +*.exe +*.dll +*.pdb +*.lib +*.a + +# ===== 运行时生成 ===== +*.log +identity.pem +*.pem +*.der + +# ===== IDE / 编辑器 ===== +.idea/ +.vscode/ +*.swp +*~ + +# ===== 系统文件 ===== +Thumbs.db +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..aa0de8b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1701 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "common", + "lz4_flex", + "openh264", + "quinn", + "rcgen", + "rustls", + "tokio", + "windows", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "serde", + "tokio", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastbloom" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash", + "libm", + "portable-atomic", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.5", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lz4_flex" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minifb" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c470a74618b43cd182c21b3dc1e6123501249f3bad9a0085e95d1304ca2478" +dependencies = [ + "cc", + "dlib", + "futures", + "instant", + "js-sys", + "lazy_static", + "libc", + "orbclient", + "raw-window-handle", + "serde", + "serde_derive", + "tempfile", + "wasm-bindgen-futures", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "winapi", + "x11-dl", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nasm-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149" +dependencies = [ + "log", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openh264" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54411f498088327bf5c45d2de7e5b0ed25875ee3be5800485afe45da7206bff" +dependencies = [ + "openh264-sys2", + "wide", +] + +[[package]] +name = "openh264-sys2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414f5bd53da84e126dc9449eb6f0003bb6994b2de63c48119b0c9b7a8b28086a" +dependencies = [ + "cc", + "nasm-rs", + "walkdir", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", + "sdl2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "safe_arch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdl2" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "viewer" +version = "0.1.0" +dependencies = [ + "anyhow", + "common", + "lz4_flex", + "minifb", + "openh264", + "quinn", + "rustls", + "tokio", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags 1.3.2", + "downcast-rs", + "libc", + "nix", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags 1.3.2", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wide" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d920ac99c3c8edce110cb8d07dbb324d6d026011dce85b1e9355b70f0adacc4f" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4458603 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +resolver = "2" +members = ["common", "agent", "viewer"] + +[workspace.dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +bincode = "1.3" +tokio = { version = "1", features = ["full"] } +quinn = "0.11" +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "logging"] } +rcgen = { version = "0.13", default-features = false, features = ["ring", "pem"] } +lz4_flex = "0.11" +windows = "0.58" +minifb = "0.27" diff --git a/agent/Cargo.toml b/agent/Cargo.toml new file mode 100644 index 0000000..65a85b7 --- /dev/null +++ b/agent/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "agent" +version = "0.1.0" +edition = "2021" + +[dependencies] +common = { path = "../common" } +anyhow.workspace = true +tokio.workspace = true +quinn.workspace = true +rustls.workspace = true +rcgen.workspace = true +lz4_flex.workspace = true +openh264 = { version = "0.9.9", features = ["source"] } +windows = { workspace = true, features = [ + "Win32_Foundation", + "Win32_Graphics_Direct3D", + "Win32_Graphics_Direct3D11", + "Win32_Graphics_Dxgi", + "Win32_Graphics_Dxgi_Common", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_WindowsAndMessaging", +] } diff --git a/agent/src/capture.rs b/agent/src/capture.rs new file mode 100644 index 0000000..4097043 --- /dev/null +++ b/agent/src/capture.rs @@ -0,0 +1,256 @@ +//! DXGI Desktop Duplication 采集(Windows 8+)。 +//! 帧处理:BGRA → openh264 H.264 编码(屏幕内容实时档,5Mbps)→ 线格式。 +//! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。 + +use anyhow::{anyhow, Result}; +use common::{encode_video_frame, VideoFrameHeader}; +use openh264::encoder::{ + BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType, +}; +use openh264::formats::{BgraSliceU8, YUVBuffer}; +use openh264::OpenH264API; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use tokio::sync::watch::Sender; +use windows::core::Interface; +use windows::Win32::Foundation::HMODULE; +use windows::Win32::Graphics::Direct3D::{ + D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0, +}; +use windows::Win32::Graphics::Direct3D11::{ + D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, + D3D11_CREATE_DEVICE_FLAG, D3D11_CPU_ACCESS_READ, D3D11_MAP, D3D11_MAP_READ, + D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, +}; +use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC}; +use windows::Win32::Graphics::Dxgi::{ + IDXGIDevice, IDXGIOutput1, IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_WAIT_TIMEOUT, + DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO, +}; + +use std::time::{Duration, Instant}; + +pub struct Duplication { + _device: ID3D11Device, + context: ID3D11DeviceContext, + staging_res: ID3D11Resource, + dup: IDXGIOutputDuplication, + pub width: u32, + pub height: u32, +} + +impl Duplication { + pub fn new() -> Result { + unsafe { + let mut device: Option = None; + let mut context: Option = None; + D3D11CreateDevice( + None, + D3D_DRIVER_TYPE_HARDWARE, + HMODULE::default(), + D3D11_CREATE_DEVICE_FLAG(0), + Some(&[D3D_FEATURE_LEVEL_11_0]), + D3D11_SDK_VERSION, + Some(&mut device), + None, + Some(&mut context), + ) + .map_err(|e| anyhow!("D3D11CreateDevice failed: {e}"))?; + let device = device.ok_or_else(|| anyhow!("no d3d device"))?; + let context = context.ok_or_else(|| anyhow!("no d3d context"))?; + + let dxgi: IDXGIDevice = device.cast()?; + let adapter = dxgi.GetAdapter()?; + let output = adapter + .EnumOutputs(0) + .map_err(|e| anyhow!("no display output: {e}"))?; + let output1: IDXGIOutput1 = output.cast()?; + let dup: IDXGIOutputDuplication = output1 + .DuplicateOutput(&device) + .map_err(|e| anyhow!("DuplicateOutput failed: {e}"))?; + + let desc: DXGI_OUTDUPL_DESC = dup.GetDesc(); + let (width, height) = (desc.ModeDesc.Width, desc.ModeDesc.Height); + anyhow::ensure!(width > 0 && height > 0, "bad desktop size"); + + let sdesc = D3D11_TEXTURE2D_DESC { + Width: width, + Height: height, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, + MiscFlags: 0, + }; + let mut staging_opt: Option = None; + device + .CreateTexture2D(&sdesc, None, Some(&mut staging_opt)) + .map_err(|e| anyhow!("CreateTexture2D failed: {e}"))?; + let staging = staging_opt.ok_or_else(|| anyhow!("no staging texture"))?; + let staging_res: ID3D11Resource = staging.cast()?; + + Ok(Self { + _device: device, + context, + staging_res, + dup, + width, + height, + }) + } + } + + /// true=拿到新帧并已复制到 staging;false=超时无更新 + pub fn acquire(&mut self, timeout_ms: u32) -> Result { + unsafe { + let mut info = DXGI_OUTDUPL_FRAME_INFO::default(); + let mut resource: Option = None; + match self.dup.AcquireNextFrame(timeout_ms, &mut info, &mut resource) { + Ok(()) => { + let res = resource.ok_or_else(|| anyhow!("no desktop resource"))?; + let src: ID3D11Resource = res.cast()?; + self.context.CopyResource(&self.staging_res, &src); + let _ = self.dup.ReleaseFrame(); + Ok(true) + } + Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => Ok(false), + Err(e) => Err(e.into()), + } + } + } + + /// 把 staging 纹理映射到内存并去掉行间距 + pub fn copy_frame(&mut self) -> Result> { + unsafe { + let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); + self.context + .Map(&self.staging_res, 0, D3D11_MAP_READ, 0, Some(&mut mapped)) + .map_err(|e| anyhow!("Map staging failed: {e}"))?; + let (w, h) = (self.width as usize, self.height as usize); + let pitch = mapped.RowPitch as usize; + let src = std::slice::from_raw_parts(mapped.pData as *const u8, pitch * h); + let mut out = Vec::with_capacity(w * h * 4); + for row in 0..h { + out.extend_from_slice(&src[row * pitch..row * pitch + w * 4]); + } + self.context.Unmap(&self.staging_res, 0); + Ok(out) + } + } +} + +#[allow(dead_code)] +pub fn desktop_size() -> Result<(u32, u32)> { + let d = Duplication::new()?; + Ok((d.width, d.height)) +} + +/// 采集主循环:独立线程运行;连接断开(通道关闭)时退出。 +/// 通道为 watch 单槽(latest-wins):发送即覆盖,网络慢时不产生积压。 +pub fn run(tx: Sender>>, keyreq: Arc) { + let start = Instant::now(); + loop { + let mut dup = match Duplication::new() { + Ok(d) => d, + Err(e) => { + eprintln!("[capture] init failed: {e}, retry in 2s"); + std::thread::sleep(Duration::from_secs(2)); + continue; + } + }; + eprintln!( + "[capture] duplication started {}x{}", + dup.width, dup.height + ); + + // H.264 编码器:屏幕内容实时档 + 码率控制(对标 UU:3.2Mbps@60fps) + let mut encoder = match Encoder::with_api_config( + OpenH264API::from_source(), + EncoderConfig::new() + .bitrate(BitRate::from_bps(5_000_000)) + .max_frame_rate(FrameRate::from_hz(30.0)) + .rate_control_mode(RateControlMode::Bitrate) + .usage_type(UsageType::ScreenContentRealTime) + .intra_frame_period(IntraFramePeriod::from_num_frames(300)), + ) { + Ok(e) => e, + Err(e) => { + eprintln!("[capture] encoder init failed: {e}"); + return; + } + }; + + let mut last_frame: Option> = None; + let mut last_sent = Instant::now() - Duration::from_secs(10); + let min_frame = Duration::from_millis(33); // 30fps 上限 + loop { + let iter_start = Instant::now(); + match dup.acquire(80) { + Ok(true) => match dup.copy_frame() { + Ok(raw) => { + // 主控端请求关键帧(解码异常恢复) + if keyreq.swap(false, Ordering::Relaxed) { + encoder.force_intra_frame(); + } + let header = VideoFrameHeader { + width: dup.width, + height: dup.height, + ts_ms: start.elapsed().as_millis() as u64, + }; + let yuv = YUVBuffer::from_bgra8_source(BgraSliceU8::new( + &raw, + (dup.width as usize, dup.height as usize), + )); + match encoder.encode(&yuv) { + Ok(ebs) => { + let payload = ebs.to_vec(); + let frame = encode_video_frame(&header, &payload); + if tx.send(Some(frame.clone())).is_err() { + return; // 会话结束 + } + last_frame = Some(frame); + last_sent = Instant::now(); + } + Err(e) => { + eprintln!("[capture] encode failed: {e}"); + } + } + // 帧间隔节流到 ~30fps + let el = iter_start.elapsed(); + if el < min_frame { + std::thread::sleep(min_frame - el); + } + } + Err(e) => { + eprintln!("[capture] frame copy failed: {e}"); + break; // 重建 duplication + } + }, + Ok(false) => { + // 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变) + if last_sent.elapsed() > Duration::from_millis(500) { + if let Some(f) = last_frame.as_ref() { + if tx.send(Some(f.clone())).is_err() { + return; + } + last_sent = Instant::now(); + } + } else { + std::thread::sleep(Duration::from_millis(8)); + } + } + Err(e) => { + eprintln!("[capture] acquire failed: {e}"); + break; // ACCESS_LOST 等,重建 + } + } + } + std::thread::sleep(Duration::from_millis(300)); + } +} diff --git a/agent/src/input.rs b/agent/src/input.rs new file mode 100644 index 0000000..9ed19f9 --- /dev/null +++ b/agent/src/input.rs @@ -0,0 +1,103 @@ +//! 输入注入:SendInput 实现(鼠标归一化绝对坐标 + 扫描码键盘)。 + +use common::{InputEvent, MouseButton}; +use windows::Win32::UI::Input::KeyboardAndMouse::{ + SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, + KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_MIDDLEDOWN, + MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, + MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEINPUT, VIRTUAL_KEY, MOUSE_EVENT_FLAGS, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, + SM_YVIRTUALSCREEN, +}; + +// windows 0.58 未导出 LEFT 常量,按 WinUser.h 原值定义 +const MOUSEEVENTF_LEFTDOWN: u32 = 0x0002; +const MOUSEEVENTF_LEFTUP: u32 = 0x0004; + +fn send_mouse(flags: u32, dx: i32, dy: i32, data: u32) { + let inp = INPUT { + r#type: INPUT_MOUSE, + Anonymous: INPUT_0 { + mi: MOUSEINPUT { + dx, + dy, + mouseData: data, + dwFlags: MOUSE_EVENT_FLAGS(flags), + time: 0, + dwExtraInfo: 0, + }, + }, + }; + unsafe { + SendInput(&[inp], std::mem::size_of::() as i32); + } +} + +fn send_key(scancode: u16, extended: bool, up: bool) { + let mut flags = KEYEVENTF_SCANCODE; + if extended { + flags |= KEYEVENTF_EXTENDEDKEY; + } + if up { + flags |= KEYEVENTF_KEYUP; + } + let inp = INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VIRTUAL_KEY(0), + wScan: scancode, + dwFlags: flags, + time: 0, + dwExtraInfo: 0, + }, + }, + }; + unsafe { + SendInput(&[inp], std::mem::size_of::() as i32); + } +} + +/// 打印虚拟桌面信息(调试用) +#[allow(dead_code)] +pub fn virtual_desktop() -> (i32, i32, i32, i32) { + unsafe { + ( + GetSystemMetrics(SM_XVIRTUALSCREEN), + GetSystemMetrics(SM_YVIRTUALSCREEN), + GetSystemMetrics(SM_CXVIRTUALSCREEN), + GetSystemMetrics(SM_CYVIRTUALSCREEN), + ) + } +} + +pub fn handle(ev: InputEvent) { + match ev { + InputEvent::MouseMove { x, y } => { + // 主控端已按虚拟桌面归一化,这里直接透传 0..65535 + send_mouse( + MOUSEEVENTF_ABSOLUTE.0 | MOUSEEVENTF_VIRTUALDESK.0 | MOUSEEVENTF_MOVE.0, + x as i32, + y as i32, + 0, + ); + } + InputEvent::MouseDown(b) => match b { + MouseButton::Left => send_mouse(MOUSEEVENTF_LEFTDOWN, 0, 0, 0), + MouseButton::Right => send_mouse(MOUSEEVENTF_RIGHTDOWN.0, 0, 0, 0), + MouseButton::Middle => send_mouse(MOUSEEVENTF_MIDDLEDOWN.0, 0, 0, 0), + }, + InputEvent::MouseUp(b) => match b { + MouseButton::Left => send_mouse(MOUSEEVENTF_LEFTUP, 0, 0, 0), + MouseButton::Right => send_mouse(MOUSEEVENTF_RIGHTUP.0, 0, 0, 0), + MouseButton::Middle => send_mouse(MOUSEEVENTF_MIDDLEUP.0, 0, 0, 0), + }, + InputEvent::Wheel { dy } => { + send_mouse(MOUSEEVENTF_WHEEL.0, 0, 0, dy as u32); + } + InputEvent::KeyDown { scancode, extended } => send_key(scancode, extended, false), + InputEvent::KeyUp { scancode, extended } => send_key(scancode, extended, true), + } +} diff --git a/agent/src/main.rs b/agent/src/main.rs new file mode 100644 index 0000000..72f3961 --- /dev/null +++ b/agent/src/main.rs @@ -0,0 +1,142 @@ +//! 被控端 Agent:DXGI 桌面复制采集 → LZ4 压缩 → QUIC 下行视频;上行接收输入事件注入。 +//! P0 优化:视频帧经 watch 单槽通道(latest-wins,网络慢时只发最新帧,不积压); +//! 键鼠/控制走独立 QUIC 双向流,不与视频互相阻塞。 + +mod capture; +mod input; + +use anyhow::Result; +use common::{read_control, write_control, ControlMsg}; +use rustls::pki_types::PrivatePkcs8KeyDer; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use tokio::io::AsyncWriteExt; +use tokio::sync::{mpsc, watch}; + +fn parse_args() -> Result<(u16, String)> { + let mut port = 47800u16; + let mut code = "123456".to_string(); + let a: Vec = std::env::args().collect(); + let mut i = 1; + while i < a.len() { + match a[i].as_str() { + "--port" => port = a[i + 1].parse()?, + "--code" => code = a[i + 1].clone(), + _ => {} + } + i += 2; + } + Ok((port, code)) +} + +#[tokio::main] +async fn main() -> Result<()> { + let (port, code) = parse_args()?; + rustls::crypto::ring::default_provider() + .install_default() + .ok(); + + // 自签名证书(MVP:客户端跳过校验;正式版改为固定公钥/账号体系) + let ck = rcgen::generate_simple_self_signed(vec!["localhost".into()])?; + let cert = ck.cert.der().clone(); + let key = PrivatePkcs8KeyDer::from(ck.key_pair.serialize_der()); + + // 传输参数调优: + // - 收发窗口放大,避免高 RTT 下吞吐被 窗口/RTT 死锁(默认 512KB 流窗口) + // - 发送缓冲上限 2MB:QUIC 内部排队有界(约 1 帧多),配合上游 latest-wins 丢帧 + // 把端到端排队延迟压在 ~0.4s 内 + let mut transport = quinn::TransportConfig::default(); + transport.receive_window(quinn::VarInt::from_u32(16 * 1024 * 1024)); + transport.stream_receive_window(quinn::VarInt::from_u32(8 * 1024 * 1024)); + transport.send_window(2 * 1024 * 1024); + let mut server_config = quinn::ServerConfig::with_single_cert(vec![cert], key.into())?; + server_config.transport_config(Arc::new(transport)); + + let endpoint = quinn::Endpoint::server( + server_config, + format!("0.0.0.0:{port}").parse()?, + )?; + println!("[agent] listening 0.0.0.0:{port}, verify code = {code}"); + + let conn = endpoint + .accept() + .await + .ok_or_else(|| anyhow::anyhow!("endpoint closed"))? + .await?; + println!("[agent] peer connected: {}", conn.remote_address()); + + // 流 1:认证 + 视频下行 + let (mut send, mut recv) = conn.accept_bi().await?; + + // 认证 + let first = read_control(&mut recv).await?; + let code_ok = matches!(&first, ControlMsg::Auth { code: c } if *c == code); + if !code_ok { + write_control(&mut send, &ControlMsg::AuthFail("bad code".into())).await?; + send.finish()?; + anyhow::bail!("auth failed"); + } + let (w, h) = capture::desktop_size()?; + write_control(&mut send, &ControlMsg::AuthOk { width: w, height: h }).await?; + println!("[agent] auth ok, desktop {w}x{h}"); + + // 流 2:键鼠输入 + 控制上行(与视频分离,避免被视频积压阻塞) + let (mut isend, mut irecv) = conn.accept_bi().await?; + + // 采集线程 → 视频帧(watch 单槽:新帧覆盖旧帧,latest-wins) + let (tx, mut frames_rx) = watch::channel::>>(None); + let keyreq = Arc::new(AtomicBool::new(false)); + let keyreq_capture = keyreq.clone(); + std::thread::spawn(move || capture::run(tx, keyreq_capture)); + + // 上行控制消息(Pong)经通道交给主循环写流 2,避免多任务争抢 + let (ctl_tx, mut ctl_rx) = mpsc::unbounded_channel::(); + let t0 = std::time::Instant::now(); + + // 读循环(流 2):输入事件 + Ping + let keyreq_reader = keyreq.clone(); + let reader = tokio::spawn(async move { + loop { + match read_control(&mut irecv).await { + Ok(ControlMsg::Input(ev)) => input::handle(ev), + Ok(ControlMsg::KeyFrameReq) => { + keyreq_reader.store(true, Ordering::Relaxed); + } + Ok(ControlMsg::Ping(t)) => { + let agent_ts = t0.elapsed().as_millis() as u64; + let _ = ctl_tx.send(ControlMsg::Pong { t, agent_ts }); + } + Ok(_) => {} + Err(_) => break, + } + } + }); + + loop { + tokio::select! { + changed = frames_rx.changed() => { + if changed.is_err() { + break; // 采集线程退出 + } + if let Some(data) = frames_rx.borrow().clone() { + if send.write_all(&data).await.is_err() { + break; + } + } + } + ctl = ctl_rx.recv() => { + match ctl { + Some(m) => { + if write_control(&mut isend, &m).await.is_err() { + break; + } + } + None => break, + } + } + } + } + reader.abort(); + println!("[agent] session closed"); + Ok(()) +} diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..ba6cc58 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +serde.workspace = true +bincode.workspace = true +tokio.workspace = true diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000..c72d345 --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,102 @@ +//! 通用协议与帧封装:控制消息(bincode + 长度前缀)与视频帧格式。 + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +/// 控制通道消息(双向) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ControlMsg { + /// 主控端认证(临时验证码,MVP 明文走 QUIC-TLS 加密通道) + Auth { code: String }, + AuthOk { width: u32, height: u32 }, + AuthFail(String), + KeyFrameReq, + Ping(u64), + /// t=主控端发送时刻(主控时钟),agent_ts=被控端收到时刻(被控时钟) + /// 两者配合可估计时钟偏移,进而用帧头 ts_ms 计算端到端延迟 + Pong { t: u64, agent_ts: u64 }, + Input(InputEvent), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum MouseButton { + Left, + Right, + Middle, +} + +/// 输入事件。鼠标坐标为虚拟桌面归一化 0..=65535; +/// 按键为 Set-1 扫描码,extended 标记扩展键(方向键/Home 等,需要 EXTENDEDKEY 标志)。 +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum InputEvent { + MouseMove { x: u16, y: u16 }, + MouseDown(MouseButton), + MouseUp(MouseButton), + Wheel { dy: i32 }, + KeyDown { scancode: u16, extended: bool }, + KeyUp { scancode: u16, extended: bool }, +} + +/// 视频帧头(载荷为 LZ4 压缩的 BGRA 像素,按行紧凑排列) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VideoFrameHeader { + pub width: u32, + pub height: u32, + pub ts_ms: u64, +} + +async fn read_u32le(r: &mut R) -> Result { + let mut b = [0u8; 4]; + r.read_exact(&mut b).await?; + Ok(u32::from_le_bytes(b)) +} + +/// 编码一条控制消息为「长度前缀 + bincode」 +pub fn encode_control(msg: &ControlMsg) -> Vec { + let body = bincode::serialize(msg).expect("serialize control"); + let mut out = Vec::with_capacity(body.len() + 4); + out.extend_from_slice(&(body.len() as u32).to_le_bytes()); + out.extend_from_slice(&body); + out +} + +pub async fn write_control( + w: &mut W, + msg: &ControlMsg, +) -> Result<()> { + w.write_all(&encode_control(msg)).await?; + Ok(()) +} + +pub async fn read_control(r: &mut R) -> Result { + let len = read_u32le(r).await? as usize; + anyhow::ensure!(len < 1 << 20, "control frame too large: {len}"); + let mut buf = vec![0u8; len]; + r.read_exact(&mut buf).await?; + Ok(bincode::deserialize(&buf)?) +} + +/// 编码视频帧:[总长 u32][头长 u32][bincode 头][LZ4 载荷] +pub fn encode_video_frame(header: &VideoFrameHeader, payload: &[u8]) -> Vec { + let hb = bincode::serialize(header).expect("serialize header"); + let total = 4 + hb.len() + payload.len(); + let mut out = Vec::with_capacity(total + 4); + out.extend_from_slice(&(total as u32).to_le_bytes()); + out.extend_from_slice(&(hb.len() as u32).to_le_bytes()); + out.extend_from_slice(&hb); + out.extend_from_slice(payload); + out +} + +pub async fn read_video_frame( + r: &mut R, +) -> Result<(VideoFrameHeader, Vec)> { + let total = read_u32le(r).await? as usize; + anyhow::ensure!(total < 256 << 20, "video frame too large: {total}"); + let mut buf = vec![0u8; total]; + r.read_exact(&mut buf).await?; + let hlen = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize; + let header: VideoFrameHeader = bincode::deserialize(&buf[4..4 + hlen])?; + Ok((header, buf[4 + hlen..].to_vec())) +} diff --git a/viewer/Cargo.toml b/viewer/Cargo.toml new file mode 100644 index 0000000..67cf690 --- /dev/null +++ b/viewer/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "viewer" +version = "0.1.0" +edition = "2021" + +[dependencies] +common = { path = "../common" } +anyhow.workspace = true +tokio.workspace = true +quinn.workspace = true +rustls.workspace = true +lz4_flex.workspace = true +openh264 = { version = "0.9.9", features = ["source"] } +minifb.workspace = true diff --git a/viewer/src/keymap.rs b/viewer/src/keymap.rs new file mode 100644 index 0000000..ba88c58 --- /dev/null +++ b/viewer/src/keymap.rs @@ -0,0 +1,73 @@ +//! minifb::Key → Set-1 扫描码映射。返回 (扫描码, 是否扩展键)。 + +use minifb::Key; + +pub fn map(k: Key) -> Option<(u16, bool)> { + use Key::*; + Some(match k { + A => (0x1E, false), + B => (0x30, false), + C => (0x2E, false), + D => (0x20, false), + E => (0x12, false), + F => (0x21, false), + G => (0x22, false), + H => (0x23, false), + I => (0x17, false), + J => (0x24, false), + K => (0x25, false), + L => (0x26, false), + M => (0x32, false), + N => (0x31, false), + O => (0x18, false), + P => (0x19, false), + Q => (0x10, false), + R => (0x13, false), + S => (0x1F, false), + T => (0x14, false), + U => (0x16, false), + V => (0x2F, false), + W => (0x11, false), + X => (0x2D, false), + Y => (0x15, false), + Z => (0x2C, false), + Key1 => (0x02, false), + Key2 => (0x03, false), + Key3 => (0x04, false), + Key4 => (0x05, false), + Key5 => (0x06, false), + Key6 => (0x07, false), + Key7 => (0x08, false), + Key8 => (0x09, false), + Key9 => (0x0A, false), + Key0 => (0x0B, false), + Space => (0x39, false), + Enter => (0x1C, false), + Backspace => (0x0E, false), + Tab => (0x0F, false), + CapsLock => (0x3A, false), + Backquote => (0x29, false), + Minus => (0x0C, false), + Equal => (0x0D, false), + Backslash => (0x2B, false), + Comma => (0x33, false), + Period => (0x34, false), + Slash => (0x35, false), + Semicolon => (0x27, false), + Apostrophe => (0x28, false), + LeftShift => (0x2A, false), + LeftCtrl => (0x1D, false), + LeftAlt => (0x38, false), + LeftSuper => (0x5B, true), + Up => (0x48, true), + Down => (0x50, true), + Left => (0x4B, true), + Right => (0x4D, true), + Home => (0x47, true), + End => (0x4F, true), + Delete => (0x53, true), + PageUp => (0x49, true), + PageDown => (0x51, true), + _ => return None, + }) +} diff --git a/viewer/src/main.rs b/viewer/src/main.rs new file mode 100644 index 0000000..a708f21 --- /dev/null +++ b/viewer/src/main.rs @@ -0,0 +1,554 @@ +//! 主控端 Viewer:QUIC 连接被控端 → 解压帧 → minifb 渲染;采集键鼠上行。 +//! HUD:右上角半透明白底参数面板(FPS / RTT / 端到端延迟 / 码率),延迟绿-黄-红分级。 + +mod keymap; + +use anyhow::{bail, Result}; +use common::{read_control, write_control, ControlMsg, InputEvent, MouseButton}; +use minifb::{Key, MouseMode, Window, WindowOptions}; +use openh264::formats::YUVSource; +use rustls::client::danger::{ + HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier, +}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use rustls::{DigitallySignedStruct, SignatureScheme}; +use std::collections::HashSet; +use std::net::ToSocketAddrs; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; +use std::sync::{Arc, OnceLock}; +use std::time::{Duration, Instant}; + +/// MVP:跳过服务端证书校验(自签名)。正式版固定公钥/账号绑定。 +#[derive(Debug)] +struct SkipVerify; +impl ServerCertVerifier for SkipVerify { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + fn verify_tls12_signature( + &self, + _message: &[u8], + _certificate: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + fn verify_tls13_signature( + &self, + _message: &[u8], + _certificate: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ED25519, + SignatureScheme::RSA_PSS_SHA256, + ] + } +} + +fn parse_args() -> Result<(String, u16, String)> { + let mut addr = "127.0.0.1".to_string(); + let mut port = 47800u16; + let mut code = "123456".to_string(); + let a: Vec = std::env::args().collect(); + let mut i = 1; + while i < a.len() { + match a[i].as_str() { + "--addr" => addr = a[i + 1].clone(), + "--port" => port = a[i + 1].parse()?, + "--code" => code = a[i + 1].clone(), + _ => {} + } + i += 2; + } + Ok((addr, port, code)) +} + +static T0: OnceLock = OnceLock::new(); +fn now_ms() -> u64 { + T0.get().unwrap().elapsed().as_millis() as u64 +} + +/// 会话统计(HUD 数据源,全部原子量,跨任务共享) +#[derive(Default)] +struct Stats { + /// 最近 RTT ms(0=未知) + rtt: AtomicU64, + /// 被控时钟 - 主控时钟 偏移估计 ms(EMA 平滑) + offset: AtomicI64, + offset_valid: AtomicBool, + /// 端到端延迟估计 ms(采集→网络→接收,EMA 平滑) + e2e: AtomicU64, + e2e_valid: AtomicBool, + /// 累计收帧数 / 字节数 + frames: AtomicU64, + bytes: AtomicU64, +} + +fn main() -> Result<()> { + let (addr, port, code) = parse_args()?; + T0.set(Instant::now()).ok(); + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + rt.block_on(run(addr, port, code)) +} + +async fn run(addr: String, port: u16, code: String) -> Result<()> { + rustls::crypto::ring::default_provider() + .install_default() + .ok(); + + let tls = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(SkipVerify)) + .with_no_client_auth(); + let qc: quinn::crypto::rustls::QuicClientConfig = Arc::new(tls).try_into()?; + // 接收窗口放大:视频是下行大流量方向,默认 512KB 流窗口会死锁吞吐 + let mut transport = quinn::TransportConfig::default(); + transport.receive_window(quinn::VarInt::from_u32(16 * 1024 * 1024)); + transport.stream_receive_window(quinn::VarInt::from_u32(8 * 1024 * 1024)); + let mut client_config = quinn::ClientConfig::new(Arc::new(qc)); + client_config.transport_config(Arc::new(transport)); + let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse()?)?; + endpoint.set_default_client_config(client_config); + + let sock_addr = (addr.as_str(), port) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow::anyhow!("resolve failed: {addr}"))?; + let conn = endpoint + .connect(sock_addr, "localhost")? + .await + .map_err(|e| anyhow::anyhow!("connect {addr}:{port} failed: {e}"))?; + println!("[viewer] connected to {addr}:{port}"); + + let (mut send, mut recv) = conn.open_bi().await?; + + write_control(&mut send, &ControlMsg::Auth { code }).await?; + match read_control(&mut recv).await? { + ControlMsg::AuthOk { width, height } => { + println!("[viewer] auth ok, remote desktop {width}x{height}"); + // 流 2:键鼠输入 + Ping/Pong 独立通道,不与视频流竞争 + let (isend, irecv) = conn.open_bi().await?; + session(isend, irecv, recv, width, height) + } + ControlMsg::AuthFail(e) => bail!("auth failed: {e}"), + m => bail!("unexpected message: {m:?}"), + } +} + +fn session( + send: quinn::SendStream, + mut irecv: quinn::RecvStream, + mut recv: quinn::RecvStream, + width: u32, + height: u32, +) -> Result<()> { + let (w, h) = (width as usize, height as usize); + let stats = Arc::new(Stats::default()); + + // 读任务(流 1):H.264 解码 → RGB → mpsc;同时统计端到端延迟与流量 + let (frame_tx, frame_rx) = std::sync::mpsc::channel::>(); + { + let stats = stats.clone(); + let mut decoder = openh264::decoder::Decoder::new() + .map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?; + tokio::spawn(async move { + loop { + match common::read_video_frame(&mut recv).await { + Ok((hdr, payload)) => { + stats.bytes.fetch_add(payload.len() as u64, Ordering::Relaxed); + stats.frames.fetch_add(1, Ordering::Relaxed); + // 端到端延迟 = 接收时刻 - 时钟偏移 - 采集时刻(EMA 平滑) + if stats.offset_valid.load(Ordering::Relaxed) { + let est = + now_ms() as i64 - stats.offset.load(Ordering::Relaxed) + - hdr.ts_ms as i64; + if (0..5000).contains(&est) { + let old = stats.e2e.load(Ordering::Relaxed) as i64; + let sm = if stats.e2e_valid.load(Ordering::Relaxed) { + (old * 4 + est) / 5 + } else { + est + }; + stats.e2e.store(sm as u64, Ordering::Relaxed); + stats.e2e_valid.store(true, Ordering::Relaxed); + } + } + match decoder.decode(&payload) { + Ok(Some(yuv)) => { + let (dw, dh) = yuv.dimensions(); + let mut rgb = vec![0u8; dw * dh * 3]; + yuv.write_rgb8(&mut rgb); + if frame_tx.send(rgb).is_err() { + break; + } + } + Ok(None) => {} + Err(e) => { + // 解码异常:请求被控端强制关键帧 + eprintln!("[viewer] decode failed: {e}, request keyframe"); + let _ = frame_tx.send(Vec::new()); // 空帧占位唤醒渲染循环 + } + } + } + Err(e) => { + eprintln!("[viewer] stream closed: {e}"); + break; + } + } + } + }); + } + + // 读任务(流 2):Pong → RTT 与时钟偏移估计 + { + let stats = stats.clone(); + tokio::spawn(async move { + loop { + match read_control(&mut irecv).await { + Ok(ControlMsg::Pong { t, agent_ts }) => { + let r = now_ms(); + stats.rtt.store(r.saturating_sub(t), Ordering::Relaxed); + // agent_ts ≈ (t+r)/2 时刻的 agent 时钟读数 → 偏移 EMA + let est = agent_ts as i64 - ((t + r) / 2) as i64; + let old = stats.offset.load(Ordering::Relaxed); + let sm = if stats.offset_valid.load(Ordering::Relaxed) { + (old * 3 + est) / 4 + } else { + est + }; + stats.offset.store(sm, Ordering::Relaxed); + stats.offset_valid.store(true, Ordering::Relaxed); + } + Ok(_) => {} + Err(_) => break, + } + } + }); + } + + // 写任务(流 2):输入 / Ping + let (msg_tx, mut msg_rx) = tokio::sync::mpsc::unbounded_channel::>(); + tokio::spawn(async move { + let mut send = send; + while let Some(b) = msg_rx.recv().await { + if send.write_all(&b).await.is_err() { + break; + } + } + let _ = send.finish(); + }); + let push = |tx: &tokio::sync::mpsc::UnboundedSender>, m: ControlMsg| { + let _ = tx.send(common::encode_control(&m)); + }; + + let mut window = Window::new( + "mvp-remote viewer (Esc 退出)", + w, + h, + WindowOptions { + resize: false, + ..Default::default() + }, + )?; + + let mut buffer = vec![0u32; w * h]; + let mut prev_keys: HashSet = HashSet::new(); + let mut prev_btns = [false; 3]; + let mut last_pos: Option<(u16, u16)> = None; + let mut dirty = false; + let mut last_ping = Instant::now() - Duration::from_secs(1); + // HUD 每秒刷新的统计基线 + let mut hud_last = Instant::now(); + let mut hud_frames = 0u64; + let mut hud_bytes = 0u64; + let mut hud_fps = 0u64; + let mut hud_mbps = 0f64; + + loop { + if !window.is_open() || window.is_key_down(Key::Escape) { + break; + } + + // 0) 每 500ms 发一次 Ping(RTT/时钟偏移测量) + if last_ping.elapsed() >= Duration::from_millis(500) { + last_ping = Instant::now(); + push(&msg_tx, ControlMsg::Ping(now_ms())); + } + + // 1) 渲染最新帧(只保留最新,丢弃积压);空帧=解码异常,请求关键帧 + loop { + match frame_rx.try_recv() { + Ok(raw) => { + if raw.is_empty() { + push(&msg_tx, ControlMsg::KeyFrameReq); + } else if raw.len() == w * h * 3 { + blit(&mut buffer, &raw); + dirty = true; + } + } + Err(_) => break, + } + } + + // 2) 键盘 + let keys: HashSet = window.get_keys().into_iter().collect(); + for k in keys.difference(&prev_keys) { + if let Some((sc, ext)) = keymap::map(*k) { + push( + &msg_tx, + ControlMsg::Input(InputEvent::KeyDown { + scancode: sc, + extended: ext, + }), + ); + } + } + for k in prev_keys.difference(&keys) { + if let Some((sc, ext)) = keymap::map(*k) { + push( + &msg_tx, + ControlMsg::Input(InputEvent::KeyUp { + scancode: sc, + extended: ext, + }), + ); + } + } + prev_keys = keys; + + // 3) 鼠标移动(窗口坐标 → 虚拟桌面归一化 0..65535;位置变化才发送) + if let Some((x, y)) = window.get_mouse_pos(MouseMode::Discard) { + let nx = ((x / w as f32) * 65535.0).clamp(0.0, 65535.0) as u16; + let ny = ((y / h as f32) * 65535.0).clamp(0.0, 65535.0) as u16; + if last_pos != Some((nx, ny)) { + last_pos = Some((nx, ny)); + push(&msg_tx, ControlMsg::Input(InputEvent::MouseMove { x: nx, y: ny })); + } + } + + // 4) 鼠标按键(边沿触发) + for (i, (btn, mfb)) in BTN_MAP.iter().enumerate() { + let down = window.get_mouse_down(*mfb); + if down != prev_btns[i] { + prev_btns[i] = down; + push( + &msg_tx, + ControlMsg::Input(if down { + InputEvent::MouseDown(*btn) + } else { + InputEvent::MouseUp(*btn) + }), + ); + } + } + + // 5) 滚轮 + if let Some((_, dy)) = window.get_scroll_wheel() { + if dy.abs() >= 1.0 { + push( + &msg_tx, + ControlMsg::Input(InputEvent::Wheel { dy: dy as i32 * 120 }), + ); + } + } + + // 6) HUD:每秒刷新一次指标,画在右上角 + if hud_last.elapsed() >= Duration::from_secs(1) { + let f = stats.frames.load(Ordering::Relaxed); + let b = stats.bytes.load(Ordering::Relaxed); + hud_fps = f.saturating_sub(hud_frames); + hud_mbps = (b.saturating_sub(hud_bytes)) as f64 * 8.0 / 1e6; + hud_frames = f; + hud_bytes = b; + hud_last = Instant::now(); + } + if dirty { + let rtt = stats.rtt.load(Ordering::Relaxed); + let e2e = stats.e2e.load(Ordering::Relaxed); + let e2e_ok = stats.e2e_valid.load(Ordering::Relaxed); + draw_hud( + &mut buffer, + w, + &[ + ("FPS", &format!("{hud_fps}"), lat_color(hud_fps >= 25, hud_fps >= 15)), + ("RTT", &format!("{rtt}ms"), lat_color(rtt < 50 && rtt > 0, rtt < 100)), + ( + "E2E", + &if e2e_ok { format!("{e2e}ms") } else { "--".into() }, + lat_color(e2e < 100, e2e < 200), + ), + ("RATE", &format!("{:.0}MBP", hud_mbps), C_GRAY), + ("RES", &format!("{w}x{h}"), C_GRAY), + ], + ); + window.update_with_buffer(&buffer, w, h)?; + dirty = false; + } else { + window.update(); + std::thread::sleep(Duration::from_millis(4)); + } + } + + println!("[viewer] closed"); + Ok(()) +} + +const BTN_MAP: [(MouseButton, minifb::MouseButton); 3] = [ + (MouseButton::Left, minifb::MouseButton::Left), + (MouseButton::Right, minifb::MouseButton::Right), + (MouseButton::Middle, minifb::MouseButton::Middle), +]; + +// ---------- HUD ---------- + +const C_GREEN: u32 = 0x0A8F3C; +const C_AMBER: u32 = 0xC07000; +const C_RED: u32 = 0xC02222; +const C_GRAY: u32 = 0x3A3A3A; + +fn lat_color(good: bool, mid: bool) -> u32 { + if good { + C_GREEN + } else if mid { + C_AMBER + } else { + C_RED + } +} + +/// 5x7 点阵字体(列优先,bit0=顶行)。覆盖 HUD 用到的字符。 +fn glyph(c: u8) -> [u8; 5] { + match c { + b'0' => [0x3E, 0x51, 0x49, 0x45, 0x3E], + b'1' => [0x00, 0x42, 0x7F, 0x40, 0x00], + b'2' => [0x42, 0x61, 0x51, 0x49, 0x46], + b'3' => [0x21, 0x41, 0x45, 0x4B, 0x31], + b'4' => [0x18, 0x14, 0x12, 0x7F, 0x10], + b'5' => [0x27, 0x45, 0x45, 0x45, 0x39], + b'6' => [0x3C, 0x4A, 0x49, 0x49, 0x30], + b'7' => [0x01, 0x71, 0x09, 0x05, 0x03], + b'8' => [0x36, 0x49, 0x49, 0x49, 0x36], + b'9' => [0x06, 0x49, 0x49, 0x29, 0x1E], + b'A' => [0x7E, 0x11, 0x11, 0x11, 0x7E], + b'B' => [0x7F, 0x49, 0x49, 0x49, 0x36], + b'C' => [0x3E, 0x41, 0x41, 0x41, 0x22], + b'D' => [0x7F, 0x41, 0x41, 0x22, 0x1C], + b'E' => [0x7F, 0x49, 0x49, 0x49, 0x41], + b'F' => [0x7F, 0x09, 0x09, 0x09, 0x01], + b'G' => [0x3E, 0x41, 0x49, 0x49, 0x7A], + b'H' => [0x7F, 0x08, 0x08, 0x08, 0x7F], + b'I' => [0x00, 0x41, 0x7F, 0x41, 0x00], + b'J' => [0x20, 0x40, 0x41, 0x3F, 0x01], + b'K' => [0x7F, 0x08, 0x14, 0x22, 0x41], + b'L' => [0x7F, 0x40, 0x40, 0x40, 0x40], + b'M' => [0x7F, 0x02, 0x0C, 0x02, 0x7F], + b'N' => [0x7F, 0x04, 0x08, 0x10, 0x7F], + b'O' => [0x3E, 0x41, 0x41, 0x41, 0x3E], + b'P' => [0x7F, 0x09, 0x09, 0x09, 0x06], + b'Q' => [0x3E, 0x41, 0x51, 0x21, 0x5E], + b'R' => [0x7F, 0x09, 0x19, 0x29, 0x46], + b'S' => [0x46, 0x49, 0x49, 0x49, 0x31], + b'T' => [0x01, 0x01, 0x7F, 0x01, 0x01], + b'U' => [0x3F, 0x40, 0x40, 0x40, 0x3F], + b'V' => [0x1F, 0x20, 0x40, 0x20, 0x1F], + b'W' => [0x7F, 0x20, 0x18, 0x20, 0x7F], + b'X' => [0x63, 0x14, 0x08, 0x14, 0x63], + b'Y' => [0x03, 0x04, 0x78, 0x04, 0x03], + b'Z' => [0x61, 0x51, 0x49, 0x45, 0x43], + b':' => [0x00, 0x36, 0x36, 0x00, 0x00], + b'.' => [0x00, 0x60, 0x60, 0x00, 0x00], + b'-' => [0x08, 0x08, 0x08, 0x08, 0x08], + b' ' => [0x00; 5], + _ => [0x00; 5], + } +} + +/// 在 (x,y) 处以 scale 倍率画一行文字 +fn draw_text( + buffer: &mut [u32], + w: usize, + mut x: usize, + y: usize, + s: &str, + color: u32, + scale: usize, +) { + let h = buffer.len() / w.max(1); + for ch in s.bytes() { + let g = glyph(ch.to_ascii_uppercase()); + for (col, bits) in g.iter().enumerate() { + for row in 0..7 { + if bits & (1 << row) != 0 { + for sy in 0..scale { + for sx in 0..scale { + let px = x + col * scale + sx; + let py = y + row * scale + sy; + if px < w && py < h { + buffer[py * w + px] = color; + } + } + } + } + } + } + x += 6 * scale; + } +} + +/// 右上角半透明白底 HUD:lines = [(标签, 值, 颜色)] +fn draw_hud(buffer: &mut [u32], w: usize, lines: &[(&str, &str, u32)]) { + const SCALE: usize = 2; + const PAD: usize = 10; + const MARGIN: usize = 12; + const LINE: usize = 8 * SCALE; + + let h = buffer.len() / w.max(1); + let line_w = |l: &(&str, &str, u32)| (l.0.len() + 2 + l.1.len()) * 6 * SCALE; + let box_w = lines.iter().map(line_w).max().unwrap_or(0) + PAD * 2; + let box_h = lines.len() * LINE + PAD * 2; + let x0 = w.saturating_sub(box_w + MARGIN); + let y0 = MARGIN; + + // 半透明白底:px' = px*0.35 + 255*0.65 + for by in y0..(y0 + box_h).min(h) { + for bx in x0..(x0 + box_w).min(w) { + let p = &mut buffer[by * w + bx]; + let v = *p; + let r = (v >> 16) & 0xFF; + let g = (v >> 8) & 0xFF; + let b = v & 0xFF; + let mix = |c: u32| c * 89 / 255 + 166; + *p = (mix(r) << 16) | (mix(g) << 8) | mix(b); + } + } + + for (i, (label, value, color)) in lines.iter().enumerate() { + let mut x = x0 + PAD; + let y = y0 + PAD + i * LINE; + draw_text(buffer, w, x, y, label, C_GRAY, SCALE); + x += (label.len() + 2) * 6 * SCALE; + draw_text(buffer, w, x, y, value, *color, SCALE); + } +} + +/// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲 +fn blit(buffer: &mut [u32], raw: &[u8]) { + for (px, chunk) in buffer.iter_mut().zip(raw.chunks_exact(3)) { + *px = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32; + } +} diff --git a/构建运行手册.md b/构建运行手册.md new file mode 100644 index 0000000..112d7c2 --- /dev/null +++ b/构建运行手册.md @@ -0,0 +1,76 @@ +# MVP 远程控制 Demo · 构建运行手册 + +> 项目路径:`D:\wordkb\2026-10-22-15-52-47\mvp-remote\` +> 形态:Rust workspace,三个 crate:`common`(协议)、`agent`(被控端)、`viewer`(主控端) + +## 1. 功能范围(量级一 Demo) + +- ✅ DXGI Desktop Duplication 全屏采集(GPU 拷贝,脏区/硬编码留待二期) +- ✅ LZ4 压缩 + QUIC 加密传输(自签名证书,传输层已加密) +- ✅ 验证码认证(默认 123456) +- ✅ 键鼠远程控制(SendInput 注入,虚拟桌面绝对坐标 + 扫描码) +- ✅ 桌面静止时低频重发(保持画面新鲜) +- ❌ 暂无:硬件编码、P2P 打洞/中继(当前仅局域网/可直连地址)、音频、文件传输、Linux 被控 + +## 2. 环境要求 + +- Windows 8+(被控端需要 DXGI Duplication;主控端 Win7 亦可) +- 工具链已全部部署在 D 盘(无需再装): + - Rust (GNU):`uurev/tools/rust/cargo/bin` + - MinGW-w64 14.2:`uurev/tools/mingw64/bin` + +## 3. 构建 + +```bash +export RUSTUP_HOME="D:\\wordkb\\2026-10-22-15-52-47\\uurev\\tools\\rust\\rustup" +export CARGO_HOME="D:\\wordkb\\2026-10-22-15-52-47\\uurev\\tools\\rust\\cargo" +export PATH="/d/wordkb/2026-10-22-15-52-47/uurev/tools/rust/cargo/bin:/d/wordkb/2026-10-22-15-52-47/uurev/tools/mingw64/bin:$PATH" +cd /d/wordkb/2026-10-22-15-52-47/mvp-remote +cargo build --release +``` + +产物: +- 被控端 `target/release/agent.exe` +- 主控端 `target/release/viewer.exe` + +## 4. 运行(两台机器,或同机双开验证) + +被控端(要被控制的电脑): +```bash +agent.exe --port 47800 --code 888888 +``` + +主控端(操作的电脑): +```bash +viewer.exe --addr <被控端IP> --port 47800 --code 888888 +``` + +同机自测:被控端 `--port 47800`,主控端 `--addr 127.0.0.1`。 + +操作说明:窗口内直接键鼠操作;**Esc 退出**。窗口尺寸=被控端分辨率(暂不可缩放)。 + +## 5. 代码结构导读(演进锚点) + +| 文件 | 职责 | 二期改造点 | +|---|---|---| +| `common/src/lib.rs` | 控制消息/视频帧线格式 | 换 protobuf;加 FEC/序列号 | +| `agent/src/capture.rs` | DXGI 采集 + LZ4 | 加脏矩形、NVENC/QSV 硬编(替换 LZ4) | +| `agent/src/input.rs` | SendInput 注入 | 加剪贴板/多显示器虚拟桌面细化 | +| `viewer/src/main.rs` | minifb 渲染 + 输入采集 | 换 GPU 解码渲染(D3D11/YUV);加自适应码率 | +| `viewer/src/keymap.rs` | 扫描码映射 | 补全 F 区/小键盘/IME 文本通道 | + +## 6. 已知限制(MVP 预期内) + +1. 带宽:1080p LZ4 裸帧约 20-60MB/s,**仅适合局域网**。公网可用必须上 H.264 硬编(二期第一个任务)。 +2. 安顿证书:客户端跳过服务端证书校验(代码内 `SkipVerify` 已标注),存在中间人风险,仅限测试。 +3. 被控端需在**交互会话**中运行(RDP 断开后桌面复制会失效——二期做服务化+虚拟桌面处理)。 +4. 高 DPI/多屏:当前只采集主输出(EnumOutputs(0))。 +5. 主控端窗口固定为远端分辨率,颜色序 BGRA→0RGB 若异常可在 `blit()` 一键翻转。 + +## 7. 下一阶段任务队列(Phase 1 剩余) + +1. H.264:接 openh264(软编兜底)→ NVENC/QSV(硬编优先) +2. 码率自适应 + 关键帧请求(协议已留 `KeyFrameReq`) +3. QUIC 多流:视频/控制/键鼠分离流(键鼠零阻塞) +4. P2P 打洞 + coturn 中继(自建信令服务器) +5. 被控端服务化(Windows Service + 会话切换处理)