Files
enricobuehler 99eb679c07 feat(clients): a moved mgmt port now outlives the advert that announced it
Moving the mgmt port off 47990 (the fix for sharing a box with a Sunshine fork, whose web UI owns
that port) only ever worked for as long as mDNS did. The real port lived in the advert and nowhere
else: every client read it live and threw it away, so on a VPN, a routed subnet, or any
multicast-dead network the library silently fell back to a port nothing was listening on.

`KnownHost` gains `mgmt_port: Option<u16>` + `effective_mgmt_port()` + `learn_mgmt_port()`, exactly
the shape `mac` and `os` already use ("learned from the advert while online, persisted so it
survives the host going to sleep") — except this one is load-bearing rather than cosmetic, so
`upsert` states the preserve rule explicitly instead of relying on the does-not-mention-it accident
that `clipboard_sync` survives by, and `upsert_trusted` carries it across a re-key.

Wired through all four client families, each of which was wrong in its own way:

* CLI / Windows / Linux reached for `DEFAULT_MGMT_PORT` at the call site — the constant is the
  FALLBACK, not the answer. Windows also needed the port on `Target`, which the library screen has
  instead of a `KnownHost`.
* The session console read `advert.and_then(mgmt_port)` with NO saved fallback, two lines above an
  `os` that gets the three-rung treatment right. It now matches, and learns on every tick.
* Linux's `mgmt_port_for` consulted live adverts only; it now falls back to the store.
* Android never carried the port at all — its native discovery record stopped at 8 fields. Added
  `mgmt` as the 9th (the record's own documented "new fields append, never reorder" rule), then
  through `DiscoveredHost` -> `KnownHost` -> `LibraryScreen`.
* Apple LOOKED done and was not: `StoredHost.mgmtPort` and `effectiveMgmtPort` have existed all
  along, but nothing anywhere wrote the field and the `mgmt` TXT was never parsed — so it was
  permanently nil and every Apple client resolved to 47990 regardless. That is worse than the
  honest omissions above, because it reads as finished. Now parsed, carried on `DiscoveredHost`,
  and written by `HostStore.updateMgmtPort` at the same site that learns MACs and the OS chain.

Also `PUNKTFUNK_NATIVE_PORT` in host.env, finishing the pair with PUNKTFUNK_MGMT_BIND: `--native-port`
was likewise CLI-only and died on a package upgrade. A bad value is a startup ERROR rather than the
silent fall back to 9777 that `PUNKTFUNK_DATA_PORT` still does — the failure that reads as "I moved
the port and the client still can't reach me". The client side of the native port already worked
(`KnownHost.port` is persisted, `--connect HOST:PORT` names it).

Adding the field broke three `KnownHost` literals in tests, which is the `Default` impl's stated
purpose working ("adding a field here can't silently produce records that lack it"). All three now
carry 47991 — deliberately NOT the default, so the assertions cannot pass vacuously against a
hardcode. New coverage: forward-compat decode of a store predating the field, the resolver
fallback, re-key carry-forward, and on Android the 9th-field parse plus 0/non-numeric/out-of-range
all reading as unknown.

What this does NOT fix: a host that moved its mgmt port and has NEVER been seen over mDNS. Nothing
tells the client where to look, and the honest fix is for the host to announce it in-band — the
`Welcome` message has an established "append a trailing field, older peer decodes to the default"
pattern for exactly this, at the cost of a C ABI accessor and a bump. Left for a separate change.

Verified: Linux (punktfunk-rust-ci/pf-lxcheck2, amd64) `cargo check --all-targets` clean for
pf-host-config, punktfunk-host, pf-client-core, punktfunk-cli, punktfunk-client-linux and
punktfunk-client-session — the last confirmed non-vacuous by planting a compile_error! and watching
the gate fail (cargo prints "Compiling", not "Checking", for bin-only packages, so the usual marker
grep lies about it). Android: :kit + :app compileDebugKotlin clean, ParseRecordTest 12/12 with both
new cases named in the XML. Apple: xcframework built, `swift build` complete, SharedFoundationTests
pass. cargo fmt --all --check clean. NOT verified: the Windows client (192.168.1.133 unreachable).
2026-08-14 19:44:19 +02:00
..

punktfunk — Linux client

The native Linux app for streaming a punktfunk host to your desktop, laptop, or Steam Deck. It's a clean relm4/GTK4/libadwaita shell that finds hosts on your network, pairs with a PIN, and manages your settings and library — the stream itself runs in the sibling punktfunk-session Vulkan binary (clients/session), which the shell spawns, putting the picture on glass at your display's own resolution and refresh rate.

Built in Rust end to end (no C ABI): the shell shares its plumbing with the session binary through crates/pf-client-core, which links the punktfunk-core protocol crate and speaks the fast punktfunk/1 protocol — QUIC control plane, GF(2¹⁶) FEC + AES-GCM data plane.

Features

  • Zero-copy hardware decode, and it's ours — the session presenter decodes with Punktfunk's own decoders; no FFmpeg is linked or bundled. Vulkan Video (pf-vkdecode, decoding onto the presenter's own device) leads on NVIDIA and AMD, VAAPI (pf-vaadec driving a dlopen'd libva, exporting DRM-PRIME dmabufs) leads on Intel, whichever isn't first is the fallback, and an OpenH264/rav1d CPU rung is last.
  • Your display's native mode — the host builds a virtual output at exactly your WxH@Hz; no scaling, no letterboxing. Steady 60 fps at 1080p60, ~6 ms capture→decoded on the LAN.
  • Audio both ways — PipeWire playback with a jitter ring, plus mic uplink to the host.
  • Full controller support — SDL3 gamepads with rumble and DualSense fidelity (lightbar, player LEDs, touchpad, motion, adaptive-trigger replay). Click-to-capture keyboard and mouse, with a release chord (Ctrl+Alt+Shift+Q) and focus-loss release.
  • Find hosts automatically — mDNS discovery lists hosts on your LAN; saved hosts persist. First connect does a one-time SPAKE2 PIN pairing (or TOFU on trusted LANs), then reconnects on a pinned identity.
  • Per-host speed test to pick a bitrate, plus compositor and mode preferences in Settings.
  • Game library browser (experimental, off by default) — "Browse library…" on a saved host shows its games (Steam + custom) as a poster grid; click one to launch it in the session. Fetched from the host's management API over mTLS — paired devices are authorized by their certificate, no extra host setup.
  • Gamepad library launcher (--browse host) — a console-style, controller-driven library view of a paired host's games, rendered by the session binary's Skia console UI: A plays the focused title, B quits, L1/R1 jump. Built for the Steam Deck plugin's "Open library" launch; session end returns to the launcher. Arrow keys/Enter/Esc drive it too (no pad needed).

Get it

Most people should install a package rather than build from source:

Distro Install
Flatpak (any distro, Steam Deck) io.unom.Punktfunk — see packaging/flatpak
Ubuntu / Debian (apt) sudo apt install punktfunk-client (after adding the repo)
Fedora / Bazzite (rpm) rpm-ostree install punktfunk-client
Arch (PKGBUILD) see packaging/arch

Per-device install steps and pairing walkthrough: docs.punktfunk.unom.io/docs/install-client.

Build & run from source

Requires GTK ≥ 4.16, libadwaita ≥ 1.5, PipeWire, and SDL3 (with hidapi) development packages, plus a C compiler (the CPU rung builds OpenH264 from source). No decoder development package is needed: libva and the Vulkan loader are both opened at runtime rather than linked, so hardware decode is a fact about the box you run on — a Vulkan loader and your GPU's driver, and libva for the VAAPI rung — not about the one you build on.

# from the repo root
cargo run -p punktfunk-client-linux                 # launch the app
cargo run -p punktfunk-client-linux -- --connect HOST[:PORT]   # skip the host list and connect
cargo run -p punktfunk-client-linux -- --browse HOST           # the gamepad library launcher

The binary is named punktfunk-client — the relm4/libadwaita desktop shell (hosts, pairing/trust, settings, the desktop library page). Every stream and the console game library run in the sibling punktfunk-session Vulkan binary; the shell spawns it for connects, and --connect/--browse on the shell exec it directly (so the Decky wrapper keeps working unchanged). Headless flags stay in the shell: --pair <PIN> --connect host[:port] (pairing ceremony), --wake host[:port], and --library host[:mgmt_port] (print a host's game library).

Layout

src/
  main.rs · app.rs        entry point, relm4 AppModel (window, trust gate, session child
                          lifecycle, typed messages), primary menu, CSS
  cli.rs                  headless paths (--pair/--wake/--library), the --connect/--browse
                          exec handoff to punktfunk-session, screenshot scenes
  ui_hosts.rs             hosts page component (FactoryVecDeque cards, saved + discovered
                          grids, add-host dialog, banner)
  ui_library.rs           game-library poster grid (per-host, launches titles)
  ui_trust.rs             TOFU / PIN-pairing / request-access dialogs
  ui_settings.rs          resolution · refresh · decoder · bitrate · compositor · mic
  spawn.rs                the session-child plumbing (stdout contract → AppMsg)
tools/screenshots.sh      store screenshot capture (app self-capture; Xvfb fallback)

The UI-agnostic plumbing — session pump, the native decode ladder (Vulkan Video · VAAPI · OpenH264/rav1d), 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.