refactor(client): extract UI-agnostic plumbing into pf-client-core (phase 0)

Session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads, keymap, trust
store, mDNS discovery, library client and Wake-on-LAN move verbatim from
clients/linux into crates/pf-client-core, shared with the upcoming Vulkan
session binary (punktfunk-planning: linux-client-rearchitecture.md).
The GTK client re-exports them at the crate root so every existing
crate::-path keeps resolving; its manifest drops the moved-only deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:36:03 +02:00
parent bf07700c74
commit 0ab97b597c
20 changed files with 102 additions and 50 deletions
Generated
+20 -8
View File
@@ -2740,6 +2740,25 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pf-client-core"
version = "0.8.2"
dependencies = [
"anyhow",
"async-channel",
"ffmpeg-next",
"mdns-sd",
"opus",
"pipewire",
"punktfunk-core",
"rustls",
"sdl3",
"serde",
"serde_json",
"tracing",
"ureq",
]
[[package]]
name = "pf-driver-proto"
version = "0.0.1"
@@ -2927,21 +2946,14 @@ version = "0.8.2"
dependencies = [
"anyhow",
"async-channel",
"ffmpeg-next",
"gtk4",
"khronos-egl",
"libadwaita",
"mdns-sd",
"opus",
"pipewire",
"pf-client-core",
"punktfunk-core",
"rustls",
"sdl3",
"serde",
"serde_json",
"tracing",
"tracing-subscriber",
"ureq",
]
[[package]]
+1
View File
@@ -5,6 +5,7 @@ members = [
"crates/punktfunk-host",
"crates/punktfunk-host/vendor/usbip-sim",
"crates/punktfunk-tray",
"crates/pf-client-core",
"crates/pf-driver-proto",
"clients/probe",
"clients/linux",
+1 -1
View File
@@ -20,7 +20,7 @@ punktfunk-core = { path = "../../../crates/punktfunk-core", features = ["quic"]
jni = "0.21"
log = "0.4"
# LAN host discovery: browse the host's `_punktfunk._udp` mDNS advert — the SAME crate + service the
# Linux/Windows clients use (`clients/linux/src/discovery.rs`), replacing Android's per-OEM
# Linux/Windows clients use (`crates/pf-client-core/src/discovery.rs`), replacing Android's per-OEM
# `NsdManager` system daemon with one tested browse path. Pure Rust (socket2/if-addrs/mio), so it
# cross-compiles to the Android targets AND builds on the host (the JNI seam links into
# `cargo build --workspace`). Kotlin keeps only the Wi-Fi `MulticastLock` + permission UX.
+1 -1
View File
@@ -1,5 +1,5 @@
//! LAN host discovery over mDNS, in Rust via `mdns-sd` — the same crate + service type the
//! Linux/Windows clients use (`clients/linux/src/discovery.rs`), exposed to Kotlin over JNI.
//! Linux/Windows clients use (`crates/pf-client-core/src/discovery.rs`), exposed to Kotlin over JNI.
//!
//! Why not `NsdManager`: that API delegates to a per-OEM system mDNS daemon whose reliability
//! varies wildly (the Android client's discovery was "mostly broken"). Browsing in our own Rust
+4 -15
View File
@@ -15,6 +15,10 @@ path = "src/main.rs"
# Everything is Linux-gated so `cargo build --workspace` stays green on macOS (the Mac
# client lives in clients/apple); on other platforms this builds as a stub binary.
[target.'cfg(target_os = "linux")'.dependencies]
# Session pump, decode, audio, gamepads, trust, discovery, keymap — the UI-agnostic
# plumbing, shared with the upcoming Vulkan session binary (design: punktfunk-planning
# linux-client-rearchitecture.md). This crate keeps only the GTK shell + GL presenter.
pf-client-core = { path = "../../crates/pf-client-core" }
punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
# UI shell. GraphicsOffload needs GTK ≥ 4.14; black-background ≥ 4.16. AlertDialog/
@@ -23,25 +27,10 @@ gtk = { package = "gtk4", version = "0.11", features = ["v4_16"] }
adw = { package = "libadwaita", version = "0.9", features = ["v1_5"] }
async-channel = "2"
# Video decode (same FFmpeg pin as the host) and audio.
ffmpeg-next = "8"
opus = "0.3"
pipewire = "0.9"
# Gamepads: capture + feedback (full DualSense fidelity — touchpad/motion/triggers/LEDs
# need the hidapi driver).
sdl3 = { version = "0.18", features = ["hidapi"] }
# The VAAPI GL presenter (video_gl.rs): EGL dmabuf import into a GDK-shared context, dlopened
# at runtime (`dynamic`) so GPU-less boxes and the software path never touch libEGL.
khronos-egl = { version = "6", features = ["dynamic"] }
mdns-sd = "0.20"
# Game-library fetch from the host's management API over mTLS + fingerprint pinning.
# `ureq` is small + sync (the host uses it too) and its rustls unifies with the
# workspace's (quinn's) 0.23; the pinning verifier mirrors core's private `PinVerify`.
ureq = "2"
rustls = { version = "0.23", features = ["ring"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
+6 -6
View File
@@ -78,15 +78,15 @@ src/
ui_trust.rs TOFU / PIN-pairing / request-access dialogs
ui_settings.rs resolution · refresh · decoder · bitrate · compositor · mic
ui_stream.rs the stream window (GtkGraphicsOffload present) + input capture
launch.rs · session.rs session launch/UI glue; lifecycle over the NativeClient connector
video.rs FFmpeg VAAPI / software decode → dmabuf / texture
audio.rs PipeWire playback + mic uplink
gamepad.rs · keymap.rs SDL3 controllers + feedback; keyboard VK mapping
trust.rs · discovery.rs persistent identity, known hosts + settings, mDNS browse
library.rs mgmt-API library client (mTLS + pinned fingerprint, art proxy)
launch.rs session launch/UI glue over the shared session pump
video_gl.rs VAAPI dmabuf → RGBA GL presenter (EGL import, CICP-driven CSC)
tools/screenshots.sh store screenshot capture (app self-capture; Xvfb fallback)
```
The UI-agnostic plumbing — session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads +
keymap, trust store, mDNS discovery, library client, Wake-on-LAN — lives in
`crates/pf-client-core`, shared with the Vulkan session binary.
## Related
- **[Documentation](https://docs.punktfunk.unom.io)** — quick start, pairing, troubleshooting
+6 -18
View File
@@ -5,27 +5,19 @@
//! mirrors the Apple client: persistent identity, TOFU prompt with the host fingerprint,
//! SPAKE2 PIN pairing.
// The UI-agnostic plumbing lives in `pf-client-core`, shared with the upcoming Vulkan
// session binary (design: punktfunk-planning linux-client-rearchitecture.md, Phase 0).
// Root re-exports keep every existing `crate::video`-style path resolving unchanged.
#[cfg(target_os = "linux")]
pub use pf_client_core::{audio, discovery, gamepad, keymap, library, session, trust, video, wol};
#[cfg(target_os = "linux")]
mod app;
#[cfg(target_os = "linux")]
mod audio;
#[cfg(target_os = "linux")]
mod cli;
#[cfg(target_os = "linux")]
mod discovery;
#[cfg(target_os = "linux")]
mod gamepad;
#[cfg(target_os = "linux")]
mod keymap;
#[cfg(target_os = "linux")]
mod launch;
#[cfg(target_os = "linux")]
mod library;
#[cfg(target_os = "linux")]
mod session;
#[cfg(target_os = "linux")]
mod trust;
#[cfg(target_os = "linux")]
mod ui_gamepad_library;
#[cfg(target_os = "linux")]
mod ui_hosts;
@@ -38,12 +30,8 @@ mod ui_stream;
#[cfg(target_os = "linux")]
mod ui_trust;
#[cfg(target_os = "linux")]
mod video;
#[cfg(target_os = "linux")]
mod video_gl;
mod wol;
#[cfg(target_os = "linux")]
fn main() -> gtk::glib::ExitCode {
app::run()
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "pf-client-core"
description = "Shared Linux-client plumbing — session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads, trust store, discovery — extracted from the GTK client so the shell and the Vulkan session binary build on one implementation"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
# Same Linux gating as clients/linux: `cargo build --workspace` stays green on macOS
# (the Mac client lives in clients/apple); elsewhere this crate is `wol` plus stubs-free
# emptiness. `wol` is pure std and stays cross-platform, matching the old main.rs.
[target.'cfg(target_os = "linux")'.dependencies]
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
async-channel = "2"
# Video decode (same FFmpeg pin as the host) and audio.
ffmpeg-next = "8"
opus = "0.3"
pipewire = "0.9"
# Gamepads: capture + feedback (full DualSense fidelity — touchpad/motion/triggers/LEDs
# need the hidapi driver).
sdl3 = { version = "0.18", features = ["hidapi"] }
mdns-sd = "0.20"
# Game-library fetch from the host's management API over mTLS + fingerprint pinning.
# `ureq` is small + sync (the host uses it too) and its rustls unifies with the
# workspace's (quinn's) 0.23; the pinning verifier mirrors core's private `PinVerify`.
ureq = "2"
rustls = { version = "0.23", features = ["ring"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
+26
View File
@@ -0,0 +1,26 @@
//! Shared, UI-agnostic Linux-client plumbing, extracted verbatim from the GTK client
//! (design: punktfunk-planning `linux-client-rearchitecture.md`, Phase 0) so the desktop
//! shell and the Vulkan session binary build on one implementation.
//!
//! Nothing here may depend on a UI toolkit: the presenter contract is `session`'s
//! channels (`SessionHandle`) and `video`'s `DecodedImage` (RGBA bytes or dmabuf fds +
//! plane layout) — how frames reach the screen is the consumer's business.
#[cfg(target_os = "linux")]
pub mod audio;
#[cfg(target_os = "linux")]
pub mod discovery;
#[cfg(target_os = "linux")]
pub mod gamepad;
#[cfg(target_os = "linux")]
pub mod keymap;
#[cfg(target_os = "linux")]
pub mod library;
#[cfg(target_os = "linux")]
pub mod session;
#[cfg(target_os = "linux")]
pub mod trust;
#[cfg(target_os = "linux")]
pub mod video;
pub mod wol;
+1 -1
View File
@@ -23,7 +23,7 @@ anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Loopback HTTPS poll of GET /api/v1/local/summary. Same sync ureq + rustls(ring) stack and
# custom-verifier pattern as the Linux client's library fetch (clients/linux/src/library.rs) —
# custom-verifier pattern as the Linux client's library fetch (crates/pf-client-core/src/library.rs) —
# but ring-only (no default aws-lc-rs provider: it needs a C toolchain per target and the agent
# pins the ring provider explicitly anyway).
ureq = { version = "2", default-features = false, features = ["tls"] }