Old approach: polled recv.read() with 1ms timeout each frame. When the client disconnected, the QUIC connection could stay half-open long enough that the next client connected before the old one was detected, leaving the counter stuck at 2. New approach: spawn stream_frames as a task, race it against conn.closed() with tokio::select!. When the connection closes (from either side), the closed() future resolves immediately, the stream task is aborted, and fetch_sub fires right away. No polling, no timing gap.
- commit
- 5c71338ea3d392741563c2317a9c9df6cf8b7a24
- parent
- e662dfb91d35fbc56126d34b43cedb49e8b2797b
- author
- dami etoile <dami.etoile@gmail.com>
| M | README.md | 23 | +++++++++++++++-------- |
| M | src-tauri/ | 19 | +++++++++++++++++-- |
@@ -4,7 +4,9 @@ A native remote desktop app secured by FIDO2 hardware keys. Plug in a Google Tit
44 55 ## Status66 7-Early prototype. Core functionality works (identity derivation, streaming, input). Performance optimization in progress. Current frame rate is ~1–2 fps. The main bottleneck is openh264 software encoding on the host and lack of hardware decode on the client when WebCodecs is unavailable.7+Early prototype. Core functionality works (identity derivation, streaming, input). Current frame rate is ~28 fps with ffmpeg+NVENC (was ~1–2 fps with xcap+openh264 software encoding). The bottleneck has moved from host-side encoding to network/relay throughput and client-side decode.8+9+Codec and encoder backend are configurable in the info panel — H.264, H.265, and AV1 are all supported, with backends for NVENC, VAAPI, QSV, AMF, VideoToolbox, and software. The frontend uses WebCodecs for hardware-accelerated decode of all three codecs.810 911 ## How it works1012 @@ -18,18 +20,20 @@ The connection is end-to-end encrypted by Iroh. The key never leaves the hardwar
1820 1921 ## Architecture2022 21-```text23+```2224 +--------------------------- keyhome (Tauri v2) ---------------------------+2325 | |2426 | Frontend (vanilla JS, no npm) |2527 | - connection state, video canvas, input capture |28+| - WebCodecs decode (H.264/H.265/AV1, hardware-accelerated) |2629 | |2730 | Tauri IPC |2831 | |2932 | Rust backend |30-| - FIDO2 CTAP via ctap-hid-fido2 - Screen capture via xcap |31-| - Iroh Endpoint (P2P, relay, E2EE) - H.264 encoding via openh264 |32-| - OS keyring for identity - Input injection via enigo |33+| - FIDO2 CTAP via ctap-hid-fido2 - Screen capture+encode: ffmpeg |34+| - Iroh Endpoint (P2P, relay, E2EE) (NVENC/VAAPI/QSV/AMF/VT) |35+| - OS keyring for identity - Fallback: xcap + openh264 |36+| - Input injection via enigo - Codec: H.264/H.265/AV1 |3337 | |3438 +----------------------------------+--------------------------------------+3539 |@@ -37,7 +41,8 @@ The connection is end-to-end encrypted by Iroh. The key never leaves the hardwar
3741 v3842 +------------------------------ Host -------------------------------------+3943 | - Iroh Endpoint (identity from keyring) |40-| - xcap screen capture → openh264 H.264 encode |44+| - ffmpeg screen capture → H.264/H.265/AV1 encode (hardware) |45+| - Fallback: xcap capture → openh264 software encode |4146 | - enigo mouse/keyboard injection |4247 +-------------------------------------------------------------------------+4348 ```@@ -48,8 +53,10 @@ The connection is end-to-end encrypted by Iroh. The key never leaves the hardwar
4853 | Backend | Rust |4954 | P2P transport | Iroh (relay-assisted, end-to-end encrypted) |5055 | Hardware auth | FIDO2 CTAP via `ctap-hid-fido2` |51-| Screen capture | `xcap` |52-| Video encoding | `openh264` (software) |56+| Screen capture + encoding | `ffmpeg` subprocess (NVENC/VAAPI/QSV/AMF/VideoToolbox) |57+| Fallback capture + encoding | `xcap` + `openh264` (software) |58+| Video codecs | H.264, H.265, AV1 (configurable) |59+| Client-side decode | WebCodecs (hardware-accelerated, all three codecs) |5360 | Input injection | `enigo` |5461 | Identity storage | OS keyring |5562 | Frontend | Vanilla JS (no npm, no build step) |@@ -731,8 +731,23 @@ impl ProtocolHandler for FrameStreamHandler {
731731 let count = self.connections.fetch_add(1, Ordering::SeqCst) + 1;732732 let _ = self.app.emit("host-connections", count);733733 eprintln!("[host] client connected: {} (total: {})", conn.remote_id(), count);734- if let Err(e) = stream_frames(conn, &self.app).await {735- eprintln!("[host] stream error: {}", e);734+ // Spawn stream_frames, race against connection close735+ let conn_clone = conn.clone();736+ let app_clone = self.app.clone();737+ let mut stream_task = tokio::spawn(async move {738+ stream_frames(conn_clone, &app_clone).await739+ });740+ // Wait for either stream to end or connection to close741+ tokio::select! {742+ result = &mut stream_task => {743+ if let Ok(Err(e)) = result {744+ eprintln!("[host] stream error: {}", e);745+ }746+ }747+ _ = conn.closed() => {748+ eprintln!("[host] connection closed by peer");749+ stream_task.abort();750+ }736751 }737752 let count = self.connections.fetch_sub(1, Ordering::SeqCst) - 1;738753 let _ = self.app.emit("host-connections", count);