Files
enricobuehler 8956bc14de
apple / swift (push) Successful in 53s
android / android (push) Successful in 3m48s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 34s
ci / rust (push) Successful in 2m21s
ci / bench (push) Successful in 1m36s
decky / build-publish (push) Successful in 31s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
flatpak / build-publish (push) Failing after 4s
deb / build-publish (push) Successful in 2m38s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m9s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m42s
docker / deploy-docs (push) Successful in 16s
feat(packaging/flatpak,decky): Steam Deck client flatpak + plugin deploy + CI
Ship the punktfunk Linux client to the Steam Deck as a Flatpak — the only viable
SteamOS install path, since /usr is read-only and lacks libadwaita/SDL3 — and
publish both it and the Decky plugin through Gitea. Built and validated live on a
Steam Deck (SteamOS 3.7): bundle installs user-scope, all libs resolve, libavcodec
resolves to the codecs-extra HEVC build, devices=all for DualSense hidraw.

packaging/flatpak (new):
- io.unom.Punktfunk.yml on GNOME 50 / freedesktop-sdk 25.08. rust-stable//25.08
  (rustc 1.96 — the GTK4 chain needs >=1.92; the EOL GNOME-48/24.08 rust-stable at
  1.89 could not build it) + llvm20 (libclang for bindgen in ffmpeg-sys-next/sdl3-sys).
  HEVC libavcodec comes from the runtime's auto codecs-extra extension point (no
  app-side codec declaration). Bundled SDL3 3.4.10 (matches sdl3-sys 0.6.6+SDL-3.4.10).
  finish-args: wayland/fallback-x11, --device=all (GPU/VAAPI + evdev + hidraw — flatpak
  cannot bind /dev/hidrawN char devices via --filesystem), pulseaudio, network,
  ~/.config/punktfunk.
- metainfo.xml, desktop, square SVG icon, build-flatpak.sh (offline cargo-sources;
  on-Deck org.flatpak.Builder or CI), README.

clients/decky:
- add LICENSE (MIT), fix package.json license (BSD-3-Clause -> Apache-2.0 OR MIT),
  add scripts/{package.sh,deploy.sh} (the plugins dir is root-owned: stage to /tmp,
  sudo install, restart plugin_loader), align the launcher fallback to the real
  flatpak app id io.unom.Punktfunk, rewrite the install section.

.gitea/workflows:
- flatpak.yml: privileged Fedora container builds the bundle and publishes to the
  Gitea generic registry (+ release attachment on tags).
- decky.yml: pnpm build -> store-layout zip -> registry (stable latest/ URL for
  Decky "install from URL").

docs: packaging/README + packaging/flatpak/README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:43:35 +02:00

6.2 KiB

punktfunk Decky plugin (SteamOS / Steam Deck)

A Decky Loader plugin that adds a punktfunk panel to the Steam Deck's Quick Access Menu (the QAM, opened with the button), so you can launch the punktfunk streaming client from Gaming Mode without dropping to the desktop.

Because Decky plugins run inside Steam's CEF, the panel is built from real Steam UI primitives (@decky/ui: PanelSection, PanelSectionRow, ButtonItem, Field, Spinner) — so it looks and feels native to Gaming Mode.

Spike / launcher only. This is a minimal but functional first cut: discover hosts, connect, disconnect. It launches the existing native GTK4 client (punktfunk-client) over the top of Gaming Mode. An in-stream overlay (latency / bitrate HUD, mid-session controls) and a fuller real-Steam-components UI are the next steps. Runtime behavior on a real Deck is untested — only the build is verified here.

What it does

  1. Refresh — browses the LAN over mDNS for punktfunk/1 hosts (the _punktfunk._udp service) via the backend discover().
  2. Lists discovered hosts — name, ip:port, and a lock icon for whether pairing is required (pair=required in the host's TXT record).
  3. Connect — selecting a host calls connect(host, port), which launches punktfunk-client --connect host:port; a toast and the status line reflect the result.
  4. Disconnectdisconnect() terminates the launched client.

Architecture

File Role
src/index.tsx Frontend QAM panel (@decky/ui + @decky/api).
main.py Backend Plugin class: discover / connect / disconnect / status exposed over the Decky bridge.
plugin.json Decky plugin manifest.
decky.pyi Type stub for the injected decky module (vendored from the template).

Discovery (discover())

Shells out to avahi-browse -rpt _punktfunk._udp (SteamOS and Bazzite ship avahi-daemon; this avoids bundling python-zeroconf):

  • -r resolve services, -p parseable output, -t terminate after the cache dump.
  • Resolved records start with = and are semicolon-separated: =;iface;protocol;name;type;domain;hostname;address;port;txt.
  • The txt column is space-separated, quoted "key=value" tokens. We read the keys the host advertises (crates/punktfunk-host/src/discovery.rs): proto, fp, pair, id.
  • Records are deduped on the id TXT key (a host re-advertises per interface and across IPv4/IPv6), preferring the IPv4 address for the user-facing host string.

Client launch (connect())

The client binary punktfunk-client is resolved in order: PATH/usr/bin/usr/local/bin~/.local/bin → a flatpak run io.unom.Punktfunk fallback. The resolved argv and a clear client-not-found error surface to the UI. The child PID is tracked so disconnect() (and plugin _unload) can terminate it.

On the Steam Deck the client install is the flatpak io.unom.Punktfunk (packaging/flatpak/) — SteamOS /usr is read-only and lacks libadwaita/libSDL3, so the flatpak (which bundles them) is the canonical path; the resolver's flatpak fallback launches exactly that.

Prerequisites

  • Decky Loader installed on the Deck (https://decky.xyz/).
  • punktfunk-client (the GTK4/libadwaita Linux client, crate punktfunk-client-linux) installed and runnable on the Deck — via .deb/RPM/flatpak, or symlinked into ~/.local/bin.
  • avahi (avahi-daemon + avahi-browse) for discovery — present on SteamOS/Bazzite.
  • A punktfunk/1 host on the LAN (punktfunk-host serve --native or m3-host).

Build

pnpm install
pnpm build      # rollup → dist/index.js

(npm install && npm run build also works.)

Install on the Deck

CI (.gitea/workflows/decky.yml) builds the plugin into a store-layout zip and publishes it to Gitea's generic package registry on every push to main and on v* tags, exposing a stable URL. In Decky's settings → Developer ModeInstall Plugin from URL, paste:

https://git.unom.io/api/packages/unom/generic/punktfunk-decky/latest/punktfunk.zip

(or a pinned version: .../punktfunk-decky/<version>/punktfunk.zip). On tags the same zip is also attached to the Gitea release. The zip's layout is the store-required one — a single top-level punktfunk/ dir holding plugin.json, package.json, main.py, dist/index.js, README.md, and LICENSE.

Option B — manual dev copy (sideload)

Decky's ~/homebrew/plugins/ is root-owned (PluginLoader runs as root and manages it), so a plain rsync into it fails — stage to a writable temp dir, then sudo-install and restart the loader. The two helper scripts do exactly this:

cd clients/decky
pnpm install
pnpm run package                       # → out/punktfunk/ + out/punktfunk-v<ver>.zip
DECK=deck@<deck-ip> pnpm run deploy    # rsync → /tmp, sudo cp into plugins/, chown root, restart

deploy.sh prompts for the Deck's sudo password interactively (via ssh -t); set DECKPASS=… to run it non-interactively. Equivalent by hand:

cd clients/decky && pnpm build && bash scripts/package.sh
rsync -azp --delete out/punktfunk/ deck@<deck-ip>:/tmp/punktfunk/
ssh -t deck@<deck-ip> 'sudo sh -c "rm -rf ~deck/homebrew/plugins/punktfunk && \
  cp -r /tmp/punktfunk ~deck/homebrew/plugins/punktfunk && \
  chown -R root:root ~deck/homebrew/plugins/punktfunk && systemctl restart plugin_loader"'

A loader restart is required for an out-of-band install to appear. The punktfunk panel then shows up in the Quick Access Menu.

The plugin launches the client via the flatpak io.unom.Punktfunk (see ../../packaging/flatpak/README.md) — install that on the Deck too, or the panel's Connect surfaces a client-not-found error.

Limitations / next steps

  • Launcher only — no in-stream overlay yet; the client owns the full session once launched.
  • mDNS discovery depends on avahi-browse; no manual "add host by IP" entry yet.
  • Pairing (PIN ceremony) is handled by the launched client, not the panel.
  • Not yet tested on real Deck hardware.