Docs for two audiences: a get-started track that fits on one screen, a Switching-from-Sunshine page, and install commands quoted from platforms.json #340

Merged
enricobuehler merged 1 commits from worktree-docs-wp2-rewrite into worktree-docs-wp0-wp1-anti-drift 2026-08-19 18:16:31 +00:00
57 changed files with 2713 additions and 3239 deletions
+2 -1
View File
@@ -112,7 +112,8 @@ dense. When a change touches a user-facing fact, update the docs-site page that
PR.
CI enforces the cheap half of this (`scripts/ci/check-docs-drift.sh` and `check-docs-links.sh`):
the OpenAPI snapshot must match `api/openapi.json`, every `PUNKTFUNK_*` variable the docs mention
the OpenAPI snapshot must match `api/openapi.json`, the docs-site copy of `data/platforms.json` must
match the canonical one, every `PUNKTFUNK_*` variable the docs mention
must still exist in the tree, the counts of undocumented `PUNKTFUNK_*` variables and undocumented
`punktfunk-host` subcommands may never grow (document the new knob, or consciously raise the
baseline in the script), and internal docs links must resolve.
+95 -15
View File
@@ -1,14 +1,20 @@
{
"$comment": "Single source for the install/port facts that used to drift across four surfaces (docs-and-onboarding-overhaul WP1). Consumers: docs-site install pages (WP2), the website download page (WP3), the guided install script (WP4). A port number, repo URL or install command lives HERE and nowhere else — pages quote it, they don't restate it. Validated by scripts/ci/check-docs-drift.sh (parse only; ponytail: cross-check against code literals when a consumer exists).",
"$comment": "Single source for the install/port facts that used to drift across four surfaces (docs-and-onboarding-overhaul WP1). Consumers: docs-site install pages (<Install platform=…/> and <Ports/> in docs-site/src/components/platforms.tsx, reading the byte-identical snapshot docs-site/src/data/platforms.json that check-docs-drift.sh gates), the website download page (WP3), the guided install script (WP4). A port number, repo URL or install command lives HERE and nowhere else — pages quote it, they don't restate it. `install` is the shell snippet that puts the package on the box (lines, in order); `url` is a download/store link where there is no command. Validated by scripts/ci/check-docs-drift.sh (parse + snapshot sync; ponytail: cross-check against code literals when a consumer exists).",
"ports": {
"mgmt": {
"port": 47990,
"proto": "tcp",
"what": "management REST API + web console endpoint",
"what": "management REST API (HTTPS + token; read-only status/library off loopback)",
"env": "PUNKTFUNK_MGMT_BIND",
"conflict": "Sunshine / Apollo / Vibeshine serve their web UI on 47990 too — the one port still shared with them once GameStream compat is off. Move it via PUNKTFUNK_MGMT_BIND; clients relearn it from discovery."
},
"web": {
"port": 47992,
"proto": "tcp",
"what": "web console (HTTPS, login-gated); plugin interfaces on 47993",
"also": [47993]
},
"native": {
"port": 9777,
"proto": "udp",
@@ -18,7 +24,7 @@
"data": {
"port": null,
"proto": "udp",
"what": "per-session video data plane — random port + hole-punch by default, pinnable",
"what": "per-session video data plane — ephemeral port the client hole-punches; nothing fixed to open",
"env": "PUNKTFUNK_DATA_PORT"
},
"mdns": {
@@ -29,26 +35,36 @@
"gamestream": {
"tcp": [47984, 47989, 48010],
"udp": [47998, 47999, 48000],
"what": "GameStream/Moonlight-compat planes (opt-in, `serve --gamestream`)",
"what": "GameStream/Moonlight-compat planes (opt-in, PUNKTFUNK_GAMESTREAM=1)",
"conflict": "Sunshine / Apollo / Vibeshine bind these same fixed ports and advertise the same mDNS name — run only one GameStream host at a time, or keep punktfunk native-only."
}
},
"firewall": {
"$comment": "Service/profile names the Linux packages install for firewalld and ufw — the packages never open a port themselves.",
"native": "punktfunk-native",
"gamestream": "punktfunk-gamestream",
"web": "punktfunk-web"
},
"conflicts": {
"hosts": ["Sunshine", "Apollo", "Vibeshine"],
"detect": "punktfunk-host detect-conflicts",
"detectExit": "1 only when a conflicting host runs or will start on its own; dormant leftovers print but exit 0",
"docs": "/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed"
"docs": "/docs/switching-from-sunshine"
},
"platforms": [
{
"id": "debian",
"name": "Debian 13 / Ubuntu 24.04+",
"name": "Debian 13+ / Ubuntu 26.04+",
"installs": "host",
"packageManager": "apt",
"docs": "/docs/debian",
"repo": "https://git.unom.io/api/packages/unom/debian",
"install": [
"sudo install -d -m 0755 /etc/apt/keyrings",
"curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key | sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null",
"echo \"deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main\" | sudo tee /etc/apt/sources.list.d/punktfunk.list",
"sudo apt update",
"sudo apt install punktfunk-host"
@@ -60,11 +76,12 @@
"installs": "host",
"packageManager": "pacman",
"docs": "/docs/arch",
"repo": "https://git.unom.io/api/packages/unom/arch",
"install": [
"curl -fsS https://git.unom.io/api/packages/unom/arch/repository.key | sudo pacman-key --add -",
"sudo pacman-key --lsign-key E0CA04465C99C936E0B0C6510A317015A34DDD69",
"printf '\\n[punktfunk]\\nServer = https://git.unom.io/api/packages/unom/arch/$repo/$arch\\n' | sudo tee -a /etc/pacman.conf >/dev/null",
"sudo pacman -Sy punktfunk-host"
"grep -q '^\\[punktfunk\\]' /etc/pacman.conf || printf '\\n[punktfunk]\\nServer = https://git.unom.io/api/packages/unom/arch/$repo/$arch\\n' | sudo tee -a /etc/pacman.conf >/dev/null",
"sudo pacman -Syu punktfunk-host"
]
},
{
@@ -75,8 +92,18 @@
"docs": "/docs/fedora",
"repo": "https://git.unom.io/api/packages/unom/rpm/fedora-44",
"install": [
"sudo dnf install punktfunk",
"sudo usermod -aG input \"$USER\""
"sudo tee /etc/yum.repos.d/punktfunk.repo >/dev/null <<'REPO'",
"[punktfunk]",
"name=punktfunk",
"# fedora-44 on Fedora 44; bazzite on Fedora 43 (a plain Fedora 43 build of the same package)",
"baseurl=https://git.unom.io/api/packages/unom/rpm/fedora-44",
"enabled=1",
"gpgcheck=1",
"repo_gpgcheck=1",
"gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key",
" https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk",
"REPO",
"sudo dnf install punktfunk"
]
},
{
@@ -95,20 +122,32 @@
"name": "NixOS",
"installs": "host",
"packageManager": "nix",
"docs": "/docs/install",
"docs": "/docs/nixos",
"install": [
"# flake input: inputs.punktfunk.url = \"git+https://git.unom.io/unom/punktfunk\";",
"# then: imports = [ punktfunk.nixosModules.default ]; services.punktfunk.host.enable = true;"
]
},
{
"id": "steamos",
"name": "SteamOS (Steam Deck as host)",
"installs": "host",
"packageManager": "script",
"docs": "/docs/steamos-host",
"install": [
"git clone https://git.unom.io/unom/punktfunk ~/punktfunk",
"bash ~/punktfunk/scripts/steamdeck/install.sh"
]
},
{
"id": "windows",
"name": "Windows 10/11",
"name": "Windows 11 (22H2+)",
"installs": "host",
"packageManager": "winget",
"docs": "/docs/windows-host",
"install": [
"winget install unom.PunktfunkHost --interactive"
"winget source add -n punktfunk https://winget.punktfunk.unom.io -t Microsoft.Rest",
"winget install unom.PunktfunkHost"
]
},
{
@@ -116,11 +155,52 @@
"name": "Linux client (any distro)",
"installs": "client",
"packageManager": "flatpak",
"docs": "/docs/install-client",
"docs": "/docs/install-client#linux-desktop-flatpak",
"install": [
"flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref"
]
},
{
"id": "steam-deck-client",
"name": "Steam Deck (Gaming Mode)",
"installs": "client",
"packageManager": "decky",
"docs": "/docs/steam-deck"
},
{
"id": "windows-client",
"name": "Windows client",
"installs": "client",
"packageManager": "msix",
"docs": "/docs/install-client#windows",
"install": [
"curl.exe -LO https://git.unom.io/api/packages/unom/generic/punktfunk-client-windows/latest/punktfunk-client-windows_x64.msix",
"Add-AppxPackage .\\punktfunk-client-windows_x64.msix"
]
},
{
"id": "macos-client",
"name": "macOS",
"installs": "client",
"packageManager": "dmg",
"docs": "/docs/install-client#macos",
"url": "https://git.unom.io/unom/punktfunk/releases"
},
{
"id": "apple-client",
"name": "iPhone, iPad, Apple TV",
"installs": "client",
"packageManager": "testflight",
"docs": "/docs/install-client#ios-ipados-apple-tv",
"url": "https://testflight.apple.com/join/Qr7uSemk"
},
{
"id": "android-client",
"name": "Android / Android TV",
"installs": "client",
"packageManager": "play",
"docs": "/docs/install-client#android",
"url": "https://play.google.com/store/apps/details?id=io.unom.punktfunk"
}
]
}
+13
View File
@@ -29,6 +29,19 @@ of `api/openapi.json`, and the `rust` job regenerates the spec and diffs it agai
one — so a management-API change can't publish stale API docs any more, it fails CI until you run
the two commands above.
## Install commands and ports
`src/data/platforms.json` is a byte-identical snapshot of the repo-root
[`data/platforms.json`](../data/platforms.json) — the single source for install commands, repo
URLs, port facts and the Sunshine/Apollo/Vibeshine conflict facts. The `<Install platform="…" />`
and `<Ports />` MDX components (`src/components/platforms.tsx`) render from it, so no page restates
a command or a port. It's a snapshot for the same reason as `openapi.json` (the Docker build context
is this directory alone), and the same `docs-drift` job fails unless it matches:
```sh
cp data/platforms.json docs-site/src/data/platforms.json # from the repo root, after editing the canonical file
```
## Develop
```sh
+37 -39
View File
@@ -3,15 +3,14 @@ title: Access levels
description: What each paired device may do, and for how long — the three presets, the advanced toggles, temporary access that expires on its own, and what access control honestly does not cover.
---
Pairing used to be all-or-nothing: a paired device had full control of the host, forever. **Access**
changes that. Every paired device carries an **access level** — what it may send to the host — and
optionally an expiry — how long that lasts. A friend's phone can be a second controller for the
evening and nothing more; the living-room TV can watch and play but never type into your desktop;
a spectator can see and hear without sending anything.
Pairing used to be all-or-nothing: a paired device had full control of the host, forever. Now every
paired device carries an **access level** — what it may send to the host — and optionally an
expiry: a friend's phone as a second controller for the evening, a TV that can play but never type,
a spectator who only watches.
Access is **enforced by the host**. A client's UI reflects its access as a courtesy, but the host
drops anything a device isn't granted regardless of what the client sends — nothing a client can
send widens its own access.
drops anything a device isn't granted regardless of what the client sends — nothing a client sends
can widen its own access.
You manage access from the host's [web console](/docs/web-console): when you
[approve a device or arm pairing](/docs/pairing#choosing-access-when-you-admit-a-device), and any
@@ -22,16 +21,16 @@ countdown if it expires) and an edit sheet.
| Access level | What the device can do |
|---|---|
| **Full control** | Everything — keyboard, mouse, controllers, clipboard, microphone, launching games. This is what pairing has always meant, and it stays the default: every device paired before access levels existed keeps full control, and so does a plain **Approve**. |
| **Full control** | Everything — keyboard, mouse, controllers, clipboard, microphone, launching games. What pairing has always meant, and still the default: every device paired before access levels existed keeps full control, and so does a plain **Approve**. |
| **Controller only** | Gamepad input only — the guest and co-play preset. The device's pads show up as additional controllers (with rumble and pad audio), but it cannot type, move the mouse, read the clipboard, use the mic, or launch anything. |
| **View only** | See and hear the stream, send nothing. The spectator preset. |
The preset label is derived from the underlying toggles, so a hand-tuned combination simply shows
as **Custom** — there is no separate thing to keep in sync.
The preset label is derived from the underlying toggles, so a hand-tuned combination shows as
**Custom** — there is no separate thing to keep in sync.
## The advanced toggles
Each preset is a bundle of six independent grants, exposed under **Advanced** in the edit sheet:
Each preset is a bundle of six independent grants, under **Advanced** in the edit sheet:
| Toggle | Covers |
|---|---|
@@ -40,13 +39,13 @@ Each preset is a bundle of six independent grants, exposed under **Advanced** in
| **Keyboard** | Key presses. |
| **Clipboard** | The [shared clipboard](/docs/clipboard). Both switches still apply: the host operator's clipboard policy *and* this grant have to allow it — the grant can only narrow, never widen, what the operator permits. An ungranted device gets a clean "not permitted" instead of a toggle that silently does nothing. |
| **Microphone** | Sending the client's microphone to the host. Without it, the session never attaches to the host's mic service at all. |
| **Launch** | Starting a game from the host's [library](/docs/game-library) when connecting. Without it, a connect that asks to launch is refused with a clear error rather than being dropped onto the bare desktop. The library remains *visible* — this governs launching, not browsing. |
| **Launch** | Starting a game from the host's [library](/docs/game-library) when connecting. Without it, a connect that asks to launch is refused with a clear error rather than dropped onto the bare desktop. The library stays *visible* — this governs launching, not browsing. |
**Controller only deliberately does not include Launch**: in co-play the owner drives what runs. If
you want a guest picking games from the couch, that's one Advanced toggle away.
**Controller only deliberately does not include Launch**: in co-play the owner drives what runs.
Want a guest picking games? Turn on that one Advanced toggle.
A session's quality controls — resolution, bitrate, keyframe requests — are *not* governed. They
only shape that device's own stream, so restricting them would cost usability and buy no security.
only shape that device's own stream; restricting them would cost usability and buy no security.
## Temporary access
@@ -54,7 +53,7 @@ Any grant can carry an expiry, picked when you approve the device or set later i
**1 h / 4 h / 8 h / custom / forever**.
- Expiry is **wall-clock time on the host** — "4 hours" means four hours from now by the host's
clock, matching the mental model of "until tonight".
clock.
- A device streaming when its access runs out gets **warnings at 5 minutes and 1 minute** before
the deadline, then its session ends with an explicit reason: *"Your access to this host has
expired."* Only that device's sessions end — yours is untouched.
@@ -65,43 +64,42 @@ Any grant can carry an expiry, picked when you approve the device or set later i
extending re-arms the running session's deadline, and Expire now ends it with the same clean
"access expired" message — no lingering stream.
Edits other than expiry are just as immediate: changing a device's access level while it streams
takes effect within moments, and removing the device ends its sessions. Access is per *device*,
not per session — two sessions from the same device share one grant.
Other edits are just as immediate: changing a device's access level while it streams takes effect
within moments, and removing the device ends its sessions. Access is per *device*, not per session
— two sessions from the same device share one grant.
## What this does not cover
Be honest with yourself about three limits before relying on access levels:
Three limits before relying on access levels:
> **A view-only guest still sees your whole desktop.** On the shared-desktop backends every
> session shows the *same* desktop — access levels govern what a device can send *in*, not what it
> sees going *out*. A view-only or controller-only guest watches and hears everything you do,
> notifications included. Don't read email with a spectator attached.
> notifications included.
- **Moonlight / GameStream devices are not governed yet.** Access levels currently apply to the
native Punktfunk protocol. A device paired via [Moonlight](/docs/moonlight) has full control and
shows an honest **Full (ungoverned)** chip in the console — not a fake editor. When enforcement
reaches the GameStream plane, it will be *silent* from the client's side: the GameStream
protocol has no way to tell a Moonlight client about its access, so an ungranted keyboard will
simply be inert, with the explanation visible only in the console.
- **Moonlight / GameStream devices are not governed yet.** Access levels apply to the native
Punktfunk protocol. A device paired via [Moonlight](/docs/moonlight) has full control and shows
an honest **Full (ungoverned)** chip in the console — not a fake editor. When enforcement reaches
the GameStream plane it will be *silent* from the client's side: the protocol has no way to tell
a Moonlight client about its access, so an ungranted keyboard will simply be inert, with the
explanation visible only in the console.
- **Older Punktfunk clients are enforced, but can't explain it.** The host enforces access
identically for every client version. A client from before this feature just lacks the chrome:
identically for every client version; a client from before this feature just lacks the chrome:
no "Controller only · ends in 2 h" chip, no expiry warnings, a generic disconnect instead of
"access expired" — and an ungranted keyboard is silently inert rather than never captured in the
first place. If a guest reports "my keyboard does nothing", check their access level in the
console first, then whether their client is current.
"access expired" — and an ungranted keyboard is silently inert rather than never captured. If a
guest reports "my keyboard does nothing", check their access level in the console first, then
whether their client is current.
Up-to-date native clients do get the chrome: they stop capturing what can't land (no keyboard grab
Up-to-date native clients get the chrome: they stop capturing what can't land (no keyboard grab
without the Keyboard grant), hide the clipboard and mic controls when ungranted, show a small
overlay chip naming the session's access and time remaining, and surface the expiry warnings as
toasts.
## Where enforcement happens
For the security-minded: the host checks every input event against the device's grants before
injecting it, refuses ungranted planes at session setup (no Gamepad grant means the virtual pads
are never created; no Microphone grant means the mic plane never attaches), and re-pairing a
device **preserves** its existing access — the only way to widen a grant is the console's own
dialogs, behind the console login. Dropped traffic is logged once per session and category, not
per event, so a misbehaving client can't flood the log. See [Security & Safe
Use](/docs/security) for the wider picture.
The host checks every input event against the device's grants before injecting it, refuses
ungranted planes at session setup (no Gamepad grant means the virtual pads are never created; no
Microphone grant means the mic plane never attaches), and re-pairing a device **preserves** its
existing access — the only way to widen a grant is the console's own dialogs, behind the console
login. Dropped traffic is logged once per session and category, not per event, so a misbehaving
client can't flood the log. See [Security & Safe Use](/docs/security) for the wider picture.
-214
View File
@@ -1,214 +0,0 @@
---
title: Arch Linux
description: Install a Punktfunk host on Arch (and Arch-derived distros) from the signed pacman binary repo.
---
Set up a Punktfunk host on **Arch Linux** (or an Arch-derived distro like CachyOS/EndeavourOS). The
host installs from a **signed pacman binary repo**, so it updates with `pacman -Syu` like the rest
of your system — no building required. Host encode is **NVENC on NVIDIA**; on **AMD/Intel** HEVC
and AV1 go through **Vulkan Video**, with **VAAPI** for H.264 and as the fallback
(`PUNKTFUNK_ENCODER=auto` picks per GPU).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
> Prefer to build it yourself? A split `PKGBUILD` (host + client + optional web console) is in the
> repo at `packaging/arch/` — see the [appendix](#appendix--build-from-source-pkgbuild). The binary
> repo below is the supported path.
## 1. GPU prerequisites
- **NVIDIA:** `sudo pacman -S --needed nvidia-utils` (provides NVENC + the EGL/CUDA zero-copy path).
Arch's stock `ffmpeg` already has NVENC built in — no RPM-Fusion-style swap like Fedora needs.
- **AMD / Intel:** the Mesa stack. HEVC/AV1 encode goes through **Vulkan Video** by default, so
install the Vulkan driver — `vulkan-radeon` (AMD) or `vulkan-intel` (Intel) — alongside the VAAPI
drivers (`libva-mesa-driver` for AMD, `intel-media-driver` for Intel), which carry H.264 and the
fallback path. Both are usually already installed on a desktop.
## 2. Add the signed repo
The registry **signs its database and every package**, so first trust its key once (after this,
packages install signature-verified):
```sh
# Trust the registry signing key.
curl -fsS https://git.unom.io/api/packages/unom/arch/repository.key \
| sudo pacman-key --add -
sudo pacman-key --lsign-key E0CA04465C99C936E0B0C6510A317015A34DDD69
# Add the repo (append to /etc/pacman.conf). No SigLevel line needed — pacman's default
# verifies signed packages against the key you just trusted. (printf, not a heredoc, so this
# works in fish too — CachyOS's default shell has no `<<EOF` support.)
printf '\n[punktfunk]\nServer = https://git.unom.io/api/packages/unom/arch/$repo/$arch\n' \
| sudo tee -a /etc/pacman.conf >/dev/null
```
> **Stable vs canary.** `[punktfunk]` is the **stable** channel — it moves only when a `vX.Y.Z`
> release is cut. For the latest `main` build, use `[punktfunk-canary]` instead (same `Server` line,
> just the repo name). Enable exactly one. See [Release Channels](/docs/channels).
## 3. Install the host
```sh
sudo pacman -Syu punktfunk-host # the streaming host
sudo pacman -Syu punktfunk-web # optional: the browser management console (pairing + status)
sudo pacman -Syu punktfunk-gamescope # optional: HDR (10-bit BT.2020 PQ) off gamescope sessions
sudo pacman -Syu punktfunk-scripting # optional: the plugin/script runner (see below)
sudo usermod -aG input "$USER" # /dev/uinput access for virtual gamepads (re-login to apply)
```
Also join `punktfunk` if **either** applies — you want the **virtual Steam Deck controller**
(paddles, trackpads, gyro — it reaches games as a real USB pad, which is why Steam Input adopts
it), or this box autologins into Steam **Gaming Mode** and you want the host to take that session
over at the client's resolution:
```sh
sudo usermod -aG punktfunk "$USER" # usbip/vhci + display-manager takeover (re-login to apply)
```
That is a second group on purpose. It grants write access to the usbip `attach` file, which
materialises an arbitrary emulated USB device — so it stays off the `input` group everyone is
routinely told to join. Join it only on a machine you trust. On a plain desktop host, everything
else still works without it and the pad simply arrives as an ordinary Xbox 360 controller; on a
Gaming Mode box the takeover silently degrades to mirroring the box's own screen — see
[gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
Each install is a **full** `-Syu`, on purpose: our packages are built against current Arch
sonames, and `pacman -Sy <pkg>` would drop one onto a system whose other packages are still old —
the classic partial upgrade that breaks Arch boxes. To take several in one go, name them on a
single line: `sudo pacman -Syu punktfunk-host punktfunk-web punktfunk-gamescope`.
`punktfunk-scripting` is the runner behind [Plugins](/docs/plugins); it isn't started for you —
`systemctl --user enable --now punktfunk-scripting` when you want it. `punktfunk-client` (the native
GTK4 Linux client) is in the same repo if this box is also a client. The host package ships the
systemd **user** units, the udev rule, the UDP socket-buffer sysctl tuning, and example configs.
Updates later are a normal `sudo pacman -Syu`, then `systemctl --user restart punktfunk-host` so the
running host picks up the new binary. A `-Syu` moves every Punktfunk package you installed, so
restart `punktfunk-web` the same way if you run the console. The web console can run the update for
you — see [Updating the Host](/docs/updating); on Arch that button additionally needs
`PACMAN_FULL_SYSUPGRADE=1` in `/etc/punktfunk/update.conf`, because the only pacman update we will
run is a full one.
## 4. Configure and run
The host runs as a systemd **`--user`** service — it needs your session's PipeWire and D-Bus. Copy a
starting config:
```sh
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env
```
How the host creates its virtual display and injects input depends on your desktop, not your distro —
edit `host.env` for the desktop you run, following its page for the exact settings and any quirks:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Hyprland](/docs/hyprland)
- [Sway / wlroots](/docs/sway)
Then enable the service and turn on linger so it starts at boot without a login:
```sh
systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host
sudo loginctl enable-linger "$USER"
```
Check it came up:
```sh
systemctl --user status punktfunk-host # active
journalctl --user -u punktfunk-host -f # watch a client connect
```
Enable the browser console, find your login password, and arm PIN pairing from
[The Web Console](/docs/web-console). For a headless KWin appliance that streams at boot with no
graphical login, see [KDE → Headless session](/docs/kde#headless-session). Full reference:
[Configuration](/docs/configuration) · [Running as a Service](/docs/running-as-a-service).
## 5. Open the firewall (if you have one)
**Stock Arch ships no firewall** — every port is already open, so you can skip this. But **CachyOS
enables `ufw` by default** (firewalld is not installed), and some other spins (e.g. EndeavourOS)
enable **`firewalld`** — an Arch package never opens ports for you, so on those the host is
unreachable until you allow it.
The `punktfunk-host` package installs openers for **both**, so it's a one-liner whichever you run.
The unit you enabled in step 4 runs `serve --gamestream` — the package installs it as it ships and
only rewrites the binary path — so that host serves **both** the native `punktfunk/1` plane and
stock [Moonlight](/docs/moonlight) clients, and needs **both** openers:
```sh
# ufw — CachyOS (and Ubuntu, once you enable ufw):
sudo ufw allow punktfunk-native
# firewalld — Fedora-like spins (EndeavourOS, …):
sudo firewall-cmd --reload # load the installed definitions
sudo firewall-cmd --permanent --add-service=punktfunk-native
sudo firewall-cmd --reload
```
Enabled **GameStream/Moonlight compat** (`PUNKTFUNK_GAMESTREAM=1` in `host.env` — see
[What the unit starts](/docs/running-as-a-service#what-the-unit-starts)), or you pass
`--gamestream` by hand? Then also open its service:
```sh
sudo ufw allow punktfunk-gamestream # ufw
sudo firewall-cmd --permanent --add-service=punktfunk-gamestream && sudo firewall-cmd --reload
```
`punktfunk-native` opens the QUIC control port (UDP 9777), mDNS discovery and the mgmt/library API
(TCP 47990); `punktfunk-gamestream` opens the fixed Moonlight ports — TCP 47984, 47989 and 48010,
UDP 4799848000 — plus the same mDNS.
The media **data plane** uses an *ephemeral* UDP port that the client opens with a hole-punch — the
host streams back out through the path the client opened, so there's **nothing fixed to open** as
long as the firewall allows outbound UDP (the default for both ufw and firewalld).
Enabled the **web console** (`punktfunk-web`, above) and want to reach it from your phone or another
machine? It's not opened by the streaming rules — open its port too, the same one-liner way:
```sh
sudo ufw allow punktfunk-web # ufw
sudo firewall-cmd --permanent --add-service=punktfunk-web && sudo firewall-cmd --reload # firewalld
```
That opens **TCP 47992** (HTTPS, login-gated). The mgmt API (47990) is opened for paired clients by the
`punktfunk-native` profile (game-library browsing over mTLS); off-loopback it serves only read-only
status/library, and every admin action stays loopback-only. Full port lists (`nftables`, explicit ports) are in
[`packaging/arch/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md#firewall).
## 6. Connect a client
From any [client](/docs/clients), `--discover` finds the host on the LAN. On first connect, complete
the **PIN pairing**: arm it from [The Web Console](/docs/web-console#arm-pairing), which displays a
4-digit PIN to type into the client. (Pairing is required by default; pass `serve --open` only if
you deliberately want to disable it.) See [Clients](/docs/clients) for per-platform setup.
## Next steps
- **Keep it current** — [Updating the Host](/docs/updating).
- **Remove it again** — [Uninstalling](/docs/uninstall).
- **Something not working?** — [Troubleshooting](/docs/troubleshooting).
## Appendix — build from source (PKGBUILD)
To build instead of using the binary repo, use the split `PKGBUILD` in `packaging/arch/` (produces
`punktfunk-host` + `punktfunk-client`; set `PF_WITH_WEB=1` to also build `punktfunk-web` and
`PF_WITH_SCRIPTING=1` to also build `punktfunk-scripting` — both need `bun`):
```sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk/packaging/arch
# Build the working tree (no git fetch):
PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver
sudo pacman -U punktfunk-host-*.pkg.tar.zst
```
NVENC/EGL come from the NVIDIA driver (`nvidia-utils`); on a GPU-less builder, symlink the CUDA
stub into the link path first (the `PKGBUILD` header documents this). Full details, the
Fedora→Arch dependency map, and the systemd-sysext mechanism are in
[`packaging/arch/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md).
(For a **SteamOS host**, use the [on-device installer](/docs/steamos-host) instead — it builds
the host and the HDR gamescope against the running OS.)
+81
View File
@@ -0,0 +1,81 @@
---
title: Arch Linux
description: Install the Punktfunk host on Arch, CachyOS or EndeavourOS from the signed pacman repo — four steps.
---
For **Arch Linux** and Arch-based distros (CachyOS, EndeavourOS, …). The host comes from a signed
binary repo and updates with `pacman -Syu` like everything else. SteamOS is different — it has
[its own page](/docs/steamos-host).
## 1. GPU driver
- **NVIDIA:** `sudo pacman -S --needed nvidia-utils` (NVENC and the zero-copy path; Arch's `ffmpeg`
already has NVENC built in).
- **AMD / Intel:** the Mesa stack you already have — `vulkan-radeon` / `vulkan-intel` for Vulkan
Video, `libva-mesa-driver` / `intel-media-driver` for VAAPI. Usually all installed on a desktop.
## 2. Install the host
Trust the repo key once, add the repo, install. Every install and update here is a **full**
`-Syu` on purpose — our packages are built against current Arch sonames, and `pacman -Sy <pkg>` is
the partial upgrade that breaks Arch boxes:
<Install platform="arch" />
The browser console is **optional** on Arch, so name it yourself — same line, full upgrade:
`sudo pacman -Syu punktfunk-web`. (Also in the repo: `punktfunk-gamescope` for HDR off gamescope,
`punktfunk-scripting` for [plugins](/docs/plugins), `punktfunk-client` if this box is also a client.)
From then on a normal `sudo pacman -Syu` moves every Punktfunk package; restart the host afterwards
(`systemctl --user restart punktfunk-host`) — or let the [console do it](/docs/updating).
## 3. Let it use your controllers
Join the `input` group (virtual gamepads go through `/dev/uinput`), then **log out and back in**:
```sh
sudo usermod -aG input "$USER"
```
Want the **virtual Steam Deck controller** (paddles, trackpads, gyro), or does this PC boot into Steam
**Gaming Mode**? Also join the `punktfunk` group —
[what it gates and why it's separate](/docs/gamescope#nobara-and-other-autologin-display-managers).
## 4. Start it
From a terminal inside your desktop session:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
systemctl --user enable --now punktfunk-scripting # if you installed the plugin runner — other distros start it for you, Arch doesn't
```
**Firewall:** stock Arch has none. **CachyOS enables `ufw`**, EndeavourOS enables **firewalld** —
on those the host is unreachable until you allow it (the package installed the profiles):
```sh
sudo ufw allow punktfunk-native && sudo ufw allow punktfunk-web # CachyOS (ufw)
sudo firewall-cmd --reload && sudo firewall-cmd --permanent --add-service=punktfunk-native --add-service=punktfunk-web && sudo firewall-cmd --reload # firewalld
```
[Ports & firewall](/docs/ports) has every port, and the GameStream profile if you turn Moonlight
compat on.
**That's the install.** Continue with the [Quick Start from step 3](/docs/quickstart#3-open-the-web-console)
— open the console, pair a client, stream.
## When you want more
- **pacman says `database already registered`?** The repo got added twice —
[the one-line fix](/docs/troubleshooting#pacman-error-could-not-register-punktfunk-database-database-already-registered).
**`unable to satisfy dependency 'libavcodec.so=…'`?** FFmpeg major mismatch —
[what to do](/docs/troubleshooting#pacman-unable-to-satisfy-dependency-libavcodecso).
- `punktfunk-host detect-conflicts` tells you if Sunshine or Apollo is also running;
[Troubleshooting](/docs/troubleshooting) starts from the symptom.
- Your desktop's particulars — [KDE](/docs/kde), [GNOME](/docs/gnome), [gamescope](/docs/gamescope),
[Hyprland](/docs/hyprland), [Sway](/docs/sway).
- Stream with nobody logged in — [Running as a service](/docs/running-as-a-service) (`sudo loginctl
enable-linger "$USER"` is the one extra line).
- Track `main` instead of releases (`[punktfunk-canary]`, same `Server` line — enable exactly one) —
[Release channels](/docs/channels). Build it yourself with the split `PKGBUILD` —
[Build from source](/docs/build-from-source#arch-pkgbuild).
+43 -49
View File
@@ -8,15 +8,15 @@ disconnects, a stream starts or stops, a pairing request arrives, a virtual disp
the library changes, the host starts or shuts down. Two ways to consume them:
- **Hooks** — zero-code: entries in `~/.config/punktfunk/hooks.json` run a **command** or POST a
**webhook** when a matching event fires. This covers the common automation: Do-Not-Disturb
during a stream, a phone notification on a pairing request, pausing downloads while playing.
**webhook** when a matching event fires. Covers the common automation: Do-Not-Disturb during a
stream, a phone notification on a pairing request, pausing downloads while playing.
- **The event stream** — code: `GET /api/v1/events` on the management API is a standard
[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
stream of the same events, for scripts and integrations that want to *decide* things (e.g.
auto-approve pairing from a known subnet by calling the approve endpoint).
Hooks **observe** — they can never veto or delay a connection, a stream, or a pairing decision,
and nothing you configure here runs anywhere near the streaming path.
and nothing configured here runs anywhere near the streaming path.
## The events
@@ -38,7 +38,7 @@ and nothing you configure here runs anywhere near the streaming path.
| `host.started` / `host.stopping` | the serve planes come up / wind down | version, whether GameStream is enabled |
Every event is a small JSON document with a monotonic `seq`, a `ts_ms` timestamp, a `schema`
version (additive-only — fields get added, never renamed), and the fields above. Example:
version (additive-only — fields get added, never renamed), and the fields above:
```json
{ "seq": 42, "ts_ms": 1784227449526, "schema": 1,
@@ -81,13 +81,12 @@ Each entry:
### What the host refuses
The document is validated as a whole, and **one bad entry disables every hook** — the host logs
`hooks.json invalid — hooks disabled until fixed` and runs none of them until you correct it. The
rules:
`hooks.json invalid — hooks disabled until fixed` and runs none until you correct it. The rules:
- An entry needs a non-empty `on`, plus `run` and/or `webhook`.
- `webhook` must be an `http(s)://` URL, and must **not** point at loopback, `localhost` or a
link-local address (which is also what blocks the cloud metadata endpoint). A receiver on this
same machine is what a `run` command is for. Ordinary LAN addresses — `192.168.x.x`, a ULA, a
link-local address (which also blocks the cloud metadata endpoint). A receiver on this same
machine is what a `run` command is for. Ordinary LAN addresses — `192.168.x.x`, a ULA, a
hostname — are fine, so Home Assistant on another box on your network works as written.
- `timeout_s` must be 1600.
- If `hmac_secret_file` is set but unreadable, the host **skips** that POST rather than sending it
@@ -107,10 +106,10 @@ A `run` command's shell one-liner vocabulary — the event flattened to env, val
[ "$PF_EVENT_KIND" = stream.started ] && makoctl mode -a do-not-disturb
```
Richer payloads (and the full document) are on stdin `jq` away. On a Windows host running as
the service, the command runs **in your interactive session** (never as SYSTEM); that path can't
carry per-process env or stdin, so the event JSON's path is appended as the command's last
argument instead.
Richer payloads (and the full document) are on stdin for `jq`. On a Windows host running as the
service, the command runs **in your interactive session** (never as SYSTEM); that path can't carry
per-process env or stdin, so the event JSON's path is appended as the command's last argument
instead.
Verify a signed webhook (Python):
@@ -120,15 +119,15 @@ expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(request.headers["X-Punktfunk-Signature"], expected)
```
**Rules of the road:** hooks are fire-and-forget and bounded — at most 8 in flight (extra
firings are dropped with a log line, never queued), and a command that outlives its timeout is
killed. Hook commands run as the host user, so `hooks.json` is operator-privileged config. On
Linux, when a command starts with an **absolute path** to a script, the host checks that file is
owned by you (or root) and not group/world-writable, and refuses to run it — loudly, in the log —
if it isn't. Write the full path (`/home/me/.config/punktfunk/scripts/on-stream.sh`, not `~/…`) if
you want that check: the shell expands `~` and looks up PATH names like `makoctl` only afterwards,
so those are never checked. On Windows there is no per-script check — the ACL on the config
directory is the boundary.
**Rules of the road:** hooks are fire-and-forget and bounded — at most 8 in flight (extra firings
are dropped with a log line, never queued), and a command that outlives its timeout is killed.
Hook commands run as the host user, so `hooks.json` is operator-privileged config. On Linux, when a
command starts with an **absolute path** to a script, the host checks that file is owned by you (or
root) and not group/world-writable, and refuses to run it — loudly, in the log — if it isn't. Write
the full path (`/home/me/.config/punktfunk/scripts/on-stream.sh`, not `~/…`) if you want that
check: the shell expands `~` and looks up PATH names like `makoctl` only afterwards, so those are
never checked. On Windows there is no per-script check — the ACL on the config directory is the
boundary.
The two simplest cases also exist as plain [host.env](/docs/configuration) settings, no
`hooks.json` needed: `PUNKTFUNK_ON_CONNECT_CMD` and `PUNKTFUNK_ON_DISCONNECT_CMD`.
@@ -137,9 +136,8 @@ The two simplest cases also exist as plain [host.env](/docs/configuration) setti
For per-title setup (HDR toggle, MangoHud, a VRR tweak), attach `prep` steps to a GameStream
`apps.json` entry or to a [custom library entry](/docs/game-library#adding-a-game-by-hand) — each
`do` runs **before** the title launches
(synchronously — the launch waits), each `undo` runs at session end in **reverse order**,
best-effort, even if the session crashed:
`do` runs **before** the title launches (synchronously — the launch waits), each `undo` runs at
session end in **reverse order**, best-effort, even if the session crashed:
```json
{ "id": 2, "title": "Steam", "compositor": "gamescope", "cmd": "steam -gamepadui",
@@ -153,11 +151,9 @@ A `do` that fails logs, keeps going, and its own `undo` is skipped (it never too
## Reacting to a game, not a stream
`stream.stopped` tells you the *stream* ended; `game.exited` tells you the *game* did. They are
often the same moment, but not always — a desktop stream has no game at all, and a stream can
outlive its game if you turned off "end the session when the game exits".
If you have been polling the host to work out when a game finished, you don't need to any more:
`stream.stopped` tells you the *stream* ended; `game.exited` tells you the *game* did. Often the
same moment, but not always — a desktop stream has no game at all, and a stream can outlive its
game if you turned off "end the session when the game exits". No polling needed:
```json
{ "hooks": [
@@ -170,8 +166,8 @@ Both carry the title in `PF_EVENT_GAME_TITLE` / `PF_EVENT_GAME_APP`, and `game.e
`PF_EVENT_REASON` so a script can tell "the player quit" (`exited`) from "the host closed it"
(`terminated`) — worth checking before you, say, power the TV off.
Ending the session yourself when a game exits needs no script at all: it is the default behavior,
on the console's **Virtual displays** page under
Ending the session when a game exits needs no script: it is the default, on the console's
**Virtual displays** page under
[When a game or a session ends](/docs/virtual-displays#when-a-game-ends-and-when-a-session-does).
## The event stream (`GET /api/v1/events`)
@@ -227,22 +223,21 @@ The canonical "decide, don't just observe" pattern — approve pairing from your
## Recipe: full controller passthrough (VirtualHere)
To get a controller's *native* features on the host — DualSense gyro, touchpad, adaptive
triggers, USB rumble — or to use a device no emulation can stand in for, like a racing wheel or a
HOTAS, hand the physical device from the couch to the host over
triggers, USB rumble — or to use a device no emulation can stand in for (a racing wheel, a HOTAS),
hand the physical device from the couch to the host over
[VirtualHere](https://www.virtualhere.com/) (USB-over-IP) while you play.
**Use the plugin.** [VirtualHere passthrough](/docs/plugins#virtualhere-usb-passthrough) does all of
this for you: it finds the device by name (so it survives the couch rebooting), brackets it around
the session, gives it back if anything crashes, and tells you which half of the setup is broken when
it isn't working. That is the supported route, and the rest of this section is only for people who
would rather not install a plugin.
**Use the plugin.** [VirtualHere passthrough](/docs/plugins#virtualhere-usb-passthrough) finds the
device by name (so it survives the couch rebooting), brackets it around the session, gives it back
if anything crashes, and tells you which half of the setup is broken. That is the supported route;
the rest of this section is for people who would rather not install a plugin.
**Turn off controller forwarding on the couch.** Whatever route you take below, the client that
hands the device over should stop *also* forwarding it: Settings → **Forward controllers**, off
([Client settings](/docs/client-settings#input)). Otherwise the host ends up with two controllers
for one pair of hands and games read both. On Linux and Windows it matters twice over — while the
client has the pad open it has *claimed* the device node, and VirtualHere cannot bind a device
somebody else is holding.
**Turn off controller forwarding on the couch.** Whatever route you take, the client that hands the
device over should stop *also* forwarding it: Settings → **Forward controllers**, off
([Client settings](/docs/client-settings#input)). Otherwise the host gets two controllers for one
pair of hands and games read both. On Linux and Windows it matters twice over — while the client
has the pad open it has *claimed* the device node, and VirtualHere cannot bind a device somebody
else is holding.
**The two sides.** VirtualHere is a server/client pair, and you run both: the **server on the couch**
(where the device is plugged in) shares it, and the **client on the host** mounts it. The client's
@@ -265,12 +260,11 @@ Bracket it on the stream with two [hooks](#hooks-hooksjson):
`couch-deck.11` is the device's address from `vhclientx86_64 -t LIST`.
Know what this trades away, because the plugin exists to fix exactly these: the address is
hard-coded, so it breaks when the couch reboots or the device moves port; and if the stream ends
abnormally the `stream.stopped` hook never fires, leaving the device stranded on the host until
somebody notices. There is also a
The trade-offs the plugin exists to fix: the address is hard-coded, so it breaks when the couch
reboots or the device moves port; and if the stream ends abnormally the `stream.stopped` hook never
fires, leaving the device stranded on the host until somebody notices. There is also a
[`virtualhere-dualsense.ts`](https://git.unom.io/unom/punktfunk/src/branch/main/sdk/examples/virtualhere-dualsense.ts)
SDK example if you want a worked script to build your own on.
SDK example to build your own script on.
> VirtualHere is a commercial product, sold separately by VirtualHere Pty. Ltd. — free for one
> shared device, licensed beyond that. Punktfunk is not affiliated with it.
-252
View File
@@ -1,252 +0,0 @@
---
title: Bazzite
description: Set up a Punktfunk host on Bazzite — it follows the box between Steam Gaming Mode (gamescope) and the KDE Plasma desktop automatically.
---
[Bazzite](https://bazzite.gg/) already ships everything a Punktfunk host needs — the NVIDIA driver,
NVENC, PipeWire, **gamescope**, and the **KDE Plasma desktop**. So a Bazzite host is the most
"appliance-like" setup, and it streams **both** of Bazzite's faces:
- **Steam Gaming Mode** (gamescope) — the couch/handheld game UI.
- **The KDE Plasma desktop** — the full desktop you get from "Switch to Desktop".
The host **auto-detects which one is live and follows the box across the switch** — including
mid-stream. You flip between Gaming Mode and Desktop with Bazzite's normal Steam UI /
"Switch to Desktop"; the host just re-targets whatever's running and keeps streaming. Nothing in
`host.env` forces a mode.
> Ideal for a dedicated game-streaming box that you also occasionally want as a remote desktop. For a
> pure desktop machine, install on [Ubuntu](/docs/ubuntu) or [Fedora](/docs/fedora) and configure the
> [KDE](/docs/kde) or [GNOME](/docs/gnome) desktop directly — simpler.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## Install
The host installs as a **systemd system extension (sysext)** — no `rpm-ostree` layering. The
Bazzite docs treat layering as a last resort (layered packages slow every OS update and can block
upgrades until removed); a sysext never enters an rpm-ostree transaction: it overlays `/usr`
read-only from `/var/lib/extensions/`, survives OS updates, installs and updates **without a
reboot**, and is removable in one command. This is the same mechanism the Fedora Atomic
maintainers ship via the [fedora-sysexts](https://fedora-sysexts.github.io/) project.
```sh
# One-time bootstrap (afterwards the updater is on PATH as `punktfunk-sysext`):
curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
sudo bash punktfunk-sysext.sh install # add `--channel canary` for rolling builds
```
That downloads the newest image — host + tray + web console + the plugin runner
(`punktfunk-scripting`), plus the HDR `punktfunk-gamescope` build — merges it, and applies the
udev/sysctl setup on the spot; the host is usable immediately, no reboot. The feed's checksum
manifest is OpenPGP-signed by packages@unom.io (key `AF245C506F4E4763`, the same one that signs our
RPMs), and `punktfunk-sysext` checks that signature against a key baked into the script before it
trusts a single checksum — so it needs `gpg` on the box, and it refuses a feed it can't verify.
The plugin runner rides along in the image and is **started for you** — the image bakes in its
`default.target.wants` symlink, because the game-library scanners ship as
[plugins](/docs/plugins). To turn it off: `systemctl --user mask punktfunk-scripting` (`mask`, not
`disable` — a plain disable cannot remove a symlink that lives in `/usr`).
From then on:
```sh
sudo punktfunk-sysext update # fetch + merge the newest build
sudo punktfunk-sysext status # channel, installed vs latest version
sudo punktfunk-sysext remove # unmerge and delete the image (~/.config/punktfunk is kept)
```
After an update, restart the host so it runs the new binary (the updater prints this reminder too).
The image carries the console as well, so restart that first if you enabled it:
```sh
systemctl --user restart punktfunk-web # only if you run the console
systemctl --user restart punktfunk-host
```
To **switch channel** later, re-run the install: `sudo punktfunk-sysext install --channel canary`
(or `--channel stable`). `update` takes no channel flag — it follows whatever the last install wrote
to `/etc/punktfunk-sysext.conf`. To be able to **go back** to a build that worked, keep a copy of the
image before you update, and re-install that file afterwards:
```sh
sudo cp /var/lib/extensions/punktfunk.raw ~/punktfunk-known-good.raw # before updating
sudo punktfunk-sysext install --from-file ~/punktfunk-known-good.raw # to go back to it
```
The web console can also run the update for you — see [Updating the Host](/docs/updating), which
needs the one-time `sudo usermod -aG punktfunk-update $USER`.
`remove` deletes the image and the `/etc` files it seeded (the tray autostart entry, and the
gamescope session drop-in unless you've edited it), but three things it created outside `/usr` stay
behind. To clear those too — services first, because once the image unmerges their binaries are
gone and the units just keep failing:
```sh
systemctl --user disable --now punktfunk-host punktfunk-web
sudo punktfunk-sysext remove
sudo rm -f /etc/modules-load.d/punktfunk.conf /etc/udev/rules.d/60-punktfunk.rules
sudo groupdel punktfunk-update # the (empty) group for web-console updates
```
[Uninstalling](/docs/uninstall) has the same walkthrough for the other install methods, and for the
clients.
Three things to know:
- **After a Bazzite major rebase** (Fedora 43 → 44) the old image **refuses to load** rather than
run against mismatched system libraries — run `sudo punktfunk-sysext update` once and it fetches
the image built for the new base.
- **Already layering Punktfunk?** Install the sysext (it shadows the layered copy immediately),
then drop the layer so it stops slowing your updates:
`sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot`.
- **If it refuses the feed.** `refusing to install from an unsigned feed` means that Fedora major's
feed predates signing; it gets sealed on the next publish. To install from it anyway, accepting
that the images are unauthenticated, run
`sudo env PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1 bash punktfunk-sysext.sh install`. The other message,
`the feed's SHA256SUMS is NOT signed by packages@unom.io`, is not the same thing — don't install;
re-download the script and try again.
For a fully baked appliance image there's also a **bootc** Containerfile that installs the RPMs
from the registry at image-build time — see `packaging/bootc/` in the repo. Plain `rpm-ostree`
layering from the [RPM registry](https://git.unom.io/unom/-/packages) keeps working too: add the
repo exactly as on [Fedora](/docs/fedora), with the `baseurl` group matching your Fedora base, then
`sudo rpm-ostree install punktfunk punktfunk-web` and reboot. The sysext is still the supported
default. Building from source also works (Bazzite is Fedora Atomic underneath — same steps as
[Fedora](/docs/fedora)).
## Allow controller input
Gamepad and DualSense input needs your user in the `input` group. On Bazzite, don't use
`usermod` — the base is immutable and the group is managed by a recipe. Use:
```sh
ujust add-user-to-input-group
```
Then **log out and back in**. (A controller that's "detected but does nothing" is almost always this
permission, not a client problem.)
Then join `punktfunk``usermod` is fine here, because unlike `input` this group is ours and the
sysext creates it on merge:
```sh
sudo usermod -aG punktfunk "$USER" # then log out and back in
```
This box **is** a Gaming Mode box, so that group is not optional in practice: it authorizes the
helper the host uses to stop the display manager when it takes the Gaming Mode session over at your
client's resolution, and it gates the usbip `attach` file the **virtual Steam Deck controller**
(paddles, trackpads, gyro) attaches through. It is a separate group on purpose — writing that file
can materialise arbitrary emulated USB hardware, so it is not folded into the group everyone is
told to join for gamepads. Without it the pad arrives as an ordinary Xbox 360 controller, and the
takeover degrades to mirroring the box's own screen — see
[gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
## Configure
The RPM ships a Bazzite-tuned config you can copy as your starting point:
```sh
mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
```
The template is deliberately minimal — it does **not** force a compositor, because the host
auto-detects Gaming Mode (gamescope) vs Desktop (KWin) on every connect and follows the switch
mid-stream. No session anchors are needed either (a user service inherits the right runtime dir).
The only settings that matter (GPU zero-copy is on by default):
```sh
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy (dmabuf → CUDA → NVENC) is ON by default; auto-falls back to CPU. Set =0 to force CPU.
```
### Gaming Mode: attach vs managed
For Gaming Mode there are two models. The template forces **neither** — the host picks per connect,
and on Bazzite (which ships `gamescope-session-plus`) that is **managed**:
- **Managed** (what you get by default here) — the host takes the box's gamescope over and
relaunches it **headless** at the *client's* exact resolution and refresh — Game Mode on the
virtual screen — restoring the box on idle. This is the model that gives the client a display of
its **own**, and the only one under which a game launched from a client's library gets a
dedicated session. It needs the [`punktfunk` group](#allow-controller-input): the takeover stops
the display manager for the length of the stream, and without that grant it cannot.
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`) — the **box** owns its gamescope session on its own
display, and the host attaches to whatever's live without ever tearing it down (on a headless
box, a box-owned autologin session is restarted at the client's resolution on a mismatch; with a
display connected it streams at the box's own mode). Switching Desktop ↔ Game is rock-solid, and
the cost is that a box with a screen attached serves the client a **mirror** of that screen
rather than its own display. Setting it also outranks a dedicated game session.
`=0` turns the attach override off, the same as removing the line.
Full treatment: [Steam / gamescope → How the host gets a
gamescope](/docs/gamescope#how-the-host-gets-a-gamescope).
Mid-stream Gaming ↔ Desktop following (`PUNKTFUNK_SESSION_WATCH`) is **on by default** on
Bazzite/SteamOS. See [Configuration](/docs/configuration) for the full list of knobs.
### Streaming the KDE Plasma desktop
The **virtual output** (video) for the Desktop session needs no config — the host package ships an
`io.unom.Punktfunk.Host.desktop` file whose `X-KDE-Wayland-Interfaces` grants the host KWin's
restricted screencast protocol on a normal interactive Plasma session (background:
[KDE Plasma](/docs/kde)). After a **fresh host install, log out and back into the Desktop session
once** so KWin re-reads that grant.
The one thing a normal KDE login lacks is the RemoteDesktop grant for headless **input** injection.
Seed it once (as the streaming user, no root) so the host auto-approves instead of popping an
un-answerable dialog:
```sh
bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
```
Gaming Mode needs none of this — it auto-attaches.
## Run as an always-on host
Bazzite hosts are typically headless. Enable the host service and linger so it starts at boot — see
[Running as a Service](/docs/running-as-a-service). One host service covers both Gaming Mode and the
Desktop; it follows whichever the box is in.
```sh
systemctl --user enable --now punktfunk-host
systemctl --user enable --now punktfunk-web # web console: pairing + status
sudo loginctl enable-linger "$USER" # start at boot with nobody logged in
```
Without that last line the `--user` units don't start until someone logs in — which on a headless box
never happens.
Then open [The Web Console](/docs/web-console) for the login password and to
[arm pairing](/docs/web-console#arm-pairing).
## Good to know
These apply to the **Gaming Mode (gamescope)** path; the KDE Desktop path is unaffected:
- **gamescope 3.16.22 or newer is required; 3.16.23 or newer for the Steam overlay.** Below 3.16.22
headless capture can deadlock; between the two, capture works but the Steam overlay (Shift+Tab /
the Quick Access Menu) is never painted into the captured node. Bazzite's current gamescope is
past both; this only bites if you've pinned an old one.
- **Forcing attach costs you the cursor, HDR and your own display.** The sysext ships the
`punktfunk-gamescope` build, but it only reaches a session the host starts itself — under
`PUNKTFUNK_GAMESCOPE_ATTACH=1` the live session is Bazzite's own stock gamescope. The managed
default gets you the compositor-drawn pointer, real HDR and a display of the client's own. If you
deliberately stay on attach, also set `PUNKTFUNK_GAMESCOPE_HDR=0` and
`PUNKTFUNK_GAMESCOPE_BIN=/usr/bin/gamescope`. Why each half breaks:
[gamescope → Known limits](/docs/gamescope#known-limits) for the cursor,
[HDR → Linux + gamescope](/docs/hdr#linux--gamescope) for the failed connect.
⚠ Older templates set `PUNKTFUNK_GAMESCOPE_ATTACH=1` for you — if you copied one, delete that
line from `~/.config/punktfunk/host.env`, because an upgrade never rewrites a file you already
have.
Those are the two that bite on Bazzite. The full set — touch, mouse modes, the clipboard — is on
[gamescope → Known limits](/docs/gamescope#known-limits).
Then [connect a client](/docs/clients) — Moonlight works great for couch gaming, and the Apple app for
Apple TV / iPad. Trouble? See [Troubleshooting](/docs/troubleshooting).
+94
View File
@@ -0,0 +1,94 @@
---
title: Bazzite
description: Install the Punktfunk host on Bazzite (or any Fedora Atomic spin) as a sysext — no layering, no reboot — and stream both Gaming Mode and the desktop.
---
Bazzite already ships everything a host needs — the NVIDIA driver, NVENC, PipeWire, gamescope, KDE
Plasma — so this is the most appliance-like setup. One host streams **both** of Bazzite's faces,
Steam **Gaming Mode** and the **KDE desktop**, and follows the box when you switch, even mid-stream.
Nothing in the config picks a mode.
## 1. Install the host
The host installs as a **systemd system extension** — it overlays `/usr` from
`/var/lib/extensions/`, survives OS updates, and installs, updates and uninstalls **without a
reboot** (no `rpm-ostree` layering, which Bazzite's docs treat as a last resort). The feed is
signed; the installer refuses one it can't verify.
<Install platform="bazzite" />
That fetches the newest image — host, web console, tray, the plugin runner (started for you, because
the game-library scanners are [plugins](/docs/plugins)) and the HDR `punktfunk-gamescope` build — and
applies the udev/sysctl setup on the spot. From then on `sudo punktfunk-sysext update` (or the
[console's update button](/docs/updating)) moves you to the newest build; `status` shows where you
are. After an update restart the host: `systemctl --user restart punktfunk-web punktfunk-host`.
## 2. Let it use your controllers
On Bazzite the `input` group is managed by a recipe, so don't `usermod` it — use the helper. The
`punktfunk` group is ours (the sysext creates it), and on a Gaming Mode box it isn't optional: it
authorizes the host to take the Gaming Mode session over at your client's resolution, and gates the
virtual Steam Deck controller. Then **log out and back in**:
```sh
ujust add-user-to-input-group
sudo usermod -aG punktfunk "$USER"
```
([Why it's a separate group](/docs/gamescope#nobara-and-other-autologin-display-managers).)
## 3. Start it
Bazzite hosts are usually headless, so enable the services **and** linger, from a terminal in the
desktop session — without linger the `--user` units wait for a login that never comes:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
sudo loginctl enable-linger "$USER"
```
Two one-time steps for streaming the **KDE desktop** (Gaming Mode needs neither): log out and back
into the Desktop session once so KWin re-reads the screencast grant the package installed, and seed
the input grant so the host auto-approves instead of popping a dialog nobody can answer:
```sh
bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
```
## 4. Open the firewall
Bazzite runs **firewalld**, and a package never opens ports for you — until you allow it, no client
can reach the host. The image installed the service definitions; enable them once:
```sh
sudo firewall-cmd --reload # load the definitions the package installed
sudo firewall-cmd --permanent --add-service=punktfunk-native --add-service=punktfunk-web
sudo firewall-cmd --reload
```
(`punktfunk-web` is only needed to reach the console from another device; Moonlight compat needs
`punktfunk-gamestream` too — [Ports & firewall](/docs/ports).)
**That's the install.** Continue with the [Quick Start from step 3](/docs/quickstart#3-open-the-web-console)
— open the console, pair a client, stream. Moonlight works great for couch gaming here, the Apple
app for Apple TV and iPad.
## When you want more
- **Gaming Mode: what the client gets.** By default the host relaunches Gaming Mode *headless* at the
client's exact resolution (a display of its own, real HDR, the compositor's cursor) and restores
the box on idle. `PUNKTFUNK_GAMESCOPE_ATTACH=1` makes it attach to the box's own session instead —
rock-solid switching, but a box with a screen serves a *mirror* of it, and loses HDR and the cursor.
[How the host gets a gamescope](/docs/gamescope#how-the-host-gets-a-gamescope) has the whole
model; the Bazzite template (`/usr/share/punktfunk/host.env.bazzite`) forces neither.
- **Stream lags, then freezes, with a DualSense-type client pad** — an SELinux storm, with a shipped
fix: [Troubleshooting](/docs/troubleshooting#stream-lags-then-freezes-with-a-dualsense-pad-bazzite-selinux).
- **Channels, rollback, after a Bazzite major rebase** —
[Updating → Bazzite sysext](/docs/updating#bazzite-sysext-channels-rollback-and-rebases).
Removing it — [Uninstall](/docs/uninstall#bazzite--fedora-atomic-systemd-sysext).
- **Other ways in.** Plain `rpm-ostree` layering from the [RPM repo](/docs/fedora#2-install-the-host)
(use the `bazzite` group) still works, and `packaging/bootc/` in the repo bakes an appliance image;
the sysext is the supported default. Already layering? Install the sysext (it shadows the layer at
once), then `sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot`.
- Everything Gaming-Mode-specific — overlay, touch, HDR, the two gamescope version floors —
[Steam / gamescope](/docs/gamescope); the desktop — [KDE](/docs/kde).
@@ -0,0 +1,90 @@
---
title: Build from source
description: Compile the Linux host yourself — on Ubuntu/Debian, Fedora, or with the Arch PKGBUILD — when no package fits your release or you want to track main.
---
The package repos are the supported path ([Install the Host](/docs/install)). Build from source when
your release is older than a package supports (Ubuntu before 26.04, Debian 12, a Fedora without a
repo group), or to hack on it. A source build gets **no packaged units and no clean updates** — you
wire the service up by hand ([Running as a service](/docs/running-as-a-service) shows the unit).
Two build features matter on every distro: `punktfunk-host/nvenc` (direct NVENC on NVIDIA) and
`punktfunk-host/vulkan-encode` (Vulkan Video on AMD/Intel). They're what the packaged builds use;
without them the host falls back to the slower libav backends. Rust comes from
[rustup](https://rustup.rs) if you don't have it:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
## Ubuntu / Debian
The packaged host is built against **FFmpeg 8**. Ubuntu 26.04's and Debian 13's `libavcodec-dev`
are new enough; Ubuntu 24.04's is FFmpeg 6.1 — build FFmpeg 8 yourself first there (what
`ci/rust-ci-noble.Dockerfile` does), or stick with the packaged host.
```sh
sudo apt install build-essential pkg-config cmake clang libclang-dev nasm git curl \
pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev \
libwayland-dev wayland-protocols libxkbcommon-dev libopus-dev \
libdrm-dev libgbm-dev libgl-dev libegl-dev libgles-dev mesa-common-dev libva-dev \
ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \
libnvidia-egl-wayland1 libnvidia-egl-gbm1 libei-dev
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release --locked \
--features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host
```
## Fedora
```sh
sudo dnf install gcc gcc-c++ make cmake clang clang-devel nasm git pkgconf-pkg-config \
pipewire-devel wayland-devel wayland-protocols-devel libxkbcommon-devel opus-devel \
libdrm-devel mesa-libgbm-devel mesa-libGL-devel mesa-libEGL-devel mesa-libGLES-devel libva-devel \
ffmpeg-devel libei-devel
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release --locked \
--features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host
```
`ffmpeg-devel` must be RPM Fusion's (with NVENC), not `ffmpeg-free-devel`. `mesa-libGL-devel` isn't
optional — the zero-copy GPU path links `libGL`, and without it the build fails at link time with
`cannot find -lGL`. To build an RPM instead, use the same toolchain CI does:
`docker build --build-arg FEDORA_VERSION=NN -f ci/fedora-rpm.Dockerfile -t pf-rpm ci`, then run
`packaging/rpm/build-rpm.sh` inside it.
## Arch (PKGBUILD)
The split `PKGBUILD` in `packaging/arch/` produces `punktfunk-host` and `punktfunk-client`; set
`PF_WITH_WEB=1` to also build `punktfunk-web` and `PF_WITH_SCRIPTING=1` for `punktfunk-scripting`
(both need `bun`):
```sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk/packaging/arch
PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver # builds the working tree, no git fetch
sudo pacman -U punktfunk-host-*.pkg.tar.zst
```
NVENC/EGL come from `nvidia-utils`; on a GPU-less builder, symlink the CUDA stub into the link path
first (the `PKGBUILD` header documents this). Packager notes, the Fedora→Arch dependency map and the
sysext mechanism: [packaging/arch](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md).
For a **SteamOS** host don't use the PKGBUILD — the [on-device installer](/docs/steamos-host) builds
ABI-matched to the running OS.
## Running what you built
The binary lands at `target/release/punktfunk-host`. Run it from inside your desktop session — it
auto-detects the compositor:
```sh
target/release/punktfunk-host serve # secure native-only host
target/release/punktfunk-host serve --gamestream # + Moonlight compat (trusted LAN only)
```
To run it as a user service, copy `scripts/punktfunk-host.service` to
`~/.config/systemd/user/` (it already points at `%h/punktfunk/target/release/punktfunk-host`), then
`systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host`. The other
workspace members (`punktfunk-web`, `punktfunk-scripting`, the client) build the same way — the
root [README](https://git.unom.io/unom/punktfunk#build--test-from-source) covers the dev loop.
+81 -92
View File
@@ -6,21 +6,21 @@ description: Every setting a Punktfunk client stores — what it does, what it d
The host has [its own settings reference](/docs/configuration). This page is the other half: the
settings each **client** keeps, which together decide what a session looks like.
Most of them are a *request*. The client asks, the host answers, and the answer comes back in the
handshake — so a setting the host can't honor is usually a quiet downgrade rather than an error.
Most of them are a *request*. The client asks, the host answers in the handshake — so a setting the
host can't honor is usually a quiet downgrade rather than an error.
## Where the settings live
The Linux, Windows, Mac, iPhone/iPad and Android apps group settings the same way — **General**,
**Display**, **Input**, **Audio**, **Controllers** — under *Preferences* on Linux and *Settings*
elsewhere. The Apple TV app shows one scrolling list instead, and so does any client's settings
screen reached with a controller. A controller-driven launch (Steam Deck Gaming Mode) opens the
client's **console home**, whose settings screen is one steppable list of sections — **Stream**,
**Video**, **Presentation**, **Audio**, **Controller**, **Touchscreen**, **Interface**,
**Profiles**. On a Steam Deck that list *is* the settings surface: the
[Decky plugin](/docs/steam-deck) is a launcher and keeps no settings of its own, and its **Open
Punktfunk** button puts the console home one tap from the Quick Access Menu. The console home is
part of the client — it is not the host's [web console](/docs/web-console).
elsewhere. The Apple TV app shows one scrolling list instead, as does any client's settings screen
reached with a controller. A controller-driven launch (Steam Deck Gaming Mode) opens the client's
**console home**, whose settings screen is one steppable list of sections — **Stream**, **Video**,
**Presentation**, **Audio**, **Controller**, **Touchscreen**, **Interface**, **Profiles**. On a
Steam Deck that list *is* the settings surface: the [Decky plugin](/docs/steam-deck) is a launcher
and keeps no settings of its own, and its **Open Punktfunk** button puts the console home one tap
from the Quick Access Menu. The console home is part of the client — it is not the host's
[web console](/docs/web-console).
Linux stores them in `~/.config/punktfunk/client-gtk-settings.json`, the same file the console home
writes, so a change in either shows up in the other. Windows uses
@@ -30,8 +30,8 @@ Changes apply to the **next** session — a running stream keeps what it started
is the exception in effect, not in reading: it too is read at connect, but once a session is running
with it on, every window resize renegotiates the mode.)
Not every client offers every setting, and the wording on screen varies a little between them — the
names below are the ones the Linux app uses. The differences that matter are noted per setting.
Not every client offers every setting; the names below are the Linux app's, and differences that
matter are noted per setting.
## Video
@@ -73,8 +73,8 @@ link. The stops run 0.5× to 4×. The result is floored to an even size and capp
**Video codec***default: Automatic.* A soft preference: the host emits your choice when it can
also produce it, otherwise the best codec you both speak, in the order HEVC → AV1 → H.264.
**PyroWave** is never auto-picked — pick it explicitly on Linux, Windows, the console home, or
an Apple device whose decode probe passes; anywhere else it isn't offered, and asking for it lands on
**PyroWave** is never auto-picked — pick it explicitly on Linux, Windows, the console home, or an
Apple device whose decode probe passes; anywhere else it isn't offered, and asking for it lands on
that same order. See [PyroWave](/docs/pyrowave). The Android and Apple apps hide AV1 unless the
device has a hardware AV1 decoder; Android never offers PyroWave.
@@ -91,24 +91,23 @@ decode probe to pass). The console home offers the toggle; Android doesn't.
**Prioritize***default: Lowest latency.* What the client optimizes for when a decoded frame is
ready. **Lowest latency** shows every frame the moment the display can take it, so a network hiccup
becomes an occasional repeated or skipped frame. **Smoothness** holds a small buffer that evens
those hiccups out, at that buffer's worth of added delay. Linux and Windows apps and the console
home; the Apple and Android apps have carried the same setting for a while, and it is stored
under the same name, so a [profile](/docs/profiles-and-links) means the same thing on every device.
becomes an occasional repeated or skipped frame. **Smoothness** holds a small buffer that evens those
hiccups out, at that buffer's worth of added delay. Linux and Windows apps and the console home; the
Apple and Android apps have it too, stored under the same name, so a
[profile](/docs/profiles-and-links) means the same thing on every device.
**Smoothness buffer***default: Automatic (two frames).* How many frames are held back before
showing. Each frame absorbs roughly one screen refresh of network hiccup and costs one refresh of
delay — so on a 120 Hz screen, two frames is about 17 ms of extra delay bought against 17 ms of
jitter. If you never see stutter, you don't need this. The row appears wherever **Prioritize** is
offered, and only once you have picked **Smoothness** — under Lowest latency there are no held
frames for it to count, so it isn't shown at all.
offered, and only once you have picked **Smoothness**.
**V-Sync***default: on.* Tear-free presentation. Turning it off asks the GPU to show each frame
the instant it's ready instead of waiting for the screen's next refresh: the lowest delay a display
can give you, at the cost of visible tearing on fast motion. It is **best-effort** — not every
driver or compositor offers a tearing mode, and where none is available the stream stays tear-free.
The Detailed [stats overlay](/docs/stats) names the mode actually in use, so you can tell "off"
from "off but unavailable". Linux and Windows apps and the console home.
**V-Sync***default: on.* Tear-free presentation. Off asks the GPU to show each frame the instant
it's ready instead of waiting for the screen's next refresh: the lowest delay a display can give
you, at the cost of visible tearing on fast motion. It is **best-effort** — not every driver or
compositor offers a tearing mode, and where none is available the stream stays tear-free. The
Detailed [stats overlay](/docs/stats) names the mode actually in use, so you can tell "off" from
"off but unavailable". Linux and Windows apps and the console home.
**Follow variable refresh rate***default: on.* On a VRR / FreeSync / G-Sync screen, let the panel
refresh in step with the stream rather than on a fixed cadence — which removes the wait between a
@@ -132,21 +131,20 @@ claims a sink advertising exactly that many channels, so applications produce re
**Windows** host loopback-captures your current output endpoint and lets Windows convert it — so 5.1
from a stereo endpoint is an upmix, not new channels. Offered everywhere.
**Microphone** — *default: off on Linux, Windows, Android and the console home; on in the
Apple app.* Sends this device's microphone to the host's virtual mic. On Linux and Windows the
row is spelled *Stream microphone*, and **Ctrl+Alt+Shift+V** mutes it mid-stream without ending
anything — see [Muting your microphone](/docs/input#muting-your-microphone).
**Microphone** — *default: off on Linux, Windows, Android and the console home; on in the Apple
app.* Sends this device's microphone to the host's virtual mic. On Linux and Windows the row is
spelled *Stream microphone*, and **Ctrl+Alt+Shift+V** mutes it mid-stream without ending anything —
see [Muting your microphone](/docs/input#muting-your-microphone).
**Echo cancellation***default: on.* Stops the host's audio, playing out of this device's
speakers, from being picked up by the microphone and sent straight back. It hands the microphone
to the system's own canceller rather than doing the work itself: on **Linux** that means capturing
from an echo-cancelled PipeWire source when your desktop provides one, on **Windows** asking WASAPI
for the Communications stream category so the endpoint's processing engages, and on **Apple** and
**Echo cancellation***default: on.* Stops the host's audio, playing out of this device's speakers,
from being picked up by the microphone and sent straight back. It hands the microphone to the
system's own canceller rather than doing the work itself: on **Linux** that means capturing from an
echo-cancelled PipeWire source when your desktop provides one, on **Windows** asking WASAPI for the
Communications stream category so the endpoint's processing engages, and on **Apple** and
**Android** the platform's voice-processing mode. Turn it off if your microphone already runs its
own processing, or if the canceller makes your voice sound thin. The row sits under the microphone
toggle and greys out while the microphone is off. Offered by the Linux, Windows, Apple, Android and
console-home clients. What it can and can't fix is in
[Why do I hear myself](/docs/echo).
console-home clients. What it can and can't fix is in [Why do I hear myself](/docs/echo).
**Speaker** and **Microphone** device pickers — *default: System default.* Which endpoint stream
audio plays out of, and which input feeds the uplink. Only the Linux app (PipeWire nodes) and the
@@ -164,41 +162,39 @@ more settings are worth naming here.
**Forward controllers***default: on*, on every client. Off, the controllers connected to *this*
device are not sent to the host at all. That is what you want when your controller already reaches
the host by some other route — [USB passthrough](/docs/automation#recipe-full-controller-passthrough-virtualhere)
such as VirtualHere, or simply a pad plugged into the host itself. Leaving forwarding on in that
situation hands the host two controllers for one pair of hands, and games read both: a stick drifts
because the second pad is centred, or a menu takes every input twice.
the host by some other route —
[USB passthrough](/docs/automation#recipe-full-controller-passthrough-virtualhere) such as
VirtualHere, or a pad plugged into the host itself. Leaving forwarding on there hands the host two
controllers for one pair of hands, and games read both: a stick drifts because the second pad is
centred, or a menu takes every input twice.
On Linux and Windows it does more than stay quiet. Opening a controller is what *claims* it — the
client's SDL takes the device node — and a claimed device is one a passthrough tool cannot bind. So
with this off the session never opens the controller at all, which is precisely what leaves it free
for VirtualHere to hand over. The consequence to know: the
On Linux and Windows, opening a controller is what *claims* it — the client's SDL takes the device
node — and a passthrough tool cannot bind a claimed device; with this off the session never opens
the controller at all, leaving it free for VirtualHere to hand over. The consequence: the
[controller escape chord](/docs/input#leaving-with-a-controller) is read off forwarded pads, so it is
unavailable on those two while this is off — leave a stream with the keyboard chord or the client's
own UI. The Apple and Android apps claim nothing, so their chords keep working either way; the
Android app does stop its DualSense and Steam Controller 2 USB captures, which *do* claim the
device.
Android app does stop its DualSense and Steam Controller 2 USB captures, which *do* claim the device.
The rows below it — which pad, and what type — have nothing to act on while this is off, and every
client greys them out to say so.
**Gamepad type** (*Controller type* on Apple, Android and the console home) — *default: Automatic*,
which matches each physical controller. The pickers offer Xbox 360, Xbox One, DualSense and
DualShock 4 everywhere, plus Steam Deck on Linux, Android and the console home. Your client
declares a type per pad as it connects — Automatic declares what that controller really is, an
explicit choice declares your choice — and the host builds each virtual pad from that. A type the
host has no backend for degrades to an Xbox 360 pad rather than failing: Xbox One on a Windows host,
for instance, or any Sony pad on a Linux host that can't open `/dev/uhid`.
DualShock 4 everywhere, plus Steam Deck on Linux, Android and the console home. Your client declares
a type per pad as it connects — Automatic declares what that controller really is, an explicit
choice declares your choice — and the host builds each virtual pad from that. A type the host has no
backend for degrades to an Xbox 360 pad rather than failing: Xbox One on a Windows host, for
instance, or any Sony pad on a Linux host that can't open `/dev/uhid`.
That degrade is the one thing worth knowing about **motion**. An Xbox-class virtual pad has no
gyroscope in its HID contract, so a session that ends up on one throws every motion sample away —
your controller's gyro simply does nothing, which from the couch is indistinguishable from a broken
sensor. Automatic lands there for any controller punktfunk doesn't recognise as Sony or Valve (an
8BitDo with a gyro, say), and so does a Switch Pro streaming to a Windows host, which has no
Nintendo backend to build. **If you want motion, pick a DualSense-class type** — DualSense,
DualSense Edge, DualShock 4, Switch Pro or Steam Deck all carry a motion plane. The clients detect
this case and say so on-screen for a few seconds when it happens; the setting applies from the next
session, not the one you are in.
That degrade matters for **motion**. An Xbox-class virtual pad has no gyroscope in its HID
contract, so a session that ends up on one throws every motion sample away — your controller's gyro
does nothing. Automatic lands there for any controller punktfunk doesn't recognise as Sony or Valve (an 8BitDo
with a gyro, say), and so does a Switch Pro streaming to a Windows host, which has no Nintendo
backend to build. **If you want motion, pick a DualSense-class type** — DualSense, DualSense Edge,
DualShock 4, Switch Pro or Steam Deck all carry a motion plane. The clients detect this case and say
so on-screen for a few seconds when it happens; the setting applies from the next session, not the
one you are in.
On a **Steam Deck as the client**, motion also needs Steam Input switched off for punktfunk — with
it on, Steam hands the app its own virtual Xbox pad, which has no gyro to forward no matter which
@@ -229,9 +225,8 @@ Windows apps, *off* on Android. The two halves of [controller audio](/docs/contr
DualSense's voice-coil haptics, and the little speaker in the middle of the pad. Both need a
**wired** DualSense or DualSense Edge — over Bluetooth a controller exposes no audio device at all,
and both settings quietly do nothing. Neither costs anything without a host that sends them: the
plane is negotiated, and silence is never encoded or transmitted, so leaving haptics on is free even
on a pad that never gets any. Turn **Controller speaker** off if you would rather all game audio came
out of your speakers or headset.
plane is negotiated, and silence is never encoded or transmitted. Turn **Controller speaker** off if
you would rather all game audio came out of your speakers or headset.
Offered by the Linux, Windows and Android apps. On Linux, the client also switches the controller's
sound card to Pro Audio while it needs the voice coils, and puts it back afterwards — see
@@ -240,13 +235,13 @@ for why that is necessary and how to turn it off.
**Capture system shortcuts***default: on.* Offered by the Linux, Windows and macOS apps and the
console home; Windows spells the row out as *Capture system shortcuts (Alt+Tab, Win, …)*. On a Deck it
matters only for a keyboard you attached yourself, for the reason the paragraph below gives: Gaming
Mode is gamescope, which has nothing to hold back. On, Alt+Tab and the Windows key
(Super on Linux) reach the host while the stream has input captured. Off, they act on this machine
instead — what you want when the stream shares a screen with local work. Either way the chords come
back the moment you release capture with **Ctrl+Alt+Shift+Q**, the window loses focus, or the stream
ends, and [Desktop mouse mode](/docs/input#mouse-modes) never takes them at all. Leaving this on does
mean **Ctrl+Alt+Shift+Q is your way out** of a captured stream, since Alt+Tab no longer is.
matters only for a keyboard you attached yourself: Gaming Mode is gamescope, which has nothing to
hold back. On, Alt+Tab and the Windows key (Super on Linux)
reach the host while the stream has input captured. Off, they act on this machine instead — what you
want when the stream shares a screen with local work. Either way the chords come back the moment you
release capture with **Ctrl+Alt+Shift+Q**, the window loses focus, or the stream ends, and
[Desktop mouse mode](/docs/input#mouse-modes) never takes them at all. Leaving this on does mean
**Ctrl+Alt+Shift+Q is your way out** of a captured stream, since Alt+Tab no longer is.
On macOS the chords in question are the **⌘** ones — ⌘Q above all, which reaches the host as Super+Q,
one of the most-bound chords on a Linux desktop. On, ⌘Q, ⌘W, ⌘H and the rest go to the host instead
@@ -270,7 +265,7 @@ learned. Turn it off for hosts you reach over a VPN, where "offline" usually mea
broadcast" and the wake only adds a delay. The Linux, Windows, Apple and Android apps have this
toggle, as does the console home — and on a Steam Deck it governs the
[Decky plugin's](/docs/steam-deck) launches too, because the plugin starts every stream through the
client, which reads this setting like any other connect. The console home also offers wake as an
client. The console home also offers wake as an
explicit action on an offline host, whatever the toggle says. See
[Wake-on-LAN](/docs/wake-on-lan).
@@ -282,14 +277,13 @@ offered on any paired host. See [Game library](/docs/game-library).
**Start streams in fullscreen***default: on.* On Linux and Windows, F11 or Alt+Enter leaves
fullscreen live. On a Mac the setting is **Fullscreen while streaming**, and the window comes back
when you return to the host list. The console home carries the row for the desktop client that
shares the store — a Gaming-Mode launch is fullscreen whatever it says. iPhone, iPad, Apple TV
and Android have no equivalent.
shares the store — a Gaming-Mode launch is fullscreen whatever it says. iPhone, iPad, Apple TV and
Android have no equivalent.
## Interface
These change how the client itself looks and behaves. None of them touches a stream, so none of them
can live in a [profile](/docs/profiles-and-links) — they are decisions about the device in front of
you.
These change how the client itself looks and behaves. None touches a stream, so none can live in a
[profile](/docs/profiles-and-links) — they are decisions about the device in front of you.
**Gamepad-optimized browsing***default: on.* Swaps the touch or desktop home for the
controller-optimized one: the host carousel, larger focus targets, a swipeable cover browser, and
@@ -299,22 +293,20 @@ controller-optimized home is a separate entry point rather than a switch, so the
turn off. An Android TV is always in this mode — its remote is the only input it has.
**Show it***default: With a controller.* Only shown while the switch above is on, and it decides
*when* that switch takes effect. **With a controller** is the long-standing behaviour: the
controller-optimized home appears as a pad connects and the touch interface returns when the last one
disconnects. **Always** keeps the controller-optimized home either way — for a phone or tablet that
lives docked to a TV, where the pad isn't always awake but the couch layout is always the one you
want. Apple and Android. (An Android TV is in that mode regardless, so the choice changes nothing
*when* that switch takes effect. **With a controller**: the controller-optimized home appears as a
pad connects and the touch interface returns when the last one disconnects. **Always** keeps the
controller-optimized home either way — for a phone or tablet docked to a TV, where the pad isn't
always awake. Apple and Android. (An Android TV is in that mode regardless, so the choice changes nothing
there.)
**Background***default: Violet.* The colour family the controller-optimized home's living backdrop
drifts through. Thirteen of them: seven dark fields — **Violet**, **OLED**, **Nebula**, **Abyss**,
**Ember**, **Moss**, **Graphite** — then six pale ones, **Holo**, **Sunset**, **Bloom**, **Dawn**,
**Mint** and **Opal**, which flip the whole interface to dark text on a light field. The backdrop
recolours as you step the row, so pick by looking. **OLED** is the one with a practical point rather
than a decorative one: it is true black — most of the frame is pixels switched off, which on an OLED
or AMOLED panel means no glow and no power drawn, with only a faint violet ember left in one corner.
Stored under the same name on every client, so a phone, a Deck and a desktop set to Mint all look
alike. Appearance only — nothing about a stream depends on it.
recolours as you step the row, so pick by looking. **OLED** is the one with a practical point: it is
true black — most of the frame is pixels switched off, which on an OLED or AMOLED panel means no
glow and no power drawn, with only a faint violet ember left in one corner. Stored under the same
name on every client, so a phone, a Deck and a desktop set to Mint all look alike.
The row lives in the controller-optimized settings themselves — the screen you reach with **X** from
the controller-optimized home — on every platform that has one, which includes the Steam Deck and the
@@ -329,8 +321,7 @@ superset of the one before. This setting only picks the tier a session *starts*
them live in-stream, with a shortcut that differs by platform. The Apple app additionally lets you
choose which corner the overlay sits in (Top Left, Top Right, Bottom Left, Bottom Right). The
console home has the tier picker too, as **Statistics overlay** under **Interface**. The shortcuts,
and every number in the overlay, are in
[Understanding the stats overlay](/docs/stats).
and every number in the overlay, are in [Understanding the stats overlay](/docs/stats).
## Settings that are facts about your device
@@ -352,9 +343,7 @@ stay global and **cannot be put in a settings profile**:
- **Auto-wake on connect**, and **Show game library** where it still exists (the Apple and Android
apps) — decisions about this device and this network, not about how a given host is streamed.
- Everything under **Interface****Gamepad-optimized browsing**, **Show it** and **Background**.
How this client looks and which layout it wears has nothing to do with how a host streams to it,
so binding them to a host would only make the same device change appearance depending on what it
connected to.
How this client looks has nothing to do with how a host streams to it.
One switch you might expect here isn't in Settings at all: **Share clipboard** lives in a saved
host's own edit sheet, because handing a machine your clipboard is a decision about that one host —
+56 -64
View File
@@ -5,15 +5,13 @@ description: The ways to connect to a Punktfunk host — the Apple app, Moonligh
A Punktfunk host accepts clients over its own `punktfunk/1` protocol (the macOS, Linux, Windows, and
Android apps) and over GameStream (Moonlight). Pick whichever fits the device you're streaming *to*.
Ready to install?
**[Install a Client](/docs/install-client)** has the step-by-step for every device — plus how to
**[Install a Client](/docs/install-client)** has the step-by-step for every device, plus how to
[update](/docs/install-client#keeping-a-client-up-to-date) and
[remove](/docs/install-client#removing-a-client) each one.
Two things apply to every app, whichever you pick:
[profiles and `punktfunk://` links](#profiles-and-links-every-app), and the keys and chords that
work [while you're streaming](#while-youre-streaming). What each one lets you change — resolution,
bitrate, codec, HDR, audio, controllers — is catalogued in
Two things apply to every app: [profiles and `punktfunk://` links](#profiles-and-links-every-app),
and the keys and chords that work [while you're streaming](#while-youre-streaming). What each app
lets you change — resolution, bitrate, codec, HDR, audio, controllers — is catalogued in
[Client settings](/docs/client-settings).
## Apple app (Mac, iPhone, iPad, Apple TV)
@@ -29,9 +27,9 @@ protocol — the lowest-latency, most resilient path, with the full feature set:
- A live **stats overlay** (resolution, fps, bitrate, latency) and a built-in **network speed test**
to pick a bitrate for your link.
- **Widgets, Live Activities and Shortcuts** — a hosts widget and a game-library widget for the
home screen (the library one opens a host you pick straight into its library), a Live Activity
while a session runs, and App Intents so Siri and the Shortcuts app can start a stream or jump
into a host's game library.
home screen (the library one opens a picked host's library), a Live Activity while a session
runs, and App Intents so Siri and the Shortcuts app can start a stream or jump into a host's game
library.
Open the app, pick your host, [pair](/docs/pairing) once, and stream. It builds from the
`clients/apple` directory in the repo (Swift / VideoToolbox / Metal).
@@ -40,11 +38,9 @@ Open the app, pick your host, [pair](/docs/pairing) once, and stream. It builds
Punktfunk also speaks the **GameStream** protocol, so any [Moonlight](https://moonlight-stream.org/)
client — a browser, a smart TV, an old phone, a games console — connects with no punktfunk-specific
software. (Most platforms also have a native Punktfunk app below — Moonlight is the catch-all.) See
[Connect with Moonlight](/docs/moonlight).
This is the broadest-compatibility option and great for couch gaming. It doesn't use the native
protocol's FEC/encryption extensions, but for a healthy LAN that rarely matters.
software; it's the catch-all where no native Punktfunk app exists. See
[Connect with Moonlight](/docs/moonlight). It doesn't use the native protocol's FEC/encryption
extensions, but on a healthy LAN that rarely matters.
## Linux desktop client (GTK4)
@@ -52,26 +48,26 @@ protocol's FEC/encryption extensions, but for a healthy LAN that rarely matters.
`punktfunk/1` directly, with vendor-ordered hardware decode (**Vulkan Video first on NVIDIA and
AMD**, **VAAPI dmabuf first on Intel**; whichever isn't first is the fallback, and software decode
is last), PipeWire audio, and SDL3 controllers (rumble, lightbar, DualSense touchpad/motion). The
decoders are Punktfunk's own — the client links no FFmpeg at all, and talks to your GPU's Vulkan
and VAAPI drivers directly. To force one, pick it in *Preferences → Display → Video decoder* or
set `PUNKTFUNK_DECODER=native-vulkan|native-vaapi|software`. Like the Apple app it discovers hosts
on your network automatically, does PIN pairing, pins reconnects, and browses the host's
**game library** (with cover art) so you can launch a title straight into the stream.
decoders are Punktfunk's own — the client links no FFmpeg and talks to your GPU's Vulkan and VAAPI
drivers directly. To force one, pick it in *Preferences → Display → Video decoder* or set
`PUNKTFUNK_DECODER=native-vulkan|native-vaapi|software`. Like the Apple app it discovers hosts
automatically, does PIN pairing, pins reconnects, and browses the host's **game library** (with
cover art) to launch a title straight into the stream.
It ships as a real package, not just a source build — full steps in
It ships as real packages — full steps in
[Install a Client](/docs/install-client#linux-desktop-flatpak):
- **Any Flatpak distro (recommended)** — one command from the hosted `flatpak.unom.io` repo; the
guide linked above has the exact command and how updates work. It's also the client the
- **Any Flatpak distro (recommended)** — one command from the hosted `flatpak.unom.io` repo (exact
command and update flow in the guide above). It's also the client the
[Decky plugin](/docs/steam-deck) uses by default, though the plugin drives a native
`punktfunk-client` just as well.
- **Ubuntu 26.04 or newer** — `apt install punktfunk-client` from the Punktfunk apt registry. The
client package needs SDL3 and GTK4 ≥ 4.20, which Ubuntu 24.04 LTS doesn't ship — on 24.04 use the
Flatpak above.
Flatpak.
- **Fedora** — `sudo dnf install punktfunk-client` from the Gitea RPM registry (add the repo as in
the [Fedora guide](/docs/fedora)).
- **Fedora Atomic / Bazzite** — use the Flatpak above. `rpm-ostree install punktfunk-client` works,
but layering slows every OS update, so it's a last resort on an image-based system (see
- **Fedora Atomic / Bazzite** — use the Flatpak. `rpm-ostree install punktfunk-client` works, but
layering slows every OS update, so it's a last resort on an image-based system (see
[Bazzite](/docs/bazzite)).
- **Arch** — `sudo pacman -Syu punktfunk-client` from the signed binary repo (see [Arch Linux](/docs/arch)).
@@ -88,7 +84,7 @@ The client also updates itself (`punktfunk-client --check-update` / `--apply-upd
## Android app (phone + Android TV)
The native Android app speaks `punktfunk/1` directly, on both phones and Android TV. It does hardware
The native Android app speaks `punktfunk/1` directly, on phones and Android TV. It does hardware
HEVC decode (including [HDR10](/docs/hdr#per-client)), Opus audio with a mic uplink, game
controllers with rumble and DualSense feedback, automatic host discovery, PIN pairing with pinned
reconnects, the host's **game library** with cover art, and a live stats overlay — with D-pad and
@@ -96,29 +92,28 @@ game-controller focus navigation for the couch. It builds from the `clients/andr
(Kotlin + a shared Rust core).
**Controllers.** Plug a **DualSense**, **DualSense Edge** or **DualShock 4** into the phone or tablet
by USB and grant the USB permission Android asks for when it attaches — Punktfunk then drives the pad
itself instead of taking what Android's gamepad layer exposes, so the host gets rumble, adaptive
triggers, the lightbar and gyro. The app's **Controllers** screen lists attached pads and their
capture state, and the switch that turns this off is *DualSense / DualShock passthrough (USB)* in
Settings. Over **Bluetooth** the pad still works as an ordinary gamepad, but adaptive triggers and
the lightbar need the USB connection.
by USB and grant the USB permission Android asks for — Punktfunk then drives the pad itself instead
of taking what Android's gamepad layer exposes, so the host gets rumble, adaptive triggers, the
lightbar and gyro. The app's **Controllers** screen lists attached pads and their capture state; the
switch that turns this off is *DualSense / DualShock passthrough (USB)* in Settings. Over
**Bluetooth** the pad still works as an ordinary gamepad, but adaptive triggers and the lightbar
need USB.
The app is on **[Google Play](https://play.google.com/store/apps/details?id=io.unom.punktfunk)** as a
public listing — no invite — or you can sideload the public APK instead (see
public listing — no invite — or sideload the public APK (see
[Install a Client](/docs/install-client#android)); canary builds ride a separate, invite-only Play
Internal testing track. Then open the app, pick your host, [pair](/docs/pairing) once, and stream.
Internal testing track. Open the app, pick your host, [pair](/docs/pairing) once, and stream.
## Windows desktop client
`punktfunk-client` for Windows (`clients/windows`) is the native graphical client for Windows — pure
Rust, the same `punktfunk/1` core as the Apple, Linux, and Android apps, with a **WinUI 3** UI (host
list, settings, PIN pairing); the stream itself runs in Punktfunk's Vulkan presenter. Its decoder
order is per-vendor: **Vulkan Video, then D3D11VA, then software** on NVIDIA and AMD, and
**D3D11VA first** on Intel and other GPUs (Intel's driver advertises Vulkan Video, but DXVA is the
proven path there), with [10-bit/HDR present](/docs/hdr#per-client), WASAPI audio + mic,
SDL3 controllers (rumble, lightbar, DualSense), network discovery, the host's **game library** with
cover art, and the full PIN-pairing trust surface. It builds for both `x86_64` and `aarch64` and
ships as a **signed MSIX**. Launch it and pick a host from the list, just like the other native apps.
`punktfunk-client` for Windows (`clients/windows`) is the native graphical client — pure Rust, the
same `punktfunk/1` core as the Apple, Linux, and Android apps, with a **WinUI 3** UI (host list,
settings, PIN pairing); the stream itself runs in Punktfunk's Vulkan presenter. Decoder order is
per-vendor: **Vulkan Video, then D3D11VA, then software** on NVIDIA and AMD, and **D3D11VA first**
on Intel and other GPUs (Intel's driver advertises Vulkan Video, but DXVA is the proven path there).
It has [10-bit/HDR present](/docs/hdr#per-client), WASAPI audio + mic, SDL3 controllers (rumble,
lightbar, DualSense), network discovery, the host's **game library** with cover art, and the full
PIN-pairing trust surface. It builds for `x86_64` and `aarch64` and ships as a **signed MSIX**.
The package installs **two** Start-menu entries — **Punktfunk**, the desktop window, and
**Punktfunk Console**, a controller-driven fullscreen interface for a TV or HTPC (host list, pairing,
@@ -130,7 +125,7 @@ settings and game library, all navigable with a pad) — plus the headless
> is a proven alternative for Windows.
For scripting, prefer the [`punktfunk` CLI](#scripting-the-punktfunk-cli). The window binary's own
headless flags stay supported too:
headless flags stay supported:
```sh
punktfunk-client # open the WinUI 3 window (host list / settings)
@@ -138,12 +133,10 @@ punktfunk-client --discover # list hosts on the
punktfunk-client --headless --speed-test --connect <host>:9777 # no window: probe the link, print measured/recommended bitrate
```
Prefer the broadest compatibility, or no install? **Moonlight** also streams to Windows (see below).
## webOS (LG TV) — community
[`pf-webos`](https://github.com/dyptan-io/pf-webos) is a native client for LG webOS TVs, built and
maintained by the community ([dyptan-io](https://github.com/dyptan-io)) on top of Punktfunk's
maintained by the community ([dyptan-io](https://github.com/dyptan-io)) on Punktfunk's
`punktfunk/1` protocol and core. It's not an official Punktfunk app, but it speaks the real protocol
directly (not Moonlight/GameStream) — LAN discovery or add-by-IP, PIN pairing with pinned reconnects,
hardware video decode via webOS's NDL DirectMedia API, and a browsable game library with cover art,
@@ -178,21 +171,21 @@ named, **6** it needs a person (pairing, or an unknown host).
Under the Flatpak, run it as `flatpak run --command=punktfunk io.unom.Punktfunk <args>`.
> The older headless flags stay supported for existing scripts — `punktfunk-client --connect`,
> `--discover` and `--headless --speed-test` on both Linux and Windows. `punktfunk` is the surface
> to build new things on: it wakes a sleeping host before connecting, which those never did.
> The older `punktfunk-client --connect`, `--discover` and `--headless --speed-test` flags stay
> supported on Linux and Windows for existing scripts, but build new things on `punktfunk`: it wakes
> a sleeping host before connecting, which those never did.
>
> `punktfunk-probe` is a different thing again — an in-repo protocol test and latency-measurement
> tool for development. It isn't shipped in any package; you build it from source.
> `punktfunk-probe` is different again — an in-repo protocol test and latency-measurement tool for
> development, not shipped in any package; you build it from source.
## Profiles and links (every app)
Two things work the same in the Apple, Linux, Windows and Android apps. **Settings profiles** are
named sets of stream overrides — bitrate, resolution, codec, HDR and the rest — that you bind to a
host or pick for a single connect, with every field you didn't touch still following your defaults.
And a **`punktfunk://` link** starts a stream from a browser, a desktop shortcut, a home-automation
rule or `punktfunk open`, carrying only *references* to things that already exist on your device —
never a setting, and never a trust decision.
host or pick for a single connect; every field you didn't touch still follows your defaults. A
**`punktfunk://` link** starts a stream from a browser, a desktop shortcut, a home-automation rule
or `punktfunk open`, carrying only *references* to things that already exist on your device — never
a setting, never a trust decision.
[Profiles and links](/docs/profiles-and-links) has both in full: the link grammar, where each app
puts **Copy link** and **Create shortcut…**, and what a link is refused for. From a script,
@@ -203,14 +196,14 @@ puts **Copy link** and **Create shortcut…**, and what a link is refused for. F
Click the stream and the desktop clients **capture** your keyboard and mouse — everything goes to
the host until you let go. **Ctrl+Alt+Shift+Q** (⌃⌥⇧Q or ⌘⎋ on a Mac) gives it back.
That chord, the three others a stream reserves, the controller chord that works with no keyboard in
reach, which app honours which of them, the two mouse modes, the three touch modes and stylus input
are all on [Mouse, touch and pen](/docs/input#getting-your-input-back).
That chord, the three others a stream reserves, the controller chord that needs no keyboard, which
app honours which, the two mouse modes, the three touch modes and stylus input are all on
[Mouse, touch and pen](/docs/input#getting-your-input-back).
Copying between the two machines is a separate opt-in: the host operator allows it in `host.env`
and you turn it on for that one host in your client. Content crosses today from the macOS, iOS,
iPadOS, Windows and Android apps — the Linux client has the switch but no bridge behind it yet, and
tvOS has no pasteboard to share. See [Shared clipboard](/docs/clipboard).
and you turn it on per host in your client. Content crosses today from the macOS, iOS, iPadOS,
Windows and Android apps — the Linux client has the switch but no bridge behind it yet, and tvOS
has no pasteboard to share. See [Shared clipboard](/docs/clipboard).
## Which should I use?
@@ -226,5 +219,4 @@ tvOS has no pasteboard to share. See [Shared clipboard](/docs/clipboard).
| Scripts, plugins, home automation | The headless **[`punktfunk`](#scripting-the-punktfunk-cli)** CLI |
| Protocol development / latency measurement | **`punktfunk-probe`** (source build only) |
Whichever you choose, the first connection needs a one-time [pairing](/docs/pairing), and
[Install a Client](/docs/install-client) covers installing, updating and removing it.
Whichever you choose, the first connection needs a one-time [pairing](/docs/pairing).
+36 -40
View File
@@ -4,17 +4,16 @@ description: Copy on one machine and paste on the other — the two switches tha
---
Punktfunk can share the clipboard between the machine you are sitting at and the host you are
streaming. Copy a URL on your laptop, paste it into a browser on the host. Copy an error message on
the host, paste it into a chat app on your laptop.
streaming, in both directions — copy a URL on your laptop, paste it on the host, and back.
**Two separate switches have to be on:**
1. The **host** operator has to allow it, with a line in `host.env` and a host restart. This one is
off by default.
2. **You** have to turn it on for that one host, in that host's edit sheet on your client. This one
is off by default on the macOS, Windows and Linux clients — but **on by default on Android**.
1. The **host** operator has to allow it, with a line in `host.env` and a host restart. Off by
default.
2. **You** have to turn it on for that one host, in that host's edit sheet on your client. Off by
default on the macOS, Windows and Linux clients — **on by default on Android**.
Flipping one and not the other looks exactly like the feature not existing. So check both.
Flipping one and not the other looks exactly like the feature not existing. Check both.
## 1. Allow it on the host
@@ -52,14 +51,14 @@ punktfunk-host service restart
See [Configuration](/docs/configuration) for the rest of `host.env`.
> **About the file mode.** No client shipping today asks for file transfer, and no host clipboard
> backend offers file formats yet. `on` and `text-only` therefore behave the same in practice —
> `text-only` is how you make that explicit and keep it that way.
> backend offers file formats yet, so `on` and `text-only` behave the same in practice — `text-only`
> makes that explicit and keeps it that way.
## 2. Turn it on for that host, in your client
The client switch is **per saved host**, not global: handing a machine your clipboard is a decision
about *that* machine. You set it in the host's edit sheet, and it is deliberately not something a
[settings profile can carry](/docs/profiles-and-links#what-a-profile-cant-change).
The client switch is **per saved host**, not global handing a machine your clipboard is a decision
about *that* machine — so it lives in the host's edit sheet, deliberately not in a
[settings profile](/docs/profiles-and-links#what-a-profile-cant-change).
| Client | Where the switch is | Label | Default |
|---|---|---|---|
@@ -69,39 +68,38 @@ about *that* machine. You set it in the host's edit sheet, and it is deliberatel
| Linux (GTK) | Host card menu → **Edit…** | **Share clipboard** | Off |
| Android (touch) | Host card menu → **Edit…** | **Shared clipboard** | **On** |
On Android the switch is only in the touch edit dialog. The controller/TV interface — what you get
on Android TV, and on a phone when a controller is attached — has its own **Edit Host** screen with
no clipboard row, so there is nowhere to change it there. It stays on, which is the Android default.
On Android the switch is only in the touch edit dialog. The controller/TV interface — Android TV,
and a phone with a controller attached — has its own **Edit Host** screen with no clipboard row, so
it stays on, the Android default.
The setting is read when a session starts, so if you change it while streaming, reconnect.
macOS can also flip it mid-session: **Stream ▸ Share Clipboard** (⌃⌥⇧C), which becomes **Stop
Sharing Clipboard** once the host has acknowledged it. On an iPad with a hardware keyboard the same
combo works, though there is no menu bar to show it in — and only while the pointer is released, as
a captured session sends the keys to the host instead.
a captured session sends the keys to the host.
tvOS and a Steam Deck in Gaming Mode have no clipboard switch — the Apple TV has no pasteboard to
share at all, and neither the Decky panel nor the client's console home has a host edit sheet — see
share, and neither the Decky panel nor the client's console home has a host edit sheet — see
[what each client does](#which-hosts-and-clients-support-it) below.
## Nothing crosses until something pastes
A copy costs nothing. When you copy, your machine announces only the **list of formats** it now
holds — no bytes. The bytes are pulled across on a separate transfer, and only when an application
on the other end actually pastes. Copying a large image and never pasting it transfers nothing.
holds — no bytes. The bytes are pulled across on a separate transfer, only when an application on
the other end actually pastes. Copying a large image and never pasting it transfers nothing.
That holds for everything you copy on your own machine, and for both directions on the host. It
does **not** hold for a host copy arriving at a Windows or Android client: those two fetch the
content straight away and put it on your local clipboard, whether or not you ever paste. On Windows
that is because the lazy path needs Windows delayed rendering, which the client doesn't implement
yet; on Android there is no way to satisfy a paste from the network at all. The macOS and iOS
clients are lazy in both directions.
content straight away and put it on your local clipboard, whether or not you ever paste — on
Windows because the lazy path needs Windows delayed rendering, which the client doesn't implement
yet; on Android because there is no way to satisfy a paste from the network at all. The macOS and
iOS clients are lazy in both directions.
On iOS there is one deliberate exception. Backgrounding the app ends the session, and a promise
nobody can answer is worse than no promise at all — so if the host copied something and you have not
pasted it yet, those bytes are pulled across as the session ends, up to 8 MiB. That is what makes
"copy on the host, switch to Safari, paste" work on an iPad. Nothing is fetched if you never leave
the app, or if you already pasted.
iOS has one deliberate exception. Backgrounding the app ends the session, so if the host copied
something you have not pasted yet, those bytes are pulled across as the session ends, up to 8 MiB. That is what makes "copy on the host,
switch to Safari, paste" work on an iPad. Nothing is fetched if you never leave the app, or if you
already pasted.
A single transfer is capped at 64 MiB. Nothing else limits size, so a very large host-side copy can
cross to a Windows or Android client for a paste that never happens.
@@ -119,9 +117,7 @@ from a Punktfunk client. A Moonlight client has no clipboard.
## Which hosts and clients support it
**Hosts.** The host runs on Linux and Windows, and both have a clipboard backend — but on Linux it
depends on the desktop session.
On Linux the host needs one of two mechanisms in the session it is streaming:
depends on the desktop session, which needs one of two mechanisms:
- `ext-data-control-v1` — KWin, wlroots/Sway and Hyprland. Tried first.
- GNOME's own `org.gnome.Mutter.RemoteDesktop.Session` clipboard, used directly. Tried second.
@@ -154,8 +150,8 @@ registered `PNG` clipboard format. Many Windows apps publish only a bitmap, and
announced yet. The other direction is fine: an image copied on the host reaches the Windows client
either way.
The host side is richer than any client: it can offer and accept text, HTML, RTF, PNG, JPEG and
GIF. What you get is therefore whatever your client supports.
The host side is richer than any client: it can offer and accept text, HTML, RTF, PNG, JPEG and GIF.
What you get is whatever your client supports.
## Why the toggle does nothing (or is greyed out)
@@ -164,18 +160,18 @@ connected host did not advertise a clipboard. On the other clients there is noth
the per-host switch always looks available, and a host that can't do it simply does nothing. Work
through these in order:
- **The host has it off.** The default. Nothing was added to `host.env`, or the value is `off`,
`0`, `false` or empty. Fix it with step 1 above.
- **The host has it off.** The default. Nothing was added to `host.env`, or the value is `off`, `0`,
`false` or empty. Fix it with step 1 above.
- **`host.env` was edited but the host wasn't restarted.** The file is read once, at startup.
- **The switch is off for this host in your client.** It is per saved host, and off by default
everywhere except Android. Check the host's **Edit…** sheet — step 2 above.
- **The host's session has no supported backend.** The host allows the clipboard, so it still
advertises the capability, but it has nothing to read the desktop's clipboard with. This is a
gamescope session, a compositor with only the old `zwlr-data-control-unstable-v1`, or a GNOME
session whose Mutter doesn't expose the direct RemoteDesktop clipboard. Nothing on screen tells
you this apart — the host log does.
advertises the capability, but has nothing to read the desktop's clipboard with: a gamescope
session, a compositor with only the old `zwlr-data-control-unstable-v1`, or a GNOME session whose
Mutter doesn't expose the direct RemoteDesktop clipboard. Nothing on screen tells you this apart —
the host log does.
- **The host is older than the feature.** A host from before clipboard sync never advertises it.
- **Your client doesn't implement it** — Linux, Steam Deck, iOS, iPadOS or tvOS. Nothing crosses
- **Your client doesn't implement it** — Linux (GTK), Steam Deck or tvOS. Nothing crosses
regardless of what the host allows.
- **You changed the switch while connected.** Reconnect, or use ⌃⌥⇧C on macOS.
- **The copy was a secret, or a format nobody handles.** Concealed content is skipped on purpose on
+77 -85
View File
@@ -3,50 +3,46 @@ title: Controller speaker and haptics
description: DualSense voice-coil haptics and the pad's built-in speaker, streamed from the host to the controller in your hands — what to enable, and what "set it to Pro Audio" means on a Linux host.
---
A DualSense is partly an audio device. Its little speaker and its two voice-coil motors — the
actuators that make a PS5 pad feel like sand, rain or a bowstring instead of a buzzing phone —
are all driven by a four-channel audio stream, not by rumble commands. Games that support them
write PCM into "the controller's audio device".
A DualSense is partly an audio device. Its speaker and its two voice-coil motors are driven by a
four-channel audio stream, not by rumble commands. Games that support them write PCM into "the
controller's audio device".
Punktfunk gives that device to the game on the host, captures what the game writes, and streams
it to the controller physically in your hands, on its own low-latency plane. Channels 12 are the
pad's speaker, channels 34 are the voice coils.
Punktfunk gives that device to the game on the host, captures what the game writes, and streams it
to the controller in your hands on its own low-latency plane. Channels 12 are the pad's speaker,
channels 34 the voice coils.
## What you need
- **A DualSense or DualSense Edge plugged in over USB** on the client. Bluetooth pads expose no
audio interface at all, so they fall back to ordinary rumble — this is a limit of the
controller, not of Punktfunk.
audio interface, so they fall back to ordinary rumble — a limit of the controller, not of Punktfunk.
- On the client, **Controller haptics** is on by default. So is **Controller speaker** on the Linux
and Windows apps — turn it off in [client settings](/docs/client-settings#input) if you would
rather all game audio came out of your speakers. On Android the speaker is opt-in.
- On a **Linux host**, a game that speaks DualSense — which in practice means running it under
**GE-Proton 11-5 or newer**. Stock Proton does not route controller audio.
and Windows apps — turn it off in [client settings](/docs/client-settings#input) if you'd rather
all game audio came out of your speakers. On Android the speaker is opt-in.
- On a **Linux host**, a game that speaks DualSense — in practice, running under **GE-Proton 11-5 or
newer**. Stock Proton does not route controller audio.
- On the host, controller audio is on by default (`PUNKTFUNK_PAD_AUDIO`).
Nothing is sent while the pad is quiet, so leaving it on costs nothing.
## "Set the controller audio to Pro Audio" — you don't have to
If you have looked into DualSense haptics on Linux before, you have probably run into this
advice: plug the pad into the Linux box, open your sound settings, find *DualSense wireless
controller (PS5)*, and switch its **Profile** to **Pro Audio**. That advice is real and it is
correct — for a pad plugged directly into the host.
The usual advice for DualSense haptics on Linux — plug the pad into the Linux box, open your sound
settings, find *DualSense wireless controller (PS5)*, switch its **Profile** to **Pro Audio** — is
correct for a pad plugged directly into the host.
The reason is channel layout. A pad's other profiles present it as a mono speaker, a stereo
headphone jack, or a positioned four-channel "surround" device. Games write their haptics as four
*unpositioned* channels, so on any of those profiles the audio system helpfully re-mixes them into
the speaker pair and the voice-coil channels are folded away. You feel nothing. Pro Audio is the
one profile that hands the four channels through untouched, in order.
*unpositioned* channels, so on any of those profiles the audio system re-mixes them into the speaker
pair and the voice-coil channels are folded away. Pro Audio is the one profile that hands the four
channels through untouched, in order.
**Punktfunk's controller audio device is already in that shape.** It is created as four raw
channels with no re-mixing, which is exactly what Pro Audio produces — so there is nothing to
switch, and no switch to make.
**Punktfunk's controller audio device is already in that shape** — four raw channels with no
re-mixing, exactly what Pro Audio produces — so there is nothing to switch.
That is also why it looks different in your sound settings. A real pad is a USB sound card, so it
gets a **Profile** dropdown; Punktfunk's is a software device, so it has no card and no dropdown.
Seeing **Wireless Controller** with a volume slider and no profile selector is what a correctly
minted controller-audio device looks like. It is not a sign that something is missing.
gets a **Profile** dropdown; Punktfunk's is a software device with no card and no dropdown. Seeing
**Wireless Controller** with a volume slider and no profile selector is what a correctly minted
controller-audio device looks like, not a sign that something is missing.
## Checking it is working
@@ -68,9 +64,9 @@ When a game actually starts driving the actuators, the pad's own driver reports
DS5 title asserted haptics-select (audio haptics) pad=0
```
That last line is the one that matters: it means a title recognised the controller as an audio
device and switched the pad out of plain rumble. If you see it and still feel nothing, the problem
is downstream — on the client or the pad. If you never see it, the game never found the device.
That last line is the one that matters: a title recognised the controller as an audio device and
switched the pad out of plain rumble. If you see it and still feel nothing, the problem is
downstream — on the client or the pad. If you never see it, the game never found the device.
You can also look at the device directly:
@@ -78,22 +74,22 @@ You can also look at the device directly:
pactl list sinks | grep -A25 Speaker__sink
```
The line to check is `audio.position = "AUX0,AUX1,AUX2,AUX3"` — four unpositioned channels is the
layout that reaches the voice coils. Anything positioned (`FL,FR,RL,RR`) would not.
Check for `audio.position = "AUX0,AUX1,AUX2,AUX3"` — four unpositioned channels is the layout that
reaches the voice coils. Anything positioned (`FL,FR,RL,RR`) would not.
## If a game does not find it
Games identify the controller's audio device by name and by USB ids, and different titles check
different things. GE-Proton has several routes to the pad, and a couple of them are opt-in per
game. Add these as launch options if a title is not cooperating:
different things. GE-Proton has several routes to the pad, a couple of them opt-in per game. Add
these as launch options if a title is not cooperating:
```
PROTON_DUALSENSE_HAPTICS_PREFER_NON_EVENT=1 %command%
```
This forces GE onto its most direct route — it opens Punktfunk's controller-audio device by name
and writes the four channels straight into it, with no re-mixing anywhere in between. It is the
first thing to try.
This forces GE onto its most direct route — it opens Punktfunk's controller-audio device by name and
writes the four channels straight into it, with no re-mixing in between. It is the first thing to
try.
Some titles additionally want:
@@ -112,55 +108,52 @@ To see which route GE took, launch the game with `WINEDEBUG=+pulse` and look for
## On a Linux client, the pad's own profile matters too
Everything above is about the host, where the controller-audio device is one Punktfunk mints. On a
Linux **client** the pad is real, and the same channel-layout problem shows up from the other side:
the voice coils are physically channels 3 and 4 of the controller's USB sound card, and a
controller almost never presents as a four-channel device on its own. Depending on your distribution
it appears as a stereo output, or as a mono *Speaker* plus a stereo *Headphones* pair. Playing into
any of those puts the haptics in the headphone jack and folds the coil channels away — audio that
looks perfectly healthy, felt as nothing at all.
On a Linux **client** the pad is real, and the same channel-layout problem shows up from the other
side: the voice coils are physically channels 3 and 4 of the controller's USB sound card, and a
controller almost never presents as a four-channel device on its own. Depending on your
distribution it appears as a stereo output, or as a mono *Speaker* plus a stereo *Headphones* pair.
Playing into any of those puts the haptics in the headphone jack and folds the coil channels away —
audio that looks healthy, felt as nothing.
**Punktfunk handles this for you.** When it needs the coils and the pad is not already presenting
four channels, it switches the controller's card to **Pro Audio** for the length of the session and
puts your setting back afterwards. You will see the profile change in your sound settings while you
are streaming; that is expected. It is never saved as the card's remembered profile.
puts your setting back afterwards. You will see the profile change in your sound settings while
streaming; that is expected. It is never saved as the card's remembered profile.
If you would rather manage the card yourself, set `PUNKTFUNK_PAD_AUDIO_PROFILE=0` on the client. Then
Punktfunk uses a four-channel profile if you have already selected one and logs what it needs if you
have not.
Many systems never reach the switch at all. On **SteamOS** a DualSense already exposes its four
channels behind a combined speaker-and-haptics output, and Punktfunk finds them there. That is a
Valve addition, though, not something every up-to-date system has: `alsa-ucm-conf` upstream — and
so Fedora, Bazzite and Arch — describes the pad as a *mono speaker plus stereo headphones* and
nothing else, which is precisely the shape that folds the coils away. The switch is the fallback
for those. **If you run the client as a Flatpak**, your audio manager may not let a sandboxed app
change a card's profile; if the log says so, switch the controller to Pro Audio yourself, which is
the same fix.
Many systems never reach the switch. On **SteamOS** a DualSense already exposes its four channels
behind a combined speaker-and-haptics output, and Punktfunk finds them there. That is a Valve
addition, not something every up-to-date system has: `alsa-ucm-conf` upstream — and so Fedora,
Bazzite and Arch — describes the pad as a *mono speaker plus stereo headphones* and nothing else,
precisely the shape that folds the coils away. The switch is the fallback for those. **If you run
the client as a Flatpak**, your audio manager may not let a sandboxed app change a card's profile; if
the log says so, switch the controller to Pro Audio yourself, which is the same fix.
Punktfunk's **host** packages (rpm, deb, Arch, and the Bazzite sysext) close that gap at the
source: they install a small ALSA profile for the DualSense that adds the combined
speaker-and-haptics output SteamOS has, and give it priority over the mono one. It adds files
rather than replacing any your distribution owns, so it upgrades cleanly and can be removed by
uninstalling Punktfunk. A pad plugged into the host then presents four channels on its own, with
no profile switching by anyone — and, because the lone mono output stops existing, games that
crashed when they opened it stop crashing. A card reads its profile once, when it appears, so
replug the pad after installing (or restart PipeWire) rather than expecting a pad that was
already plugged in to pick it up.
Punktfunk's **host** packages (rpm, deb, Arch, and the Bazzite sysext) close that gap at the source:
they install a small ALSA profile for the DualSense that adds the combined speaker-and-haptics
output SteamOS has, and give it priority over the mono one. It adds files rather than replacing any
your distribution owns, so it upgrades cleanly and can be removed by uninstalling Punktfunk. A pad
plugged into the host then presents four channels on its own, with no profile switching by anyone —
and, because the lone mono output stops existing, games that crashed when they opened it stop
crashing. A card reads its profile once, when it appears, so replug the pad after installing (or
restart PipeWire) rather than expecting an already-plugged pad to pick it up.
### Checking the client side without a host
The client can test the whole path on its own — no host, no game, no pairing. Plug in the
DualSense and run:
The client can test the whole path on its own — no host, no game, no pairing. Plug in the DualSense
and run:
```sh
punktfunk-session --pad-audio-test
```
It prints every DualSense object it can see in your audio graph, says which one it chose, and then
plays a tone into the voice coils for three seconds. **If the pad buzzes, the client side is
working** and any remaining silence is coming from the host or the game. Add `--speaker` to test
the pad's speaker instead, and `--seconds N` for a longer run.
It prints every DualSense object it can see in your audio graph, says which one it chose, and plays
a tone into the voice coils for three seconds. **If the pad buzzes, the client side is working** and
any remaining silence is coming from the host or the game. Add `--speaker` to test the pad's speaker
instead, and `--seconds N` for a longer run.
On the Steam Deck and other flatpak installs, run it inside the sandbox:
@@ -172,30 +165,29 @@ flatpak run --command=punktfunk-session io.unom.Punktfunk --pad-audio-test
The controller's speaker and its headphone jack **share a channel**. Channel 1 of the pad's audio
device is the headphone jack's right channel *and* the built-in speaker, and the controller decides
which one actually sounds. It powers up pointing at the jack — so with nothing plugged in, a
perfectly routed speaker stream is heard by nobody.
which one sounds. It powers up pointing at the jack — so with nothing plugged in, a perfectly routed
speaker stream is heard by nobody.
Punktfunk points the pad at its own speaker when **Controller speaker** is on. The voice coils are
different channels and are not affected by that choice, which is why haptics work as soon as the
audio is routed correctly and the speaker needs this extra step. A game that drives the pad's audio
settings itself still overrides it. If your pad's speaker stays quiet, `PUNKTFUNK_PAD_SPEAKER_PATH`
and `PUNKTFUNK_PAD_SPEAKER_VOLUME` let you bisect it without a rebuild.
different channels and unaffected by that choice. A game that drives the pad's audio settings
itself still overrides it. If your pad's speaker stays quiet, `PUNKTFUNK_PAD_SPEAKER_PATH` and
`PUNKTFUNK_PAD_SPEAKER_VOLUME` let you bisect it without a rebuild.
## Known limits
- **Bluetooth client pads get rumble, not haptics.** No audio interface exists over BT.
- **Titles that match the controller by container ID** — a Windows notion of "these devices are
the same physical thing" — will not recognise the pairing on a Linux host, because the virtual
pad has no USB device behind it to derive one from. Titles that match by name or by USB ids are
unaffected, which is most of them.
- **Titles that match the controller by container ID** — a Windows notion of "these devices are the
same physical thing" — will not recognise the pairing on a Linux host, because the virtual pad has
no USB device behind it to derive one from. Titles that match by name or by USB ids — most of them
— are unaffected.
- **A pad plugged into the host itself can steal the audio.** If a real DualSense is connected to
the host while you are streaming to a different one, some titles will find the local pad's sound
card first. Unplug it, or stream from a host that has no pad attached.
the host while you stream to a different one, some titles find the local pad's sound card first.
Unplug it, or stream from a host that has no pad attached.
- **The Pro Audio switch on a Linux client renames the pad's microphone too.** Switching a sound
card's profile re-creates all of its inputs and outputs, so if you had picked the DualSense's own
microphone as your [mic](/docs/client-settings#audio), that session falls back to your default
one. Pick a different microphone, or set `PUNKTFUNK_PAD_AUDIO_PROFILE=0` and select a
four-channel profile on the card yourself.
- **A client killed mid-stream leaves the pad on Pro Audio.** The profile is restored when a
session ends normally and is never written to your saved settings, so anything that reloads the
card — unplugging it, logging out, a reboot — brings your own profile back.
one. Pick a different microphone, or set `PUNKTFUNK_PAD_AUDIO_PROFILE=0` and select a four-channel
profile on the card yourself.
- **A client killed mid-stream leaves the pad on Pro Audio.** The profile is restored when a session
ends normally and is never written to your saved settings, so anything that reloads the card —
unplugging it, logging out, a reboot — brings your own profile back.
-227
View File
@@ -1,227 +0,0 @@
---
title: Debian
description: Install the Punktfunk host on Debian 13 with apt — including LMDE and Linux Mint.
---
Install a Punktfunk host on **Debian 13 ("trixie") or newer** from the apt registry. This page
covers the distro-level setup — GPU driver, package, gamepad access. How the host creates its
virtual display and injects input is desktop-specific, so pick your desktop on the
[configure pages](#configure-your-desktop) afterward rather than here.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
> **Which releases.** The host package needs **glibc 2.39 or newer**; Debian 13 has 2.41, so it
> installs and runs there. **Debian 12 (bookworm) has glibc 2.36 and cannot install it** — build
> from source ([Ubuntu appendix](/docs/ubuntu#appendix--build-from-source), which applies here too)
> or upgrade. Check yours with `ldd --version`.
> **The desktop client is not packaged for Debian yet.** `punktfunk-client` is built on Ubuntu 26.04
> and floors at `libc6 >= 2.43` (Debian 13 has 2.41), on top of needing GTK4 ≥ 4.20. On a Debian
> box, stream *to* it with a [different client](/docs/install-client) — the Flatpak, or a build from
> source. The **host**, the **web console** and the **plugin runner** all install normally.
## What works on Debian 13
| Package | Debian 13 | What it is |
|---|---|---|
| `punktfunk-host` | ✅ | The streaming host |
| `punktfunk-web` | ✅ | The browser management console |
| `punktfunk-scripting` | ✅ | The plugin/script runner |
| `punktfunk-gamescope` | ✅ | The patched gamescope (HDR + cursor + real refresh) |
| `punktfunk-client` | ❌ | Desktop client — `libc6 >= 2.43`, see above |
## 1. GPU driver
On **NVIDIA**, the driver lives in Debian's `contrib` / `non-free` / `non-free-firmware`
components, which a default install does not enable. Debian 13 keeps its sources in the deb822
format, so add them there and refresh:
```sh
sudo sed -i 's/^Components: .*/Components: main contrib non-free non-free-firmware/' \
/etc/apt/sources.list.d/debian.sources
sudo apt update
sudo apt install nvidia-driver firmware-misc-nonfree
```
Debian 13 ships driver 550, comfortably above the [535 floor](/docs/requirements).
Reboot, then confirm the driver and KMS modeset — Wayland on NVIDIA needs `modeset=1`:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y
```
If modeset is not `Y`:
```sh
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
```
> **Secure Boot:** with Secure Boot enabled, Debian's DKMS-built NVIDIA module must be signed and
> its key enrolled before it will load. If `nvidia-smi` can't talk to the driver, enrol the MOK
> (`sudo mokutil --import /var/lib/dkms/mok.pub`, reboot, choose **Enrol MOK**) or disable Secure
> Boot in firmware.
On **AMD/Intel** none of the NVIDIA steps apply. Encode runs on the Mesa stack: **Vulkan Video** for
HEVC and AV1 (`mesa-vulkan-drivers`), with **VAAPI** for H.264 and as the fallback —
`mesa-va-drivers` on AMD, `intel-media-va-driver` on Intel (the latter is in `non-free`).
## 2. Install the host (apt)
The registry is public — no auth needed, just trust its signing key:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
`punktfunk-host` `Recommends` the browser console (`punktfunk-web`), so apt pulls it in by default.
The NVIDIA driver is **not** a dependency — you installed it out of band in step 1. Later updates
are `sudo apt update && sudo apt upgrade`; restart the running host afterwards so it picks up the
new binary:
```sh
systemctl --user restart punktfunk-host
```
The `stable` component above is the stable channel. To track pre-release builds instead, see
[Release Channels](/docs/channels).
## 3. Grant gamepad access
Virtual gamepads inject through `/dev/uinput`, gated by the `input` group. Add yourself and re-login:
```sh
sudo usermod -aG input "$USER" # re-login to apply
```
Also join `punktfunk` if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro) —
it reaches games as a real USB device over usbip, which is what makes Steam Input adopt it. Join it
only on a machine you trust: writing the usbip `attach` file can materialise arbitrary emulated USB
hardware.
```sh
sudo usermod -aG punktfunk "$USER" # re-login to apply
```
## 4. Check it installed
```sh
punktfunk-host --version # the binary is on PATH
punktfunk-host detect-conflicts # exits 1 if Sunshine/Apollo is also installed
```
Two hosts on one machine is the most common reason a clean install never streams — see
[Troubleshooting](/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed).
## 5. Open the firewall (if you have one)
**Debian ships no firewall enabled by default**, so out of the box there is nothing to open. If you
run one, the package installs the openers:
```sh
# ufw:
sudo ufw allow punktfunk-native
# firewalld:
sudo firewall-cmd --reload # load the installed definitions
sudo firewall-cmd --permanent --add-service=punktfunk-native
sudo firewall-cmd --reload
```
Add `punktfunk-gamestream` for Moonlight compat and `punktfunk-web` (TCP 47992) to reach the console
from another device. Full port lists are in
[`packaging/debian/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md#firewall).
## Cinnamon, Linux Mint and LMDE
**A Cinnamon desktop cannot host a virtual display, and no setting changes that.** Punktfunk gives
each client its own screen at that device's exact resolution by asking the compositor to create a
virtual output. Cinnamon's compositor, **Muffin**, has no such API: it forked from Mutter 3.36, and
its `org.cinnamon.Muffin.ScreenCast` interface offers only `RecordMonitor` and `RecordWindow`
never the `RecordVirtual` that Mutter gained in 42. Its portal backend
(`xdg-desktop-portal-xapp`) implements no ScreenCast either, so the route that serves Sway and
Hyprland is closed too. This is upstream's to fix, not a Punktfunk setting.
**Which Mint you run decides whether there is any route at all:**
| Edition | Base | Can it host? |
|---|---|---|
| **LMDE 7 "Gigi"** | Debian 13 | ✅ Yes — via gamescope (below) |
| **Linux Mint 22.x** ("Wilma"…"Zena") | Ubuntu 24.04 | ❌ No — see [below](#linux-mint-22x-cannot-host-yet) |
| **Linux Mint 23** | Ubuntu 26.04 | ✅ Expected — due December 2026 |
On **LMDE 7**, what works is **gamescope**: the host starts its own headless gamescope for each
connecting client and runs the game inside it, so it needs no desktop compositor at all. Your
Cinnamon session keeps running untouched; the stream is the game, not the desktop.
```sh
sudo apt install punktfunk-gamescope # LMDE 7 / Debian 13 — not available on Mint 22.x
echo 'PUNKTFUNK_COMPOSITOR=gamescope' >> ~/.config/punktfunk/host.env
systemctl --user restart punktfunk-host
```
The pin is required: auto-detection reads the live session, finds Cinnamon, and stops with an error
rather than guessing. Set a game to launch with
[`PUNKTFUNK_GAMESCOPE_APP`](/docs/gamescope) or per-session launch commands, then see
[Steam / gamescope](/docs/gamescope) for the rest.
> **Install `punktfunk-gamescope`, not Debian's.** Debian ships **no** `gamescope` package at all,
> and the patched build is what gives the stream HDR, a visible cursor, and the client's real
> refresh rate instead of a hardcoded 60 Hz.
If you want to stream the **desktop** from an LMDE box, the answer today is to log into a GNOME or
Sway session instead — Debian 13 ships GNOME 48.7 and sway 1.10, both above the
[floors](/docs/requirements). (Debian 13's KDE is KWin **6.3.6**, below the 6.5.6 floor, so Plasma
is not an option there yet.)
### Linux Mint 22.x cannot host yet
**On Linux Mint 22.x — the current mainstream release, and every version until Mint 23 in December
2026 — there is no working configuration.** `punktfunk-host` will install, which makes this easy to
miss, but nothing on the box can produce a stream:
- **Cinnamon** cannot host a virtual display (above).
- **gamescope is not available and cannot be made available.** Ubuntu 24.04 packages no gamescope,
and the patched `punktfunk-gamescope` cannot run there either: 24.04 is short of what the build
needs on *five* libraries — wayland 1.22.0 (needs ≥ 1.23.1), libinput 1.25 (≥ 1.26), libavif
1.0.4 (≥ 1.2.1), pixman 0.42 (≥ 0.44), and no `libdisplay-info2` or `libxcb-errors0` at all.
- **Switching desktop does not rescue it.** Ubuntu 24.04 ships KWin **5.27** (floor 6.5.6) and GNOME
Shell **46** (floor 48). Only `sway` 1.9 is even a candidate, and that means giving up Cinnamon.
If you want to run a host on Mint hardware today, use **LMDE 7** — it is the same desktop on a
Debian 13 base, where gamescope works. Otherwise wait for **Mint 23** (Ubuntu 26.04 base), where
both the patched gamescope and the newer compositors are available.
## Configure your desktop
How the host creates its virtual display and injects input depends on your desktop, not your distro:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Hyprland](/docs/hyprland)
- [Sway / wlroots](/docs/sway)
Then bring up [The Web Console](/docs/web-console) to arm pairing and connect your first
[client](/docs/clients). To run the host at boot — including fully **headless** — see
[Running as a Service](/docs/running-as-a-service).
## Next steps
- **Keep it current** — [Updating the Host](/docs/updating).
- **Remove it again** — [Uninstalling](/docs/uninstall).
- **Something not working?** — [Troubleshooting](/docs/troubleshooting).
- **Build from source** (Debian 12, or tracking `main`) — the
[Ubuntu appendix](/docs/ubuntu#appendix--build-from-source) applies unchanged; Debian 13's
`libavcodec-dev` is new enough to build against.
+77
View File
@@ -0,0 +1,77 @@
---
title: Debian
description: Install the Punktfunk host on Debian 13 or newer with apt — including LMDE 7.
---
For **Debian 13 ("trixie") or newer**, and **LMDE 7**. Debian 12 is too old (glibc 2.36, the
package needs 2.39) — [build from source](/docs/build-from-source) there, or upgrade.
> **Desktop matters more than distro here.** Debian 13's GNOME (48) and Sway (1.10) can host; its
> KDE (KWin 6.3) is below the floor, and a Cinnamon desktop (Linux Mint, LMDE) can only host through
> gamescope. [What each desktop can do, and what Linux Mint 22 can't](/docs/requirements#cinnamon-linux-mint-and-lmde).
## 1. GPU driver
- **NVIDIA:** the driver lives in `non-free`, which a default install doesn't enable. Enable it,
install, reboot:
```sh
sudo sed -i 's/^Components: .*/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources
sudo apt update && sudo apt install nvidia-driver firmware-misc-nonfree
```
If `nvidia-smi` can't talk to the driver afterwards, Secure Boot is in the way — see
[Troubleshooting](/docs/troubleshooting#nvidia-smi-says-it-cant-communicate-with-the-driver).
- **AMD / Intel:** nothing to install — Mesa's Vulkan and VAAPI drivers are already there (Intel's
`intel-media-va-driver` is in `non-free`; the host package recommends it).
## 2. Install the host
The repo is public and signed — the `debian` in the URL is the package format, it's the same repo
Ubuntu uses. The browser console, `punktfunk-web`, comes along automatically:
<Install platform="debian" />
Updates ride along with `sudo apt upgrade`; restart the host afterwards
(`systemctl --user restart punktfunk-host`) — or let the [console do it](/docs/updating).
## 3. Let it use your controllers
Join the `input` group (virtual gamepads go through `/dev/uinput`), then **log out and back in**:
```sh
sudo usermod -aG input "$USER"
```
Want the **virtual Steam Deck controller** (paddles, trackpads, gyro), or does this PC boot into Steam
**Gaming Mode**? Also join the `punktfunk` group —
[what it gates and why it's separate](/docs/gamescope#nobara-and-other-autologin-display-managers).
## 4. Start it
From a terminal inside your desktop session:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
```
Debian ships no firewall enabled, so there is nothing to open. If you run one, the package installed
profiles for ufw and firewalld — [Ports & firewall](/docs/ports).
**Cinnamon (LMDE 7):** the host can't stream the Cinnamon desktop itself; it streams games through
gamescope instead. Add `sudo apt install punktfunk-gamescope` and put
`PUNKTFUNK_COMPOSITOR=gamescope` in `~/.config/punktfunk/host.env`, then restart the host —
[Steam / gamescope](/docs/gamescope) takes it from there.
**That's the install.** Continue with the [Quick Start from step 3](/docs/quickstart#3-open-the-web-console)
— open the console, pair a client, stream.
## When you want more
- `punktfunk-host detect-conflicts` tells you if Sunshine or Apollo is also running;
[Troubleshooting](/docs/troubleshooting) starts from the symptom.
- Your desktop's particulars — [GNOME](/docs/gnome), [Sway](/docs/sway), [gamescope](/docs/gamescope).
- Stream with nobody logged in — [Running as a service](/docs/running-as-a-service).
- The Linux **client** isn't packaged for Debian (it needs a newer glibc) — use the
[Flatpak](/docs/install-client#linux-desktop-flatpak) to stream *to* a Debian box.
- Track `main` instead of releases — [Release channels](/docs/channels).
-240
View File
@@ -1,240 +0,0 @@
---
title: Fedora
description: Install the Punktfunk host on Fedora from the RPM registry.
---
Install a Punktfunk host on **Fedora** from the self-hosted RPM registry. The host installs as an
RPM-managed systemd **`--user`** service and updates with `dnf upgrade` like the rest of your
system — no building required. It works with either **KDE Plasma** or **GNOME**; the
desktop-specific setup (which compositor captures, headless sessions, quirks) lives on the
[desktop configure pages](#5-configure-your-desktop). Host encode is **NVENC on NVIDIA**; on
**AMD/Intel** HEVC and AV1 go through **Vulkan Video**, with **VAAPI** for H.264 and as the fallback
(`PUNKTFUNK_ENCODER=auto` picks per GPU).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
Install is two parts: **GPU driver****host RPM**. Then open the firewall and point the host at
your desktop from the [desktop configure pages](#5-configure-your-desktop).
## 1. NVIDIA driver (RPM Fusion akmod)
Enable RPM Fusion (free + nonfree), then install the akmod driver + CUDA. RPM Fusion's nonfree
NVIDIA repo is sometimes pre-enabled on some spins; the full free/nonfree repos below are still
needed (they carry the NVENC ffmpeg in the next step).
```sh
sudo dnf install \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
sudo dnf install akmod-nvidia xorg-x11-drv-nvidia-cuda
```
**NVENC ffmpeg.** Fedora ships `ffmpeg-free`, which is built **without** NVENC — the host can't
encode with it. Swap to RPM Fusion's ffmpeg:
```sh
sudo dnf install --allowerasing ffmpeg ffmpeg-libs
ffmpeg -hide_banner -encoders | grep nvenc # expect hevc_nvenc / av1_nvenc / h264_nvenc
```
**Secure Boot.** If `mokutil --sb-state` says *enabled*, the akmod module is signed with a
locally-generated key that must be enrolled once:
```sh
sudo akmods --force # build + sign the module
sudo mokutil --import /etc/pki/akmods/certs/public_key.der # set a one-time password
sudo reboot
```
On the next boot a blue **MOK Manager** screen appears **on the machine's console** (not over
SSH): *Enroll MOK → Continue → Yes → (the password) → Reboot*. Then verify:
```sh
nvidia-smi # driver loads
ffmpeg -hide_banner -encoders | grep nvenc
```
(Or disable Secure Boot in firmware to skip the MOK step — fine for a dedicated test box.)
**AMD / Intel.** No akmod needed — the Mesa stack carries both encode paths. HEVC and AV1 go through
**Vulkan Video** by default (the Mesa Vulkan driver, present on any normal Fedora desktop), and
**VAAPI** is the H.264 path and the fallback. Install the freeworld VAAPI drivers for full codec
support (`mesa-va-drivers-freeworld` for AMD from RPM Fusion, `intel-media-driver` for Intel); on a
desktop these are usually already present.
## 2. Install the host (RPM)
The host is published to the self-hosted Gitea RPM registry, in a per-release group (an RPM is
soname-coupled to its base, so each Fedora release gets its own group). Pick the one matching your
release — `rpm -E %fedora` prints the number you're on:
- **Fedora 44** → `fedora-44`
- **Fedora 43** → `bazzite` — that group is a plain Fedora 43 build of the same `punktfunk` package,
so it's the right one for a regular Fedora 43 box too
Put your group in the `baseurl` below, then add the repo and install:
```sh
sudo tee /etc/yum.repos.d/punktfunk.repo >/dev/null <<'REPO'
[punktfunk]
name=punktfunk
# The group for your release: fedora-44 on Fedora 44, bazzite on Fedora 43.
baseurl=https://git.unom.io/api/packages/unom/rpm/fedora-44
enabled=1
# Packages are GPG-signed (gpgcheck=1) AND the repo metadata is Gitea-signed (repo_gpgcheck=1).
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key
https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk
REPO
sudo dnf install punktfunk
sudo usermod -aG input "$USER" # /dev/uinput access for virtual gamepads (re-login to apply)
```
Also join `punktfunk` if **either** applies — you want the **virtual Steam Deck controller**
(paddles, trackpads, gyro — it reaches games as a real USB pad, which is why Steam Input adopts
it), or this box autologins into Steam **Gaming Mode** (Nobara and friends) and you want the host
to take that session over at the client's resolution:
```sh
sudo usermod -aG punktfunk "$USER" # usbip/vhci + display-manager takeover (re-login to apply)
```
That is a second group on purpose: it grants write access to the usbip `attach` file, which
materialises an arbitrary emulated USB device, so it stays off the `input` group everyone is
routinely told to join. Join it only on a machine you trust. Skip it on a plain desktop host and
the pad simply arrives as an ordinary Xbox 360 controller; skip it on a Gaming Mode box and the
takeover silently degrades to mirroring the box's own screen — see
[gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
Updates later are just `sudo dnf upgrade punktfunk`, followed by
`systemctl --user restart punktfunk-host` so the running host picks up the new binary. The package
ships the systemd user units, the udev rule, the UDP socket-buffer sysctl tuning, and example
configs.
The group you picked above is the **stable** channel. For the latest `main` build, point `baseurl` at
`fedora-44-canary` (or `bazzite-canary`) instead — see [Release Channels](/docs/channels). Updating
in general, including the opt-in one-click button in the web console, is covered in
[Updating the Host](/docs/updating).
> `fedora-44` and `bazzite` are the only stable groups published, so on Fedora 42 or older — or on a
> release newer than 44 — there's nothing matching yet. Build one with the same toolchain CI uses —
> `docker build --build-arg FEDORA_VERSION=NN -f ci/fedora-rpm.Dockerfile -t pf-rpm ci` then run
> `packaging/rpm/build-rpm.sh` inside it — or build from source (appendix below).
## 3. Check it installed
Before moving on, confirm the binary is there and nothing else is competing for the same job:
```sh
punktfunk-host --version # the binary is on PATH
punktfunk-host detect-conflicts # exits 1 if Sunshine/Apollo is also installed
```
If `detect-conflicts` reports another streaming host, remove it before going further — two hosts on
one machine is the most common reason a clean install never streams. See
[Troubleshooting → another streaming host is installed](/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed).
Once you've enabled the service on your desktop page below, these are how you watch it:
```sh
systemctl --user status punktfunk-host # active
journalctl --user -u punktfunk-host -f # watch a client connect
```
## 4. Open the firewall
Fedora runs **firewalld** by default and the package never edits your firewall, so the host stays
unreachable until you allow it. The RPM installs the service definitions — enable them once.
The packaged unit runs `serve --gamestream` — the RPM installs it as it ships and only rewrites the
binary path — so a host you enabled with `systemctl --user enable --now punktfunk-host` serves
**both** the native `punktfunk/1` plane and stock [Moonlight](/docs/moonlight) clients, and needs
**both** services:
```sh
sudo firewall-cmd --reload # load the installed definitions
sudo firewall-cmd --permanent --add-service=punktfunk-native
sudo firewall-cmd --reload
```
Enabled **GameStream/Moonlight compat** (`PUNKTFUNK_GAMESTREAM=1` in `host.env` — see
[What the unit starts](/docs/running-as-a-service#what-the-unit-starts))? Then also:
```sh
sudo firewall-cmd --permanent --add-service=punktfunk-gamestream && sudo firewall-cmd --reload
```
`punktfunk-native` opens UDP 9777 (QUIC control), UDP 5353 (mDNS discovery) and TCP 47990 (the
mgmt/library API — HTTPS + mTLS, read-only off loopback). `punktfunk-gamestream` opens the fixed
Moonlight ports — TCP 47984, 47989 and 48010, UDP 4799848000 — plus the same mDNS. The media
**data plane** uses an ephemeral UDP port the client opens with a hole-punch, so there is nothing
fixed to open for video.
And if you want the web console reachable from another device, open **TCP 47992**:
```sh
sudo firewall-cmd --permanent --add-service=punktfunk-web && sudo firewall-cmd --reload
```
## 5. Configure your desktop
How the host creates its virtual display and injects input depends on your desktop, not your distro.
Continue on the page for the desktop you run — it covers your `host.env`, any compositor quirks, and
starting the host:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Hyprland](/docs/hyprland)
- [Sway / wlroots](/docs/sway)
Enable the browser management console (status, paired devices, arm pairing) — see
[Web Console](/docs/web-console).
For a headless KWin appliance that streams at boot with no graphical login, see
[KDE → Headless session](/docs/kde#headless-session).
Full config reference: [Configuration](/docs/configuration). Service model:
[Running as a Service](/docs/running-as-a-service).
## 6. Connect a client
From any [client](/docs/clients), `--discover` finds the host on the LAN. On first connect, complete
the **PIN pairing** — arm it from the host's [web console](/docs/web-console#arm-pairing), which
displays a 4-digit PIN to type into the client. See [Clients](/docs/clients) and
[Pairing](/docs/pairing).
## Next steps
- **Keep it current** — [Updating the Host](/docs/updating).
- **Remove it again** — [Uninstalling](/docs/uninstall).
- **Something not working?** — [Troubleshooting](/docs/troubleshooting).
## Appendix — build from source
If there's no RPM for your Fedora release and you don't want to build one, compile the host directly
(no clean updates / no packaged units — you wire those up by hand):
```sh
sudo dnf install gcc gcc-c++ make cmake clang clang-devel nasm git pkgconf-pkg-config \
pipewire-devel wayland-devel wayland-protocols-devel libxkbcommon-devel opus-devel \
libdrm-devel mesa-libgbm-devel mesa-libGL-devel mesa-libEGL-devel mesa-libGLES-devel libva-devel \
ffmpeg-devel libei-devel
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release --locked \
--features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host
```
`mesa-libGL-devel` isn't optional — the zero-copy GPU path links `libGL`, and without it the build
fails at the link step with `cannot find -lGL`. The two `--features` are what the packaged builds
use: leave them off and the host has no direct NVENC (NVIDIA) and no Vulkan Video encode
(AMD/Intel), and quietly falls back to the slower libav backends.
Then write `~/.config/punktfunk/host.env` (as in `/usr/share/punktfunk/host.env.kde`, but the host
binary is `target/release/punktfunk-host`) and run it inside your desktop session — for a headless
KWin appliance see [KDE → Headless session](/docs/kde#headless-session).
+86
View File
@@ -0,0 +1,86 @@
---
title: Fedora
description: Install the Punktfunk host on Fedora 43 or newer from the RPM repo — four steps.
---
For **Fedora 43 or newer** (Workstation or KDE). Bazzite and other Fedora Atomic spins have
[their own page](/docs/bazzite).
## 1. GPU driver
- **NVIDIA:** the driver and an NVENC-capable FFmpeg both come from **RPM Fusion** — Fedora's own
`ffmpeg-free` has no NVENC and the host cannot encode with it. Enable RPM Fusion, install, reboot:
```sh
sudo dnf install \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
sudo dnf install akmod-nvidia xorg-x11-drv-nvidia-cuda
sudo dnf install --allowerasing ffmpeg ffmpeg-libs
```
With **Secure Boot** on, the module won't load until you enrol its key — `nvidia-smi` will say it
can't talk to the driver; the fix is in
[Troubleshooting](/docs/troubleshooting#nvidia-smi-says-it-cant-communicate-with-the-driver).
- **AMD / Intel:** nothing to install — Mesa carries Vulkan Video and VAAPI. For full codec coverage
add RPM Fusion's `mesa-va-drivers-freeworld` (AMD) or `intel-media-driver` (Intel).
## 2. Install the host
The RPM repo has one group per Fedora release: **`fedora-44`** on Fedora 44, **`bazzite`** on
Fedora 43 (it's a plain Fedora 43 build of the same package). `rpm -E %fedora` prints your number —
set `baseurl` to match, then install. The browser console, `punktfunk-web`, comes along
automatically:
<Install platform="fedora" />
Updates ride along with `sudo dnf upgrade`; restart the host afterwards
(`systemctl --user restart punktfunk-host`) — or let the [console do it](/docs/updating).
## 3. Let it use your controllers
Join the `input` group (virtual gamepads go through `/dev/uinput`), then **log out and back in**:
```sh
sudo usermod -aG input "$USER"
```
Want the **virtual Steam Deck controller** (paddles, trackpads, gyro), or does this PC boot into Steam
**Gaming Mode** (Nobara and friends)? Also join the `punktfunk` group —
[what it gates and why it's separate](/docs/gamescope#nobara-and-other-autologin-display-managers).
## 4. Start it, open the firewall
From a terminal inside your desktop session:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
```
Fedora runs **firewalld**, and a package never opens ports for you — so until you allow it, no client
can reach the host. The RPM installed the service definitions; enable them once:
```sh
sudo firewall-cmd --reload # load the definitions the package installed
sudo firewall-cmd --permanent --add-service=punktfunk-native --add-service=punktfunk-web
sudo firewall-cmd --reload
```
(`punktfunk-web` is only needed to reach the console from another device. Turning on Moonlight
compat later? Add `punktfunk-gamestream` too — [Ports & firewall](/docs/ports).)
**That's the install.** Continue with the [Quick Start from step 3](/docs/quickstart#3-open-the-web-console)
— open the console, pair a client, stream.
## When you want more
- **No video on NVIDIA?** `ffmpeg-libs` is only a *recommended* dependency — without RPM Fusion's
build, NVENC fails at runtime. Re-run step 1.
[More in Troubleshooting](/docs/troubleshooting#no-video-on-fedora-nvenc-fails-ffmpeg-libs-is-missing).
- `punktfunk-host detect-conflicts` tells you if Sunshine or Apollo is also running;
[Troubleshooting](/docs/troubleshooting) starts from the symptom.
- Your desktop's particulars — [KDE](/docs/kde), [GNOME](/docs/gnome), [gamescope](/docs/gamescope).
- Stream with nobody logged in, or a headless KDE appliance —
[Running as a service](/docs/running-as-a-service), [KDE → Headless session](/docs/kde#headless-session).
- Track `main` instead of releases (`fedora-44-canary` / `bazzite-canary`) —
[Release channels](/docs/channels). No group for your release — [Build from source](/docs/build-from-source).
+53 -54
View File
@@ -3,23 +3,22 @@ title: Your game library
description: How Punktfunk finds your installed games, how to add one by hand, and how to launch a title from a client, from Moonlight, or from the command line.
---
Every Punktfunk host keeps one **game library** — a single list of titles that every surface reads
from. It has two sources: the [plugins](/docs/plugins) you install for the launchers you actually
use, and entries you add by hand in the [web console](/docs/web-console).
A Punktfunk host keeps one **game library** that every surface reads from. It has two sources: the
[plugins](/docs/plugins) you install for the launchers you use, and entries you add by hand in the
[web console](/docs/web-console).
Whichever source a title came from, it looks the same everywhere: a poster, a name, and a stable id
like `steam:570` or `custom:9f2a1c…`. Pick one on a client and the host launches it into the stream.
Whatever its source, a title looks the same everywhere: a poster, a name, and a stable id like
`steam:570` or `custom:9f2a1c…`. Pick one on a client and the host launches it into the stream.
## Where your games come from
**Install a plugin for each launcher you want in the library.** A fresh host holds no games until
you do — go to the console's **Library** page, open **Game sources**, and install the ones you use.
It takes a click each.
you do — on the console's **Library** page, open **Game sources** and install the ones you use, a
click each.
Each plugin reads that launcher's **own local files** on the host. There are no accounts to connect
and no API keys nothing leaves the machine to build the list. A launcher that isn't installed
contributes nothing, so installing a plugin you turn out not to need costs you an empty source and
nothing else.
Each plugin reads that launcher's **own local files** on the host no accounts to connect, no API
keys, nothing leaves the machine to build the list. A launcher that isn't installed contributes
nothing, so an unneeded plugin costs you an empty source and nothing else.
| Plugin | Linux host | Windows host | What it reads |
|---|---|---|---|
@@ -31,31 +30,30 @@ nothing else.
| **Playnite** | — | ✅ | Your Playnite library, whichever stores it aggregates |
| **ROM Manager** | ✅ | ✅ | Your ROM folders, matched against a metadata source |
> Through v0.27.x six of these scanners were built into the host itself and ran whether you wanted
> them or not. From **v0.28.0** they are plugins like any other. If you were already running the
> plugin for a launcher, nothing changes — the ids, art and app ids are identical by design. If you
> were relying on the built-in scanner, install that launcher's plugin once and your grid comes back
> exactly as it was, including anything you had switched off or hidden.
> Through v0.27.x six of these scanners were built into the host and always on; from **v0.28.0**
> they are plugins like any other. Already running the plugin for a launcher? Nothing changes — ids,
> art and app ids are identical by design. Relied on the built-in scanner? Install that launcher's
> plugin once and your grid comes back exactly as it was, including anything you had switched off or
> hidden.
A few things are deliberately left out. Steam's tooling — Proton, the Steam Linux Runtimes, Steamworks
Common Redistributables, SteamVR — is filtered out, so your grid holds games rather than plumbing. A
non-Steam shortcut you have hidden inside Steam stays hidden here too.
Deliberately left out: Steam's tooling — Proton, the Steam Linux Runtimes, Steamworks Common
Redistributables, SteamVR — so your grid holds games rather than plumbing, and any non-Steam
shortcut you have hidden inside Steam.
To see exactly what the host resolved, run [`punktfunk-host library`](/docs/host-cli) on the host: it
prints the whole library as JSON. That answers "does the host see my games?" without involving a
client.
[`punktfunk-host library`](/docs/host-cli), run on the host, prints the whole library as JSON —
"does the host see my games?" without involving a client.
## Turning a source off
The console's **Library** page has a **Game sources** card with one chip per source this host has.
A chip is highlighted when that source is contributing titles; click it to turn the source off.
On the console's **Library** page, the **Game sources** card shows one chip per source this host
has; a highlighted chip is contributing titles, and clicking it turns the source off.
Turning a source off hides its titles from **everywhere at once** — the console grid, every native
client, the Moonlight app list, and launching. Nothing is deleted and the change needs no restart:
the plugin keeps its titles, they simply stop being shown, and turning the source back on brings
them straight back on the next read. (To remove a source's titles for good, uninstall its plugin.)
That hides its titles **everywhere at once** — the console grid, every native client, the Moonlight
app list, and launching — with nothing deleted and no restart: the plugin keeps its titles, and
turning the source back on brings them straight back on the next read. (To remove a source's titles
for good, uninstall its plugin.)
Your hand-added entries are not a source and have no chip — they are always shown.
Hand-added entries are not a source and have no chip — they are always shown.
The choice is stored per host in `library-scanners.json`, next to the rest of the host config
(`~/.config/punktfunk/` on Linux, `%ProgramData%\punktfunk\` on Windows). Only the sources you turned
@@ -64,10 +62,10 @@ The choice is stored per host in `library-scanners.json`, next to the rest of th
## Adding a game by hand
Anything your launchers don't know about — an emulator, a ROM, a DRM-free build, a tool you want on
the couch — goes in by hand. On the console's **Library** page, click **Add custom game**.
the couch — goes in by hand: on the console's **Library** page, click **Add custom game**.
**Title** is the only required field. **Launch command** is the command the host runs for this title;
leave it empty and the entry is a poster the host has nothing to launch from.
**Title** is the only required field. **Launch command** is what the host runs for this title; leave
it empty and the entry is a poster with nothing to launch.
Under **Details (optional)** a title can carry:
@@ -90,8 +88,8 @@ runs, so it is locked down to the host user (0600 on Linux, a SYSTEM+Administrat
treat what you type there as operator-level configuration.
> **Editing replaces the whole entry.** The console form re-sends every field it knows about, so
> nothing you can see is lost. Fields the form has no input for prep/undo steps in particular — are
> **cleared** when you save an entry through the form.
> nothing you can see is lost — but fields the form has no input for (prep/undo steps in particular)
> are **cleared** when you save through the form.
### Cover art
@@ -105,9 +103,9 @@ host can see is fine. A plain Linux path like `/home/me/cover.jpg` is **not** re
Scanned titles need no art. Steam covers come from your local Steam cache, falling back to Steam's
public CDN. On a Windows host, GOG and Xbox covers are the one thing the library looks up over the
network: a background pass asks GOG's and Microsoft's public catalogs for them when the host starts,
and repeats every five minutes for any title it hasn't resolved yet. Neither needs an account or a
key, the answer is cached on the host, and a lookup that fails just leaves a title-only tile.
network: a background pass asks GOG's and Microsoft's public catalogs when the host starts and
repeats every five minutes for any title still unresolved. Neither needs an account or a key, the
answer is cached on the host, and a failed lookup just leaves a title-only tile.
## Games from a plugin
@@ -116,39 +114,40 @@ Manager and Playnite plugins get your collection into the grid, box art and all.
A library plugin can also publish a **launcher tile** — an entry that opens Steam Big Picture,
Heroic, Lutris or Playnite itself rather than a game, so you can install or fix something from the
couch. Clients group those into their own row above your titles, and each one draws its launcher's
logo. A launcher tile you don't want is a switch in that plugin's settings.
couch. Clients group those into their own row above your titles, each drawing its launcher's logo.
A launcher tile you don't want is a switch in that plugin's settings.
Entries a plugin owns are read-only to you. The host refuses a hand edit or a delete of one, because
Entries a plugin owns are read-only to you. The host refuses a hand edit or delete of one, because
the next sync would overwrite it anyway — change the title at its source and let the plugin sync
again. Only the plugin can remove its own entries, and it removes every one of them at once. Your
hand-added entries are never touched by a sync.
The console grid can't tell you which entries those are: a plugin's titles carry the same **Custom**
badge as your own and still show **Edit** and **Delete** on hover. The form and the delete
confirmation open as usual, but the host refuses the change and the entry stays exactly as it was.
badge as your own and still show **Edit** and **Delete** on hover — the form and the delete
confirmation open as usual, but the host refuses the change and the entry stays as it was.
## Launching a game
Whatever the surface, the client sends only an **id**. The host looks that id up in its own library
and runs what it already knows about the title, so a client can never hand the host a command to run.
Whatever the surface, the client sends only an **id**. The host looks it up in its own library and
runs what it already knows about the title, so a client can never hand the host a command to run.
- **Native clients** — the browser needs a **paired** host, and that is the only condition: a paired
host's card offers **Browse library…** (**Browse Library…** on Apple) with nothing to switch on
first. Pick a title and the stream starts with the host launching it. The Apple and Android apps
- **Native clients** — a **paired** host's card offers **Browse library…** (**Browse Library…** on
Apple) with nothing to switch on first; pairing is the only condition. Pick a title and the stream
starts with the host launching it. The Apple and Android apps
keep a **Show game library** switch, on by default, for turning it off. See
[Client settings](/docs/client-settings).
- **Android** — the library lives only in the controller-optimized home, which a TV always uses and a
phone or tablet switches to when a controller is connected. Press **Y** on a saved host, or open its
options and choose **Library**.
- **Steam Deck (Decky)** — the panel is a launcher and browses nothing itself: tap **Open
Punktfunk**, which opens the client's console home, and a paired host's **Library** button is
right there — full-screen covers, gamepad-navigable, and a press starts the stream with the title
launching. See [Steam Deck](/docs/steam-deck).
- **Moonlight** — when the host runs with `--gamestream`, your library appears in Moonlight's app
list beside `Desktop`, with covers served by the host. A title keeps the same app id across host
restarts, so Moonlight's cached tiles stay correct. Titles with no launch recipe are left out.
See [Moonlight](/docs/moonlight).
Punktfunk**, which opens the client's console home, where a paired host's **Library** button is
full-screen covers, gamepad-navigable, and a press starts the stream with the title launching. See
[Steam Deck](/docs/steam-deck).
- **Moonlight** — when the host runs GameStream compat (opt-in: `PUNKTFUNK_GAMESTREAM=1` in
`host.env`, or `serve --gamestream` — see [Moonlight](/docs/moonlight)), your library appears in
Moonlight's app list beside `Desktop`, with covers served by the host. A title keeps the same app id
across host restarts, so Moonlight's cached tiles stay correct. Titles with no launch recipe are
left out.
- **A link** — a [`punktfunk://` link](/docs/profiles-and-links) carries the id in a `launch=`
parameter, so a desktop shortcut, a browser bookmark or a home-automation rule starts the stream
with the title already launching: `punktfunk://connect/couch-pc?launch=steam:570`. On the Apple
+72 -76
View File
@@ -4,23 +4,23 @@ description: How an HDR10 stream is decided end to end — the four things that
---
An HDR session carries a **10-bit BT.2020 PQ (HDR10)** picture from the host's display to your
screen. It is on by default wherever it works, and a session that can't be HDR streams 8-bit BT.709
SDR instead. Which one you get is decided **before the first frame**: the host resolves every gate
below, then tells the client what it is really going to send. Nothing on this page takes effect
mid-stream, so reconnect after changing any of it.
screen. It is on by default wherever it works; otherwise the session streams 8-bit BT.709 SDR. The
host decides **before the first frame** — it resolves every gate below and tells the client what it
will really send — so nothing on this page takes effect mid-stream; reconnect after changing any of
it.
## The chain
Four things must all be true. If your stream is SDR when you expected HDR, one of these is why.
Four things must all be true. If you got SDR when you expected HDR, one of these is why.
1. **The source.** What the host captures must hand it 10-bit PQ pixels. This is the link that fails
most often, and it is entirely a host-side question — see [Per host](#per-host).
2. **The encoder.** The host GPU must encode 10-bit for the codec the session picked. The host
probes this by opening a tiny real encoder once per GPU and codec, and believes the answer.
(PyroWave skips the probe — it has its own rule, below.)
1. **The source.** What the host captures must hand it 10-bit PQ pixels. This link fails most often
and is entirely host-side — see [Per host](#per-host).
2. **The encoder.** The host GPU must encode 10-bit for the session's codec. The host probes by
opening a tiny real encoder once per GPU and codec, and believes the answer. (PyroWave skips the
probe — its own rule is below.)
3. **The codec.** Only HEVC, AV1 and PyroWave have a 10-bit path — see [Codec rules](#codec-rules).
4. **The client.** Your client must advertise 10-bit and HDR (its **HDR** setting), and be able to
present or tone-map PQ.
4. **The client.** It must advertise 10-bit and HDR (its **HDR** setting) and be able to present or
tone-map PQ.
The host allows 10-bit by default (`PUNKTFUNK_10BIT`). It only ever *allows* — the client's setting
is the real per-session switch.
@@ -29,78 +29,75 @@ is the real per-session switch.
### Windows
The Windows host creates a virtual display for your session and **turns HDR on for that display
itself** when the session negotiated 10-bit. You do not have to enable "Use HDR" in Windows Settings
first: Punktfunk enables advanced colour at capture open, waits for it to settle, then composes in
FP16 and encodes P010 BT.2020 PQ. The reverse is enforced too — a session that negotiated **SDR**
forces advanced colour **off** on that display, so a client that asked for 8-bit is never handed PQ.
If enabling it fails, the host logs a loud error and encodes 8-bit anyway: the client was already
told HDR, so that is the one place a Punktfunk label can outrun the picture. The log says so.
Two details worth knowing:
The Windows host **turns HDR on for the session's virtual display itself** when 10-bit was
negotiated — you don't have to enable "Use HDR" in Windows Settings. It enables advanced colour at
capture open, waits for it to settle, then composes in FP16 and encodes P010 BT.2020 PQ. The
reverse holds too: an **SDR** session forces advanced colour **off** on that display, so a client
that asked for 8-bit is never handed PQ. If enabling it fails, the host logs a loud error and encodes
8-bit anyway — the client was already told HDR, so that is the one place a Punktfunk label can
outrun the picture.
- **HDR and 4:4:4 compose on Windows, not on Linux.** A **Windows** host carries both: the capture
path writes full-resolution 10-bit chroma and NVENC encodes HEVC Main 4:4:4 10, so
[full chroma](/docs/client-settings) costs you nothing on an HDR desktop.
[PyroWave](/docs/pyrowave) does the same there, in 16-bit planes. On **Linux** the 4:4:4 route is
8-bit, so a session that negotiates both resolves back down to SDR — full chroma wins. AV1 never
carries 4:4:4 anywhere: Range Extensions are HEVC-only.
[full chroma](/docs/client-settings) costs nothing on an HDR desktop; [PyroWave](/docs/pyrowave)
does the same there, in 16-bit planes. On **Linux** the 4:4:4 route is 8-bit, so a session that
negotiates both resolves back down to SDR — full chroma wins. AV1 never carries 4:4:4 anywhere:
Range Extensions are HEVC-only.
- **Vulkan games need the bundled layer.** NVIDIA and AMD Vulkan drivers refuse to advertise any HDR
colour space for a surface on an indirect (virtual) display, so Vulkan games decide the device
"does not support HDR" — even though the driver happily presents an HDR swapchain there. The host
"does not support HDR" — though the driver happily presents an HDR swapchain there. The host
installer ships an implicit Vulkan layer, `VK_LAYER_PUNKTFUNK_hdr_inject`, that adds those formats
back (installer task **Install the HDR Vulkan layer**, ticked by default). It self-gates on the
monitor's live advanced-colour state, so it does nothing on an SDR session, and it already skips a
built-in list of kernel-anti-cheat titles. `DISABLE_PF_VKHDR=1` in a game's environment switches it
off for that process; `PF_VKHDR_EXCLUDE=foo.exe,bar.exe` skips further executables by name.
D3D11/D3D12 games need none of this.
monitor's live advanced-colour state, so it does nothing on an SDR session, and it skips a built-in
list of kernel-anti-cheat titles. `DISABLE_PF_VKHDR=1` in a game's environment switches it off for
that process; `PF_VKHDR_EXCLUDE=foo.exe,bar.exe` skips further executables by name. D3D11/D3D12
games need none of this.
### Linux + gamescope
A stock gamescope tone-maps its composite down to 8 bits before handing it over, so its capture
output is SDR no matter what the game rendered. Real HDR needs **`punktfunk-gamescope`**, a build
carrying a patch that adds the 10-bit PQ formats to its PipeWire node. It installs beside your system
gamescope rather than replacing it; [HDR on gamescope](/docs/gamescope#hdr-on-gamescope) has the
package for each distro.
Stock gamescope tone-maps its composite down to 8 bits before handing it over, so its capture output
is SDR whatever the game rendered. Real HDR needs **`punktfunk-gamescope`**, a build carrying a patch
that adds the 10-bit PQ formats to its PipeWire node. It installs beside your system gamescope rather
than replacing it; [HDR on gamescope](/docs/gamescope#hdr-on-gamescope) has the package for each
distro.
The host settles two facts before spawning anything: the gamescope binary it will run carries the
Before spawning anything the host settles two facts: the gamescope binary it will run carries the
patch (its `--version` banner contains `+pfhdr`), and this host is the one **starting** the session
rather than attaching to a node someone else started.
**Attach mode is the trap.** The patched build only reaches sessions the host spawns itself —
managed, `PUNKTFUNK_GAMESCOPE_SESSION`, or a bare spawn. A session started by your display manager
runs the distro's own gamescope, which offers neither the 10-bit formats nor the in-node cursor. The
host cannot tell that from the outside unless you pinned `PUNKTFUNK_GAMESCOPE_NODE`: with
runs the distro's own gamescope, which offers neither the 10-bit formats nor the in-node cursor, and
the host cannot tell that from the outside unless you pinned `PUNKTFUNK_GAMESCOPE_NODE`. With
`PUNKTFUNK_GAMESCOPE_ATTACH=1` and the patched build installed it reads the binary, believes HDR is
available, and offers it. The attached session can't answer that negotiation, so the connect fails
available, and offers it; the attached session can't answer that negotiation, so the connect fails
with no picture, the host latches an SDR downgrade for the rest of its life, and the next connect
streams — in SDR.
That combination bites on [Bazzite](/docs/bazzite), where the sysext installs `punktfunk-gamescope`
alongside a stock session gamescope. No template pins attach any more, so the managed default gets
you HDR and the compositor-drawn cursor — but an older template did, and an upgrade never rewrites a
`host.env` you already have, so check yours for `PUNKTFUNK_GAMESCOPE_ATTACH=1` and delete the line.
If you deliberately stay on attach, set `PUNKTFUNK_GAMESCOPE_HDR=0` so the failed attempt never
happens. Staying on attach also leaves the stream with no cursor;
[HDR on gamescope](/docs/gamescope#hdr-on-gamescope) has the fix for that half.
That bites on [Bazzite](/docs/bazzite), where the sysext installs `punktfunk-gamescope` alongside a
stock session gamescope. No template pins attach any more, so the managed default gets you HDR and
the compositor-drawn cursor — but an older template did, and an upgrade never rewrites a `host.env`
you already have: check yours for `PUNKTFUNK_GAMESCOPE_ATTACH=1` and delete the line. If you
deliberately stay on attach, set `PUNKTFUNK_GAMESCOPE_HDR=0` so the failed attempt never happens.
Attach also leaves the stream with no cursor; [HDR on gamescope](/docs/gamescope#hdr-on-gamescope)
has the fix for that half.
SDR content rides the same PQ container — the desktop, the Steam overlay, an SDR game — mapped in at
`PUNKTFUNK_GAMESCOPE_SDR_NITS`, which defaults to **203 nits**. That is BT.2408 reference white, and
it is the level our clients decode against, so the two ends agree out of the box. gamescope's own
default is 400, nearly a stop brighter; hosts that let it float showed a glaring, over-saturated
Steam UI and washed-out HDR game content on the same stream. Move the knob if you want a brighter or
dimmer desktop, but be aware that moving it re-opens that gap.
SDR content — the desktop, the Steam overlay, an SDR game — rides the same PQ container, mapped in
at `PUNKTFUNK_GAMESCOPE_SDR_NITS`, default **203 nits**. That is BT.2408 reference white and the
level our clients decode against, so the two ends agree out of the box. gamescope's own default is
400, nearly a stop brighter; hosts that let it float showed a glaring, over-saturated Steam UI and
washed-out HDR game content on the same stream. Moving the knob re-opens that gap.
### Linux + GNOME
A Punktfunk host serves [two protocols](/docs/how-it-works#two-protocols): its own `punktfunk/1`,
which the Linux, Windows, Apple and Android apps speak, and GameStream, which
[Moonlight](/docs/moonlight) speaks. GNOME HDR is available on the GameStream side only.
A host serves [two protocols](/docs/how-it-works#two-protocols): its own `punktfunk/1` (the Linux,
Windows, Apple and Android apps) and GameStream ([Moonlight](/docs/moonlight)). GNOME HDR is
available on the GameStream side only.
GNOME 50 added HDR screencast for **real monitors** only, so this route mirrors a monitor instead of
creating a virtual display: set `PUNKTFUNK_VIDEO_SOURCE=portal`, put the monitor in HDR mode in
**Settings → Displays**, and connect an HDR-capable client. `PUNKTFUNK_CAPTURE_MONITOR=<connector>`
pins which head, and when it is set the host checks *that* monitor's colour mode rather than asking
pins which head; when it is set the host checks *that* monitor's colour mode rather than asking
whether any monitor is in HDR. If none is, the session degrades to 8-bit SDR and says so in the log.
A Punktfunk app connecting to a GNOME host over `punktfunk/1` gets SDR. On that protocol the only
@@ -109,10 +106,10 @@ Linux HDR source is the gamescope virtual output.
### Linux virtual displays on KWin, Mutter and wlroots
**These are SDR.** Mutter's `RecordVirtual` streams and the KWin and wlroots virtual outputs are
8-bit upstream, so there is nothing for the host to capture in 10 bits — no setting changes this.
Streaming a *physical* monitor with the [Streamed screen](/docs/virtual-displays) setting is SDR to
a Punktfunk app too, HDR panel or not; the GNOME/GameStream route above is the only Linux monitor
mirror that can be HDR.
8-bit upstream, so there is nothing to capture in 10 bits — no setting changes this. Streaming a
*physical* monitor with the [Streamed screen](/docs/virtual-displays) setting is SDR to a Punktfunk
app too, HDR panel or not; the GNOME/GameStream route above is the only Linux monitor mirror that can
be HDR.
## Per client
@@ -125,12 +122,12 @@ mirror that can be HDR.
| **Moonlight** | Its own HDR toggle, which appears only when the host advertises a 10-bit codec | — |
The Linux and Windows clients are deliberately looser: they advertise HDR whenever the setting is on
and let the presenter sort out the display side — HDR10 swapchain where the compositor offers one,
and let the presenter sort out the display — HDR10 swapchain where the compositor offers one,
tone-mapped to SDR where it doesn't. The stats overlay says which happened: `HDR` versus `HDR→SDR`.
One exception: frames from **software decode** never take the HDR10 swapchain, whatever the surface
offers. On a client with no hardware HEVC decode an HDR stream is therefore presented on the SDR
swapchain without a tone-map, which looks washed out. Turn the client's HDR setting off there. The
offers, so a client with no hardware HEVC decode presents an HDR stream on the SDR swapchain without
a tone-map — washed out. Turn the client's HDR setting off there. The
[Steam Deck plugin](/docs/steam-deck) streams through this same client.
## Codec rules
@@ -139,15 +136,15 @@ swapchain without a tone-map, which looks washed out. Turn the client's HDR sett
- **AV1** — 10-bit, where the GPU encodes it. Advertised separately from HEVC, so a box that does
one and not the other tells the truth about each.
- **H.264** — never. High10 is not an encode mode on the hardware Punktfunk targets, so negotiation
never even asks. Pinning H.264 in your client settings pins the session to SDR.
never asks. Pinning H.264 in your client settings pins the session to SDR.
- **[PyroWave](/docs/pyrowave)** — carries HDR in 16-bit planes, but **only from a Windows host**.
The Linux PyroWave capture path has no HDR colour conversion, so a Linux-hosted PyroWave session
is SDR. Use HEVC or AV1 for HDR from Linux.
One more rule if you also use full chroma: a **Linux** host encodes 4:4:4 at 8 bits, so a session
that negotiates both resolves back down to SDR before the stream starts — on Linux, 4:4:4 wins. A
**Windows** host has no such trade: it carries HDR and full chroma at once. Full chroma is off until
you turn it on, so this only bites if you did.
With full chroma: a **Linux** host encodes 4:4:4 at 8 bits, so a session that negotiates both
resolves back down to SDR before the stream starts — on Linux, 4:4:4 wins. A **Windows** host
carries HDR and full chroma at once. Full chroma is off until you turn it on, so this only bites if
you did.
## Check it
@@ -170,8 +167,8 @@ set -a; . ~/.config/punktfunk/host.env; set +a
punktfunk-host hdr-probe
```
There is no `hdr-probe` on Windows. What Windows has instead is a GPU colour self-test for the
capture conversion, which needs no display or session:
There is no `hdr-probe` on Windows. Windows has instead a GPU colour self-test for the capture
conversion, which needs no display or session:
```powershell
punktfunk-host hdr-p010-selftest 1920x1080 nvidia
@@ -193,13 +190,12 @@ Host, in [`host.env`](/docs/configuration):
|---|---|---|
| `PUNKTFUNK_10BIT` | **on** | Allow 10-bit (HEVC Main10 / AV1 10-bit) at all. `0`, `false`, `off` or `no` forces every session to 8-bit SDR. |
| `PUNKTFUNK_GAMESCOPE_HDR` | **on** | Allow HDR on the gamescope backend. It only decides whether HDR is *attempted* — a host without `punktfunk-gamescope` stays SDR either way. `0` is the escape hatch that puts the gamescope backend back on the old SDR path, spawn flags included. |
| `PUNKTFUNK_GAMESCOPE_SDR_NITS` | gamescope's own (400) | How bright SDR content is inside the PQ container of an HDR gamescope session. |
| `PUNKTFUNK_VIDEO_SOURCE=portal` | unset | Required for the GNOME 50+ monitor-mirror route. GameStream/Moonlight only — it has no effect on `punktfunk/1` sessions. |
| `PUNKTFUNK_GAMESCOPE_SDR_NITS` | **203** | How bright SDR content is inside the PQ container of an HDR gamescope session. |
| `PUNKTFUNK_VIDEO_SOURCE=portal` | unset | Required for the GNOME 50+ monitor-mirror route. GameStream/Moonlight only — no effect on `punktfunk/1` sessions. |
Client: one toggle, in Settings under **Quality** with
[the rest of the video settings](/docs/client-settings#video) — **10-bit HDR** on the Linux, macOS,
iOS, iPadOS and tvOS apps, **HDR (10-bit, BT.2020 PQ)** on Windows, **HDR** on Android. It is **on
by default** on all of them. Turning it off means "never send me 10-bit", and the host then never
upgrades the session. Like the other video settings it can be set per
[profile](/docs/profiles-and-links), so a Work profile can prefer 4:4:4 while a Couch profile
prefers HDR.
by default** on all of them. Off means "never send me 10-bit", and the host then never upgrades the
session. Like the other video settings it can be set per [profile](/docs/profiles-and-links), so a
Work profile can prefer 4:4:4 while a Couch profile prefers HDR.
+2 -3
View File
@@ -150,9 +150,8 @@ systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f
```
This unit runs `serve --gamestream`, so it serves stock [Moonlight](/docs/moonlight) clients as well
as the native ones. For a native-only host, see
[What the unit starts](/docs/running-as-a-service#what-the-unit-starts).
This unit runs the secure native-only host; to serve stock [Moonlight](/docs/moonlight) clients as
well, see [What the unit starts](/docs/running-as-a-service#what-the-unit-starts).
## Bring up the console and pair
+2 -1
View File
@@ -36,7 +36,8 @@ stream and links out to the detail as you need it. The rest of these are for whe
<Card title="Quick Start" href="/docs/quickstart" description="From nothing to streaming: set up a host and connect your first client." />
<Card title="How It Works" href="/docs/how-it-works" description="The ideas behind Punktfunk in a few minutes — virtual displays, the two protocols, pairing." />
<Card title="Support Matrix" href="/docs/support-matrix" description="What works where — every host desktop, GPU and client app, each cell read out of the code that decides it." />
<Card title="Install the Host" href="/docs/install" description="Add the repo and install the package — Ubuntu, Debian, Fedora, Arch, Bazzite, SteamOS, NixOS, or Windows." />
<Card title="Install the Host" href="/docs/install" description="One page per system — Ubuntu, Debian, Fedora, Arch, Bazzite, SteamOS, NixOS, or Windows — with the install command and nothing else." />
<Card title="Switching from Sunshine" href="/docs/switching-from-sunshine" description="Run Punktfunk next to Sunshine, Apollo or Vibeshine while you try it, then migrate — what maps to what." />
<Card title="Connect a Client" href="/docs/clients" description="Stream with the native app for your device — macOS, Linux, Windows, Android — or any Moonlight client." />
<Card title="Your Game Library" href="/docs/game-library" description="The host finds your installed games by itself — browse a paired host and launch a title straight into the stream." />
<Card title="API Reference" href="/api" description="Interactive OpenAPI reference for the host's management REST API — status, devices, pairing, library." />
+55 -62
View File
@@ -5,15 +5,15 @@ description: The in-stream keyboard shortcuts that give your mouse back, the two
A stream takes your mouse and keyboard the moment you click into it. This page starts with how to
get them back, then covers driving the host with a mouse, a touchscreen and a pen. The rows that
pick these modes sit in your client's **Input** settings; the toggles that share that page with
them are in [Client settings](/docs/client-settings#input).
pick these modes sit in your client's **Input** settings; the toggles that share that page are in
[Client settings](/docs/client-settings#input).
## Getting your input back
On the Linux and Windows clients the stream runs in its own session window. Input is **captured**
when the stream starts and whenever you click the video: your local cursor disappears and keys go to
the host instead of your desktop. In the default mouse mode the pointer is also locked to the
window — see [Mouse modes](#mouse-modes) below.
the host. In the default mouse mode the pointer is also locked to the window — see
[Mouse modes](#mouse-modes).
| Shortcut | What it does |
|---|---|
@@ -31,45 +31,41 @@ Click the stream to capture input · Ctrl+Alt+Shift+Q releases · Ctrl+Alt+Shift
Ctrl+Alt+Shift+D disconnects · Ctrl+Alt+Shift+S stats
```
With a controller in use the same hint names the controller chord instead of the mouse-mode and
stats entries. The full list is always available without a stream running — see below.
With a controller in use the hint names the controller chord instead of the mouse-mode and stats
entries. The full list is always available without a stream running — see below.
### Muting your microphone
**Ctrl+Alt+Shift+V** stops sending your microphone to the host, and pressing it again resumes.
The uplink itself keeps running underneath, so unmuting is instant rather than a second of the
device warming back up.
**Ctrl+Alt+Shift+V** stops sending your microphone to the host; pressing it again resumes. The
uplink keeps running underneath, so unmuting is instant.
While you are muted a **Microphone muted** badge sits in the top-right corner of the stream. It
is deliberately separate from the [stats overlay](/docs/stats): it shows even with stats off,
because "am I still muted?" is a question you ask ten minutes later.
While muted, a **Microphone muted** badge sits in the top-right corner of the stream — separate
from the [stats overlay](/docs/stats), so it shows even with stats off.
The mute lasts for that stream only — the next session starts unmuted, and nothing is written to
your settings. If the stream isn't sending a microphone at all (**Stream microphone** off in
[client settings](/docs/client-settings#audio)) the shortcut does nothing and no badge appears,
rather than pretending to mute something.
[client settings](/docs/client-settings#audio)) the shortcut does nothing and no badge appears.
This is on the **Linux and Windows** clients — including a Steam Deck stream, which is the Linux
client, so an attached keyboard gets the chord. The Apple and Android clients have no mute shortcut
yet; turn **Stream microphone** off in their settings instead.
**Linux and Windows** clients only — a Steam Deck stream is the Linux client, so an attached
keyboard gets the chord. The Apple and Android clients have no mute shortcut yet; turn **Stream
microphone** off in their settings instead.
Alt-Tabbing away releases input on its own and takes it back when you return. A release you asked
for with the chord stays released until you opt back in. Either way, keys and buttons you were
holding are released on the host, so nothing sticks down.
You can look the shortcuts up again without a stream running: the Linux client has **Keyboard
Shortcuts** in its main menu, and the Windows client has a **Shortcuts** screen reached from its
host list. Both list the microphone mute; the in-stream hint over the video doesn't, to keep it
to one readable line.
Without a stream running, the Linux client lists the shortcuts under **Keyboard Shortcuts** in its
main menu, and the Windows client on a **Shortcuts** screen reached from its host list. Both list the
microphone mute; the in-stream hint over the video doesn't, to stay one readable line.
### On the other clients
- **macOS** honours the release, mouse-mode, disconnect and stats combos, written
**⌃⌥⇧Q / M / D / S** — but not the microphone mute. **⌘⎋** also toggles capture,
**⌃⌘F** toggles fullscreen, and **⌃⌥⇧C** starts or stops [clipboard sharing](/docs/clipboard). The
**Stream** menu lists them all except the mouse-mode combo, which works but has no menu item.
Every *other* chord goes to the host while input is captured — ⌘Q reaches the host's compositor
rather than quitting the app — unless you turn **Capture system shortcuts** off in
**⌃⌥⇧Q / M / D / S** — but not the microphone mute. **⌘⎋** also toggles capture, **⌃⌘F** toggles
fullscreen, and **⌃⌥⇧C** starts or stops [clipboard sharing](/docs/clipboard). The **Stream** menu
lists them all except the mouse-mode combo, which works but has no menu item. Every *other*
chord goes to the host while input is captured — ⌘Q reaches the host's compositor rather than
quitting the app — unless you turn **Capture system shortcuts** off in
[client settings](/docs/client-settings#input). ⌘⎋ and ⌃⌘F are held back either way, so there is
always a way out.
- **iPhone and iPad** with a hardware keyboard: **⌃⌥⇧Q** releases input while it is captured, and
@@ -91,32 +87,30 @@ Every client reserves one controller chord: **L1 + R1 + Start + Select** (LB + R
an Xbox pad), held on any connected pad.
- **Linux, Windows** — a press releases captured input, and leaves fullscreen if you didn't start
fullscreen. Keep it held about 1.5 seconds and it disconnects.
fullscreen. Hold about 1.5 seconds and it disconnects.
- **Steam Deck** — a press releases captured input only. The Decky plugin always launches the client
fullscreen, and a stream that started fullscreen stays that way. Holding disconnects, as above.
- **macOS, iPhone/iPad, Apple TV** — holding it about 1.5 seconds disconnects. There is no
quick-press step.
- **Android** — holding it about a second disconnects. A quick press does nothing; the moment the
chord completes a **Hold to quit…** cue appears so you know it registered.
- **macOS, iPhone/iPad, Apple TV** — holding about 1.5 seconds disconnects. There is no quick-press
step.
- **Android** — holding about a second disconnects. A quick press does nothing; the moment the chord
completes a **Hold to quit…** cue appears so you know it registered.
The chord is read off the pads a client forwards, so turning
[**Forward controllers**](/docs/client-settings#input) off takes it away on **Linux and Windows**
there the client stops opening the controller at all, which is the point of the setting. Use
there the client stops opening the controller at all. Use
**Ctrl+Alt+Shift+D** or the client's own UI to leave instead. The Apple and Android apps keep
watching for the chord either way.
### Statistics with a controller
The **Apple** apps reserve a second chord: **Select + X**, which cycles the
[stats overlay](/docs/stats) one level each time you complete it. It is for the moment your hands
are on a controller and the usual routes aren't — no keyboard for **⌃⌥⇧S**, no free screen for the
three-finger tap — and on **Apple TV** it is the only way there with a pad. X is deliberately none
of the four leave-chord buttons, so reaching for one chord never trips the other. Both buttons
still reach the game; only the overlay changes locally.
[stats overlay](/docs/stats) one level each time you complete it. For when you have a pad but no
keyboard for **⌃⌥⇧S** and no free screen for the three-finger tap; on **Apple TV** it is the only
way there with a pad. X is deliberately none of the four leave-chord buttons, so reaching for one
chord never trips the other. Both buttons still reach the game; only the overlay changes locally.
On the **Siri Remote**, **hold Play/Pause** for about half a second instead. A quick tap of that
button is still a right click — the click is simply sent when you let go, so the hold has
something to be.
On the **Siri Remote**, **hold Play/Pause** for about half a second instead. A quick tap is still a
right click, sent when you let go.
### The guide button (Xbox / PS / Steam) and Quick Access
@@ -126,30 +120,30 @@ devices want that button for themselves, so every client also carries a gesture
everywhere:
**Hold Select (Back / View) on its own for about a third of a second.** The host sees its guide
button go down, and it stays down for as long as you hold — so keeping it held reads as a long
press on the host, which is how SteamOS opens the **Quick Access Menu** for a regular pad. A quick
tap of Select still reaches the game, delivered when you let go (a beat late). Select pressed as
part of a combo — including the leave chord above — passes through untouched.
button held down for as long as you hold — a long press, which is how SteamOS opens the **Quick
Access Menu** for a regular pad. A quick tap of Select still reaches the game, delivered when you
let go (a beat late). Select pressed as part of a combo — including the leave chord above — passes
through untouched.
What the raw button does, per client:
- **Linux & Windows desktop, macOS, Android** — the guide press is forwarded to the host. If
Steam Big Picture or the Xbox Game Bar is also watching for it *on the device in your hands*,
both may react — that's a local setting on that device, not something the stream can suppress.
- **Linux & Windows desktop, macOS, Android** — the guide press is forwarded to the host. If Steam
Big Picture or the Xbox Game Bar is also watching for it *on the device in your hands*, both may
react — that's a local setting on that device, not something the stream can suppress.
- **Steam Deck / Gaming Mode** — the **Steam** and **`…`** buttons stay with the Deck by default:
SteamOS always opens its own menus for them, so forwarding the raw press as well opened BOTH
menus at once, the Deck's on top of the stream. Reach the host's menus with **hold-Select**, or
with the Punktfunk panel's **Host menus** buttons ([Steam Deck page](/docs/steam-deck)). The
old behavior is one setting away: **Steam / guide button → Send to host**.
SteamOS always opens its own menus for them, so forwarding the raw press as well opened BOTH menus
at once, the Deck's on top of the stream. Reach the host's menus with **hold-Select**, or with the
Punktfunk panel's **Host menus** buttons ([Steam Deck page](/docs/steam-deck)). The old behavior
is one setting away: **Steam / guide button → Send to host**.
- **iPhone / iPad** — iOS reserves the Home press for its own Game Overlay, so hold-Select is the
reliable route to the host's overlay. On iOS 27 or later you can also hand the button to the
app yourself, in the system's per-controller Home-button setting.
reliable route to the host's overlay. On iOS 27 or later you can also hand the button to the app
yourself, in the system's per-controller Home-button setting.
- **Apple TV** — tvOS never delivers the Home press to apps; hold-Select is the only route.
Both halves are [settings](/docs/client-settings#input), per profile like everything else:
**Steam / guide button** (Automatic / Send to host / This device) and **Hold Select for guide**
(Automatic / On / Off). Automatic picks the behavior above for each platform — the gesture stays
off where the raw button already works, so games that use a *held* Select keep it.
(Automatic / On / Off). Automatic picks the behavior above for each platform — the gesture stays off
where the raw button already works, so games that use a *held* Select keep it.
## Mouse modes
@@ -159,14 +153,13 @@ There are two, and they are a per-client setting called **Mouse input**:
cursor you see is the host's. This is what mouse-look in a game needs. The session window also
grabs the keyboard here, so Alt+Tab and the Windows key (Super on Linux) reach the host rather than
your own desktop — on macOS that is the ⌘ chords, ⌘Q included, with ⌘⎋ kept back as the way out.
Turn **Capture system shortcuts** off in
[client settings](/docs/client-settings#input) to keep them local.
Turn **Capture system shortcuts** off in [client settings](/docs/client-settings#input) to keep
them local.
- **Desktop (absolute)** — the pointer is not locked. It moves in and out of the stream freely and
its position is sent as an absolute point — what you want for remote desktop work. Your local
cursor is hidden over the stream; the one you see there is the host's.
**Capture is the default** on the Linux, Windows and macOS clients. **Android defaults to Desktop**
— a phone or TV is more often driven by touch or a pad than by a locked mouse.
**Capture is the default** on the Linux, Windows and macOS clients. **Android defaults to Desktop**.
Switch live with **Ctrl+Alt+Shift+M** (**⌃⌥⇧M** on macOS), whether input is captured or not. On
Android, Ctrl+Alt+Shift+Q flips the capture instead. The picker is macOS-only among the Apple apps;
@@ -217,7 +210,7 @@ The trackpad and pointer models are unaffected by all of this: they send ordinar
A stylus is not treated as a finger. Punktfunk carries **position, tip pressure, tilt angle and tilt
direction, barrel roll, hover distance, the eraser end, and two barrel buttons** on their own input
plane, so drawing and handwriting behave the way they do locally.
plane.
**Clients that send pen input:**
@@ -241,8 +234,8 @@ The Linux, Windows, macOS and Apple TV clients do not send stylus input.
tilt, rotation, the barrel button and the eraser. This needs **Windows 10 1809 or newer**.
**Before it can work on Linux**, the host needs access to `/dev/uinput` — the same `input` group step
the virtual gamepads need, covered under [After installing](/docs/install#after-installing). Without
it the host never offers pen at all.
the virtual gamepads need, step 3 of your [install guide](/docs/install). Without it the host never
offers pen at all.
**If the host is too old, or pen is switched off**, nothing breaks: the client keeps folding the
stylus into its ordinary touch or pointer path. You can still draw — just without pressure and tilt.
+49 -53
View File
@@ -5,9 +5,9 @@ description: Install the Punktfunk client for the device you're streaming to —
This page is the **install path for each client device**. For what each client *is* and which to
pick, see [Clients](/docs/clients); to install the **host**, see [Install the Host](/docs/install).
Whichever client you install, the first connection needs a one-time [pairing](/docs/pairing). If the
app installs but your host doesn't appear in its list, start at [Troubleshooting → The host isn't
found on the network](/docs/troubleshooting#the-host-isnt-found-on-the-network).
Every client needs a one-time [pairing](/docs/pairing) on its first connection. If the app installs
but your host doesn't appear in its list, start at [Troubleshooting → The host isn't found on the
network](/docs/troubleshooting#the-host-isnt-found-on-the-network).
Already installed? Skip to [Keeping a client up to date](#keeping-a-client-up-to-date) or
[Removing a client](#removing-a-client).
@@ -31,9 +31,8 @@ Already installed? Skip to [Keeping a client up to date](#keeping-a-client-up-to
## Linux desktop (Flatpak)
The **recommended** path on any Flatpak distro — install once, then `flatpak update` tracks new
builds. One command adds the signed `unom` remote, pulls the GNOME runtime from Flathub
automatically, and installs the client:
The **recommended** path on any Flatpak distro. One command adds the signed `unom` remote, pulls the
GNOME runtime from Flathub automatically, and installs the client:
```sh
flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref
@@ -47,10 +46,9 @@ touches the *system* scope and silently skips it):
flatpak update # or: flatpak update --user io.unom.Punktfunk
```
Prefer your native package manager? The client also ships as real packages (add the repo once —
see the linked guide — then it tracks updates with your normal `apt upgrade` / `dnf upgrade` /
`pacman -Syu`; a *layered* Atomic install needs one extra step, under
[Keeping a client up to date](#keeping-a-client-up-to-date)):
Prefer your native package manager? Add the repo once (see the linked guide); updates then ride your
normal `apt upgrade` / `dnf upgrade` / `pacman -Syu` (a *layered* Atomic install needs
[one extra step](#keeping-a-client-up-to-date)):
| Distro | Install | Guide |
|--------|---------|-------|
@@ -60,9 +58,9 @@ see the linked guide — then it tracks updates with your normal `apt upgrade` /
| **Arch** | `sudo pacman -Syu punktfunk-client` (signed binary repo) | [Arch Linux](/docs/arch) |
> **The client `.deb` needs SDL3 and GTK4 ≥ 4.20**, which Ubuntu 24.04 LTS doesn't ship, so
> `apt install punktfunk-client` can't satisfy its dependencies there. On 24.04 (or any older
> distro) use the **Flatpak above** — it carries its own libadwaita and SDL3. This limit is the
> *client's* alone: the host `.deb` is built separately and installs on 24.04 LTS through 26.04.
> `apt install punktfunk-client` can't resolve there. On 24.04 (or any older distro) use the
> **Flatpak above** — it carries its own libadwaita and SDL3. The limit is the *client's* alone: the
> host `.deb` is built separately and installs on 24.04 LTS through 26.04.
Then launch it, pick your host from the list, and stream. Every one of these packages — Flatpak
included — also installs the headless **`punktfunk`** command for scripts:
@@ -78,18 +76,17 @@ list: [Clients → the `punktfunk` CLI](/docs/clients#scripting-the-punktfunk-cl
## Steam Deck
Most Deck users want **Gaming Mode**: install the **[Decky plugin](/docs/steam-deck)** and a
**Punktfunk** panel lands in the Quick Access Menu, so you can find a host, get let in (a PIN, or a
request the host's operator approves), and stream **without dropping to the desktop**. Everything
else — settings, the game library, adding a host by address — is one tap away in the client's own
gamepad UI. Follow the **[Steam Deck (Decky) guide](/docs/steam-deck)** — it walks through Decky
Most Deck users want **Gaming Mode**: the **[Decky plugin](/docs/steam-deck)** puts a **Punktfunk**
panel in the Quick Access Menu find a host, get let in (a PIN, or a request the host's operator
approves), and stream **without dropping to the desktop**; settings, the game library and adding a
host by address are one tap away in the client's own gamepad UI. That guide walks through Decky
Loader, the plugin, and the one-time client install.
> The plugin doesn't decode video itself — it drives whichever `punktfunk-client` is installed on
> the Deck. The Flatpak below is the tested default; a native package or a sysext works too. If your
> client isn't one the plugin can update for you (a sysext, a nix profile, a source build), the panel
> shows you the update command instead of an **Update** button. The Gaming Mode panel comes from the
> plugin, so a client on its own won't add it. The Decky guide covers installing both, so start there.
> plugin, so a client on its own won't add it; the Decky guide covers installing both.
For **Desktop Mode** (or to add the client to Game Mode as a non-Steam app yourself), install the
Flatpak exactly as [above](#linux-desktop-flatpak) — it carries its own libadwaita + SDL3 and
@@ -104,7 +101,7 @@ See [packaging/flatpak](https://git.unom.io/unom/punktfunk/src/branch/main/packa
## Windows
The Windows client ships as a **signed MSIX** in the package registry, signed with a publicly
trusted certificate — so there is nothing to import and nothing to trust by hand. Download, install.
trusted certificate — nothing to import or trust by hand.
1. Download the package. Each channel keeps one fixed URL, so this line always fetches the current
build — in PowerShell:
@@ -115,7 +112,7 @@ trusted certificate — so there is nothing to import and nothing to trust by ha
Swap `_x64` for `_arm64` on an Arm device, and `latest` for `canary` to track `main`. The same
file is attached to every [release](https://git.unom.io/unom/punktfunk/releases), and every
build is also kept under its own version on the
build is kept under its own version on the
[packages page](https://git.unom.io/unom/-/packages) (generic group, `punktfunk-client-windows`).
2. Install it:
@@ -129,8 +126,8 @@ trusted certificate — so there is nothing to import and nothing to trust by ha
(the MSIX depends on `Microsoft.WindowsAppRuntime.2`), then re-run `Add-AppxPackage`.
Install from a signed-in desktop session. Over a remote, non-interactive session (SSH, an RMM
tool) `Add-AppxPackage` can fail with `0x80070005` when the Windows App Runtime it depends on is
in use and Windows can't restart the apps holding it.
tool) `Add-AppxPackage` can fail with `0x80070005` when the Windows App Runtime is in use and
Windows can't restart the apps holding it.
3. Launch **Punktfunk** from the Start menu and pick your host. The package also adds a second
entry, **Punktfunk Console** — the same client as a controller-driven fullscreen interface for a
@@ -142,11 +139,10 @@ trusted certificate — so there is nothing to import and nothing to trust by ha
## macOS
Download the notarized disk image from the [releases page](https://git.unom.io/unom/punktfunk/releases)
`Punktfunk-<version>.dmg`. It's Developer-ID signed, notarized, and stapled, so Gatekeeper opens
it without warnings:
Download `Punktfunk-<version>.dmg` from the [releases page](https://git.unom.io/unom/punktfunk/releases).
It's Developer-ID signed, notarized, and stapled, so Gatekeeper opens it without warnings:
1. Open `Punktfunk-<version>.dmg` and drag **Punktfunk** to **Applications**.
1. Open the `.dmg` and drag **Punktfunk** to **Applications**.
2. Launch it, pick your host from *On this network*, and [pair](/docs/pairing).
The Mac app is also in the [TestFlight beta](https://testflight.apple.com/join/Qr7uSemk); the DMG
@@ -159,19 +155,19 @@ Mac. Install Apple's [TestFlight](https://apps.apple.com/app/testflight/id899247
**[Join the Punktfunk beta on TestFlight →](https://testflight.apple.com/join/Qr7uSemk)**
Open the app, and your hosts appear automatically under *On this network*.
Open the app; your hosts appear automatically under *On this network*.
## Android
The Android client (phone + Android TV — one package, the TV layout is the same app in leanback
mode) is published on **Google Play**. It's a public listing: no invite, no tester list.
The Android client (phone + Android TV — one package; the TV layout is the same app in leanback
mode) is on **Google Play** as a public listing: no invite, no tester list.
**[Get Punktfunk on Google Play →](https://play.google.com/store/apps/details?id=io.unom.punktfunk)**
Install it, open the app, and pick your host.
**Prefer not to go through Play?** The signed APK is published publicly on every build, so you can
sideload it instead no Play account needed:
**Prefer not to go through Play?** The signed APK is published publicly on every build — sideload it
instead, no Play account needed:
```text
https://git.unom.io/api/packages/unom/generic/punktfunk-android/latest/punktfunk-android.apk
@@ -195,13 +191,13 @@ LG's webOS doesn't allow apps outside the LG Content Store without sideloading,
**Developer Mode** and the **Homebrew Channel** once:
1. Enable Developer Mode on the TV and install the [Homebrew Channel](https://www.webosbrew.org/) —
follow its [install guide](https://www.webosbrew.org/guide/getting-started.html) if you haven't
done this before.
its [install guide](https://www.webosbrew.org/guide/getting-started.html) covers it if you
haven't done this before.
2. Grab the latest `.ipk` from the
[pf-webos releases page](https://github.com/dyptan-io/pf-webos/releases/latest).
3. Install it: either sideload with `ares-install` / the project's `task deploy TV_HOST=root@<tv-ip>`
(see the repo's README), or side-copy the `.ipk` onto the TV and install it from the Homebrew
Channel's app.
3. Install it: sideload with `ares-install` / the project's `task deploy TV_HOST=root@<tv-ip>` (see
the repo's README), or copy the `.ipk` onto the TV and install it from the Homebrew Channel's
app.
4. Launch **Punktfunk** from the TV's launcher, discover your host over LAN (or add it by IP), and
[pair](/docs/pairing) with a PIN.
@@ -230,9 +226,9 @@ but keeping them close is the least surprising. (Updating the **host** is its ow
| **LG webOS** | install the newer `.ipk` the same way you installed the first one |
**Fedora Atomic, if you layered the client.** `rpm-ostree upgrade` upgrades the *base image* and
only re-resolves layered packages when that base actually changes — so on a base that sits still it
keeps reporting no updates while a newer `punktfunk-client` waits in the repo. Force a re-resolve of
just that layer, in one transaction, then reboot to activate it:
only re-resolves layered packages when that base changes — on a base that sits still it keeps
reporting no updates while a newer `punktfunk-client` waits in the repo. Force a re-resolve of just
that layer, in one transaction, then reboot to activate it:
```sh
sudo rpm-ostree refresh-md --force
@@ -240,27 +236,27 @@ sudo rpm-ostree update --uninstall punktfunk-client --install punktfunk-client
systemctl reboot
```
The client's own updater below runs exactly that dance for you, if you'd rather not remember it. (A
layered **host** has the same trap — [Updating](/docs/updating) covers it.)
The client's own updater below runs exactly that for you. (A layered **host** has the same trap —
[Updating](/docs/updating) covers it.)
**Windows, coming from 0.28.1 or earlier — uninstall first.** Those builds were signed with our own
self-signed certificate. The move to a publicly trusted one changes the package's *publisher*, and an
MSIX's identity is its name **plus** its publisher — so Windows treats the new package as a different
app rather than an update, and installing it leaves you with two **Punktfunk** entries. Remove the
old one first, then install the new `.msix` as above:
app, not an update, and installing it leaves you with two **Punktfunk** entries. Remove the old one
first, then install the new `.msix` as above:
```powershell
Get-AppxPackage *Punktfunk* | Remove-AppxPackage
```
This is one-time; releases after that upgrade in place. Note that a packaged app's settings live
*inside* its package, so removing the old one also removes this client's identity and its paired
hosts — expect to [pair](/docs/pairing) again once. Nothing on the host side is affected.
This is one-time; releases after that upgrade in place. A packaged app's settings live *inside* its
package, so removing the old one also removes this client's identity and its paired hosts — expect
to [pair](/docs/pairing) again once. Nothing on the host side is affected.
### The Linux client can update itself
The native Linux client checks its own channel and can apply the update in place, so you don't have
to remember which package manager installed it:
The native Linux client checks its own channel and can apply the update in place, whichever package
manager installed it:
```sh
punktfunk-client --check-update # prints installed vs available for this box's channel
@@ -272,16 +268,16 @@ available, and **1** when it couldn't tell (offline, or the check is disabled)
deliberately not "up to date". Add `--json` to either for machine-readable output.
Applying an update needs root, so it's **opt-in**: join the `punktfunk-update` group once, and a
packaged root helper does the install (the same group and the same grant as the host's one-click
updating, described in [Updating](/docs/updating)).
packaged root helper does the install (the same group and grant as the host's one-click updating,
described in [Updating](/docs/updating)).
```sh
sudo usermod -aG punktfunk-update $USER
```
Membership is re-read on every check, so there's no need to log out and back in. Without it,
Membership is re-read on every check, so no need to log out and back in. Without it,
`--check-update` prints the opt-in line and the plain package-manager command instead. On a Flatpak
install `--apply-update` isn't used at all — the client tells you to run `flatpak update`.
install `--apply-update` isn't used — the client tells you to run `flatpak update`.
## Removing a client
-276
View File
@@ -1,276 +0,0 @@
---
title: Install the Host
description: Install the Punktfunk host — on Linux from its package registry, or on Windows from a signed installer.
---
On Linux, the package registries are the real distribution channel. Pick your distro, add the repo, and
install with your native package manager. Each row links to the full per-distro guide (add the repo,
first-run steps, the web console) — those are the source of truth, so this page doesn't duplicate them.
On **Windows**, the host ships as a signed installer instead — see [Windows](#windows).
> **First, read [Security & Safe Use](/docs/security).** A streaming host is remote control of the
> machine. It's built for trusted local networks — don't expose it to the internet, and be thoughtful
> about which machine you host on (especially on Windows).
## Pick your distro
| Distro | Package manager | One-command happy path | Guide |
|--------|-----------------|------------------------|-------|
| **Ubuntu 26.04+** ¹ | apt | `sudo apt install punktfunk-host` | [Ubuntu](/docs/ubuntu) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Debian 13+** (incl. LMDE) | apt | `sudo apt install punktfunk-host` | [Debian](/docs/debian) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Bazzite / Fedora Atomic** | systemd-sysext | `curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh && sudo bash punktfunk-sysext.sh install` (no layering, no reboot) | [Bazzite](/docs/bazzite) · [packaging/bazzite](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/bazzite/README.md) |
| **Fedora (dnf)** | dnf / rpm-ostree | `sudo dnf install punktfunk` | [Fedora](/docs/fedora) · [packaging/rpm](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/rpm/README.md) |
| **Arch** | pacman | `sudo pacman -Syu punktfunk-host` (binary repo — always a full `-Syu`, never `-Sy`) | [Arch Linux](/docs/arch) · [packaging/arch](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md) |
| **SteamOS (host)** | on-device script | clone the repo, then `bash ~/punktfunk/scripts/steamdeck/install.sh` (builds on-device) | [SteamOS (Host)](/docs/steamos-host) |
| **NixOS / Nix** | nix flake | `nix run git+https://git.unom.io/unom/punktfunk#punktfunk-host -- serve --gamestream` | [NixOS](#nixos) · [packaging/nix](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/nix/README.md) |
> ¹ **Ubuntu 24.04 LTS installs the package but cannot host.** It ships no compositor that meets
> the [version floors](/docs/requirements) — KWin 5.27 against 6.5.6, GNOME Shell 46 against 48 —
> and no `gamescope`. Use 26.04 or newer. This is also why
> [Linux Mint 22.x cannot host](/docs/debian#linux-mint-22x-cannot-host-yet); LMDE 7 (Debian 13)
> can.
Each registry is public — no auth, you just trust the repo's signing key. Adding the repo is a
one-time step covered in the linked guide; after that, normal `apt upgrade` / `dnf upgrade` /
`pacman -Syu` (or `sudo punktfunk-sysext update` on Bazzite) tracks new builds. On **NixOS** you add
the flake as an input and enable its module rather than adding a package repo — but do add the
[binary cache](#nixos), or every build compiles from source.
> **Stable vs canary.** The repos in the per-distro guides are the **stable** channel — it only
> moves when a `vX.Y.Z` release is cut. For the latest `main` build (fast, possibly broken), point
> at the **canary** channel instead (`canary` apt distribution / `*-canary` rpm group). See
> [Release Channels](/docs/channels).
## Windows
Punktfunk also runs as a native host on **Windows 11 22H2+ (x64)**, shipped as a signed
installer — see [Windows Host](/docs/windows-host) for what it includes and its limitations.
For hardware encode you need a GPU — NVIDIA (NVENC), AMD (AMF), or Intel (QSV); there's a software
fallback without one. More detail — including the CLI `punktfunk-host service install` path — is in
[Running as a Service → Windows](/docs/running-as-a-service#windows).
### winget (recommended)
In an **admin** PowerShell, register the Punktfunk source once, then install:
```powershell
winget source add -n punktfunk https://winget.punktfunk.unom.io -t Microsoft.Rest
winget install unom.PunktfunkHost
```
Later, `winget upgrade unom.PunktfunkHost` updates it in place. Add `--interactive` to get the full
wizard instead (the optional task checkboxes, the web-console password page). winget carries
**stable** releases only — canary builds are not published there.
### Manual download
Download `punktfunk-host-setup-<ver>.exe` and run it elevated. The full procedure — where to get it,
everything the installer puts on the machine, its optional tasks, the console password, and the
`/VERYSILENT` unattended switch — lives on one page: [Windows Host → Install](/docs/windows-host#install).
This is also the path for **canary** builds, which winget doesn't carry — see
[Release Channels](/docs/channels) for that download.
> **About signing.** The installer is signed with a publicly trusted certificate, so Windows shows
> the publisher by name at the UAC prompt — there is no Unknown Publisher warning and nothing to
> import. The winget route is no different: it downloads and runs that same installer.
>
> SmartScreen is a separate mechanism that builds reputation per publisher, so shortly after a new
> signing certificate starts being used it can still show *"Windows protected your PC"* on the first
> downloads — **More info → Run anyway**. It settles as installs accumulate.
>
> The bundled **drivers** are a separate matter — they carry their own certificate, and the installer
> imports that one for you. [Windows Host](/docs/windows-host#about-the-signatures) has the detail.
>
> Releases **0.28.1 and earlier** were signed with our own self-signed certificate, and older docs
> told you to import it. Nothing needs it any more: if you imported
> `punktfunk-host-windows_<ver>.cer` back then, you can remove it from `Cert:\LocalMachine\Root` and
> `Cert:\LocalMachine\TrustedPublisher` (look for the certificate issued to **unom**, thumbprint
> `CD1EFDEEEC9743AFC38F56C5AF30C5A3009BE941`).
## NixOS
The repo's `flake.nix` is a supported install path: it builds `punktfunk-host`, `punktfunk-client`,
`punktfunk-web` and `punktfunk-scripting`, and ships a NixOS module. **`x86_64-linux` only**, and
NixOS **24.11 or newer**.
**Add the binary cache first.** Without it, a build compiles the whole Rust workspace *and*
gamescope from source — about an hour. With it you get prebuilt binaries:
```nix
nix.settings = {
substituters = [ "https://nix.unom.io" ];
trusted-public-keys = [ "punktfunk-cache-1:yhOJmHxzg6tzXpxSFzlYn6Pc6r0jHprsWqt8MZC654o=" ]; # curl https://nix.unom.io/punktfunk-cache.pub
};
```
Off NixOS, put the same two values in `/etc/nix/nix.conf` as `extra-substituters` /
`extra-trusted-public-keys`. One caveat worth knowing before you copy a flake snippet from
elsewhere: setting `inputs.punktfunk.inputs.nixpkgs.follows = "nixpkgs"` changes every store path
and so misses the cache entirely — details in
[packaging/nix](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/nix/README.md#binary-cache-do-this-before-your-first-build).
You can run it straight from the flake without NixOS (on other distros, wrap it in
[nixGL](https://github.com/nix-community/nixGL) so the GPU drivers resolve):
```sh
nix run git+https://git.unom.io/unom/punktfunk#punktfunk-host -- serve --gamestream
```
On NixOS, add the flake as an input, add `punktfunk.nixosModules.default` to your system's modules,
and enable the host:
```nix
services.punktfunk.host = {
enable = true;
users = [ "alice" ]; # added to the `input` group, for virtual gamepads
openFirewall = true;
desktopSession = true; # on a machine you log into — see below
settings = { RUST_LOG = "info"; }; # these become host.env
};
```
The module does declaratively what the deb/RPM scriptlets do — the systemd user service, udev rules,
kernel modules, sysctl tuning, the firewall ports and `input` group membership — and brings in the
web console alongside the host. Because `settings` writes the environment file for you, skip the
`host.env` step in [After installing](#after-installing).
**Set `desktopSession = true` on any machine somebody logs into.** It ties the host to
`graphical-session.target`, so restarting Plasma or GNOME restarts the host with it. Without it the
host keeps running against a compositor that no longer exists — still listening, still answering,
and failing at capture on every session after that. Leave it off for the headless appliance route
(a pinned compositor or a gamescope box), which may never reach that target. Same reasoning, and
the same caveats for Sway and Hyprland, as [Restart the host with your
desktop](/docs/running-as-a-service#restart-the-host-with-your-desktop).
The host and console user services are defined but not started (set `autoStart = true` for an
appliance), so from your graphical session enable them:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
```
The plugin runner needs no such step — like the deb and RPM, the module starts it for you, because
the game-library scanners ship as plugins. Opt out with
`services.punktfunk.scripting.autoStart = false;`.
The full option reference (client, console and scripting options, GPU driver notes, headless
appliance setup) is in
[packaging/nix](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/nix/README.md). To
update, run `nix flake update punktfunk` in your flake directory, then `sudo nixos-rebuild switch`.
## What the packages are
- **`punktfunk-host`** — the streaming host. Install this on your Linux gaming machine.
- **`punktfunk-web`** — the browser management console (pairing + status). Recommended alongside the
host. On apt and RPM the host package *recommends* it, so your package manager pulls it in by
default, and the Bazzite sysext image already contains it. On **Arch** it's an optional
dependency, so name it yourself: `sudo pacman -Syu punktfunk-web`.
- **`punktfunk-client`** — the GTK4 desktop client, for streaming *to* a Linux box (shipped via
apt / RPM / Arch, and as a Flatpak). On a **Steam Deck** take the Flatpak instead — SteamOS's
`/usr` is read-only, so the native package isn't the path there:
```sh
flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref
```
For Gaming Mode, add the [Decky plugin](/docs/steam-deck) on top of it. Full client instructions
for every device: [Install a Client](/docs/install-client).
- **`punktfunk-scripting`** — the plugin/script runner, behind [plugins](/docs/plugins) and
[automation](/docs/automation). The game-library scanners ship as plugins, so a host without the
runner can come up with an empty library — which is why **apt, dnf, the Bazzite sysext and the
NixOS module all start it for you**. On **Arch** and source installs it is not started, so enable
it yourself:
```sh
systemctl --user enable --now punktfunk-scripting
```
To opt out where it *is* on: `systemctl --user mask punktfunk-scripting` (`mask`, not `disable`
a plain disable cannot remove a symlink that lives in `/etc` or `/usr`), or on NixOS
`services.punktfunk.scripting.autoStart = false;`.
## After installing
These three steps are for the **Linux packages**. On Windows the installer does the equivalent for
you; on NixOS the module does steps 1 and 2, and [NixOS](#nixos) above has the units to enable.
1. Add yourself to the `input` group — virtual gamepads and [pen
input](/docs/input#pen-and-stylus) both need `/dev/uinput` — then re-login. The exact
command differs per distro — see your guide (`usermod -aG input "$USER"`, or `ujust
add-user-to-input-group` on Bazzite).
Also join `punktfunk``sudo usermod -aG punktfunk "$USER"`, then log out and back in — if
**either** of these is true: you want the **virtual Steam Deck controller** (paddles,
trackpads, gyro), or this box autologins into Steam **Gaming Mode** and you want the host to
take that session over at your client's resolution
([gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers)). Your package created
that group at install time and left it empty. It gates the usbip nodes the pad attaches through
*and* the helper that stops the display manager for a takeover, and it is separate from `input`
on purpose, because writing those nodes can present arbitrary emulated USB hardware — so join it
only on a machine you trust. On a plain desktop host that streams no Gaming Mode, skipping it
costs you nothing but that one pad type.
2. Put your `host.env` in place, then start the host. Every Linux package ships a systemd **user**
unit, so you don't run the host by hand — but that unit reads `~/.config/punktfunk/host.env` and
won't start until the file exists. Each package ships a template to copy; your distro and desktop
guides say which one to pick (on Bazzite it's `host.env.bazzite`):
```sh
mkdir -p ~/.config/punktfunk
# /usr/share/punktfunk/ on Fedora/Arch/Bazzite, /usr/share/punktfunk-host/ on Ubuntu
cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env
systemctl --user enable --now punktfunk-host
```
The shipped unit runs `serve --gamestream` — the native `punktfunk/1` plane **plus** the
GameStream/Moonlight-compatible planes, so stock [Moonlight](/docs/moonlight) clients work out of
the box. Those extra planes are only appropriate on a trusted LAN. To run native-only, drop the
flag with a drop-in (`systemctl --user edit punktfunk-host`):
```ini
[Service]
ExecStart=
ExecStart=/usr/bin/punktfunk-host serve
```
The empty `ExecStart=` is required — without it systemd adds a second command instead of
replacing the first — and the binary path has to match your install (`systemctl --user cat
punktfunk-host` shows it; the distro packages use `/usr/bin`). Save the drop-in, then
`systemctl --user restart punktfunk-host`. For what each mode starts, see
[Host CLI → `serve`](/docs/host-cli#serve).
3. Enable the web console:
```sh
systemctl --user enable --now punktfunk-web
```
Then open `https://<host-ip>:47992`. Reading its [login password](/docs/web-console#login-password)
and [arming PIN pairing](/docs/web-console#arm-pairing) are covered in
[The Web Console](/docs/web-console).
### Configure your desktop
How the virtual display and input work depends on your desktop — see [KDE](/docs/kde),
[GNOME](/docs/gnome), [Steam / gamescope](/docs/gamescope), [Hyprland](/docs/hyprland), or
[Sway](/docs/sway) for the compositor-specific setup.
From there, follow the [Quick Start](/docs/quickstart) to pair your first client. To run the host
automatically at boot, see [Running as a Service](/docs/running-as-a-service). If something doesn't
come up, [Troubleshooting](/docs/troubleshooting) starts from the symptom.
## Updating and removing
The web console's **Host → Updates** card tells you when a newer host is out and shows the exact
command for the way you installed — the full list, plus one-click updating and how to turn the check
off, is on [Updating the Host](/docs/updating).
To take it back off, see [Uninstall](/docs/uninstall) — it covers every install method and what is
deliberately left behind (your `~/.config/punktfunk` — identity certificate, paired devices, console
password — survives package removal).
## Building from source
If no package exists for your platform, you can build from source — see the repository README. Source
builds are a fallback; the registries are the supported path.
+39
View File
@@ -0,0 +1,39 @@
---
title: Install the Host
description: Pick the system you stream from — each guide is one page with the install command, the group to join, and how to start the host.
---
import { Cards, Card } from 'fumadocs-ui/components/card'
The **host** is the program on the PC you stream *from*. It installs from Punktfunk's own package
repositories on Linux and from a signed installer on Windows — pick your system:
<Cards>
<Card title="Ubuntu 26.04+" href="/docs/ubuntu" description="apt — one repo line, then sudo apt install punktfunk-host." />
<Card title="Debian 13+ (and LMDE)" href="/docs/debian" description="The same apt repo as Ubuntu." />
<Card title="Fedora 43+" href="/docs/fedora" description="dnf — add the RPM repo, then sudo dnf install punktfunk." />
<Card title="Arch / CachyOS" href="/docs/arch" description="pacman — a signed binary repo, then pacman -Syu punktfunk-host." />
<Card title="Bazzite / Fedora Atomic" href="/docs/bazzite" description="A systemd sysext — no layering, no reboot." />
<Card title="SteamOS (Steam Deck as host)" href="/docs/steamos-host" description="One script, built on-device." />
<Card title="NixOS" href="/docs/nixos" description="A flake input and a NixOS module, with a binary cache." />
<Card title="Windows 11" href="/docs/windows-host" description="winget or a signed installer — a service, a virtual display driver, done." />
</Cards>
Streaming **to** a device instead? That's a client — see [Install a Client](/docs/install-client).
## Good to know
- **Read [Security & Safe Use](/docs/security) once.** A streaming host is remote control of the
machine: keep it on your LAN or a VPN, and keep pairing on (the default).
- **Already running Sunshine, Apollo or Vibeshine?** They can coexist, with one port moved — see
[Switching from Sunshine](/docs/switching-from-sunshine) before you install.
- **What gets installed.** `punktfunk-host` is the host; `punktfunk-web` is the browser console
(pairing, status, library — apt and RPM pull it in automatically, on Arch you name it yourself,
the Bazzite image contains it); `punktfunk-scripting` is the plugin runner that brings your
game launchers into the [library](/docs/game-library) (started for you everywhere except Arch and
source builds — `systemctl --user enable --now punktfunk-scripting` there).
- **Stable by default.** Every guide points at the **stable** channel, which moves on `vX.Y.Z`
releases. The latest `main` build is the **canary** channel — [Release Channels](/docs/channels).
- **Updating and removing** are one page each: [Updating the Host](/docs/updating) (the console
tells you when a newer build is out) and [Uninstall](/docs/uninstall).
- **No package for your platform?** [Build from source](/docs/build-from-source).
+2 -3
View File
@@ -95,9 +95,8 @@ systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f # watch it come up and print its identity fingerprint
```
This unit runs `serve --gamestream`, so it serves stock [Moonlight](/docs/moonlight) clients as well
as the native ones. For a native-only host, see
[What the unit starts](/docs/running-as-a-service#what-the-unit-starts).
This unit runs the secure native-only host; to serve stock [Moonlight](/docs/moonlight) clients as
well, see [What the unit starts](/docs/running-as-a-service#what-the-unit-starts).
A desktop-login host should also follow your session's lifetime, or restarting Plasma leaves the host
wired to a compositor that is gone — it keeps answering, and every session after that fails at
+36 -33
View File
@@ -1,13 +1,9 @@
{
"title": "Documentation",
"pages": [
"---Get started---",
"index",
"how-it-works",
"security",
"quickstart",
"support-matrix",
"---Install the host---",
"requirements",
"install",
"ubuntu",
"debian",
@@ -15,48 +11,55 @@
"arch",
"bazzite",
"steamos-host",
"nixos",
"windows-host",
"install-client",
"pairing",
"---Guides---",
"web-console",
"clients",
"steam-deck",
"moonlight",
"game-library",
"plugins",
"virtual-displays",
"hdr",
"pyrowave",
"input",
"controller-audio",
"clipboard",
"client-settings",
"profiles-and-links",
"wake-on-lan",
"access-levels",
"automation",
"running-as-a-service",
"updating",
"uninstall",
"---Configure your desktop---",
"---Switching from Sunshine---",
"switching-from-sunshine",
"---Reference---",
"requirements",
"support-matrix",
"how-it-works",
"security",
"configuration",
"host-cli",
"ports",
"kde",
"gnome",
"gamescope",
"hyprland",
"sway",
"running-as-a-service",
"virtual-displays",
"hdr",
"pyrowave",
"host-cli",
"automation",
"plugins",
"---Connecting---",
"clients",
"install-client",
"pairing",
"access-levels",
"steam-deck",
"moonlight",
"---Using Punktfunk---",
"input",
"controller-audio",
"client-settings",
"profiles-and-links",
"game-library",
"clipboard",
"wake-on-lan",
"build-from-source",
"channels",
"roadmap",
"[API Reference](/api)",
"---Troubleshooting---",
"troubleshooting",
"echo",
"stats",
"forgot-password",
"---Project---",
"roadmap",
"channels",
"---Reference---",
"[API Reference](/api)"
"status"
]
}
+9 -9
View File
@@ -4,8 +4,8 @@ description: Stream from a Punktfunk host using any Moonlight client.
---
Punktfunk speaks the **GameStream** protocol, so [Moonlight](https://moonlight-stream.org/) connects
to it like it would to any GameStream host — no punktfunk-specific app needed. It's a great option for
a browser, a smart TV, or any device without a native client.
to it like any GameStream host — no punktfunk-specific app needed. A good option for a browser, a
smart TV, or any device without a native client.
> Many platforms also have a **native Punktfunk client** with lower latency and built-in
> discovery/pairing — including **Windows** and **Android** (phone and Android TV). See
@@ -41,15 +41,15 @@ the secure native-only host):
```
(Bare `serve` is the secure native-only default and stock Moonlight clients can't connect to it; the
native plane is always on, and `--gamestream` adds the Moonlight-compat surface.) GameStream pairs over
plain HTTP and its legacy control encryption is weaker than the native plane's, so only enable it on a
**trusted LAN**. See [Running as a Service](/docs/running-as-a-service) for the bundled unit. The host
advertises itself on the network, so Moonlight usually finds it on its own.
native plane is always on, and `--gamestream` adds the Moonlight-compat surface.) GameStream pairs
over plain HTTP and its legacy control encryption is weaker than the native plane's, so only enable
it on a **trusted LAN**. See [Running as a Service](/docs/running-as-a-service) for the bundled
unit. The host advertises itself on the network, so Moonlight usually finds it on its own.
## 2. Add the host in Moonlight
Open Moonlight. Your host should appear automatically on the same network. If it doesn't, use **Add
Host manually** and enter the host machine's IP address.
Open Moonlight. Your host should appear automatically on the same network. If not, use **Add Host
manually** and enter the host machine's IP address.
Still nothing? Two causes account for almost all of it:
@@ -74,7 +74,7 @@ Both are covered in more detail in [Troubleshooting](/docs/troubleshooting).
## 3. Pair
Moonlight's PIN is typed in on the **host** side, so you need the host's
[web console](/docs/web-console) running — it's the only UI where a Moonlight PIN can be entered.
[web console](/docs/web-console) running — the only UI where a Moonlight PIN can be entered.
1. Open the console at `https://<host-ip>:47992` and go to **Pairing**.
2. In Moonlight, select the host and choose **Pair** — it shows a 4-digit PIN.
+76
View File
@@ -0,0 +1,76 @@
---
title: NixOS
description: Install the Punktfunk host on NixOS from the repo's flake — add the binary cache, import the module, enable the host.
---
The repo's `flake.nix` builds `punktfunk-host`, `punktfunk-client`, `punktfunk-web` and
`punktfunk-scripting` and ships a NixOS module. **`x86_64-linux` only**, NixOS **24.11 or newer**.
## 1. Add the binary cache
Without it a build compiles the whole Rust workspace *and* gamescope from source — about an hour.
With it you get prebuilt binaries:
```nix
nix.settings = {
substituters = [ "https://nix.unom.io" ];
trusted-public-keys = [ "punktfunk-cache-1:yhOJmHxzg6tzXpxSFzlYn6Pc6r0jHprsWqt8MZC654o=" ]; # curl https://nix.unom.io/punktfunk-cache.pub
};
```
Off NixOS, put the same two values in `/etc/nix/nix.conf` as `extra-substituters` /
`extra-trusted-public-keys`. One trap: setting `inputs.punktfunk.inputs.nixpkgs.follows = "nixpkgs"`
changes every store path and misses the cache entirely —
[packaging/nix](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/nix/README.md#binary-cache-do-this-before-your-first-build).
## 2. Import the module and enable the host
<Install platform="nixos" />
A typical host block:
```nix
services.punktfunk.host = {
enable = true;
users = [ "alice" ]; # added to the `input` group, for virtual gamepads
openFirewall = true;
desktopSession = true; # on a machine you log into — see below
settings = { RUST_LOG = "info"; }; # these become host.env
};
```
The module does declaratively what the deb/RPM scriptlets do — the systemd user services, udev
rules, kernel modules, sysctl tuning, the firewall ports and `input` group membership — and brings
the web console in alongside the host. `settings` writes `host.env` for you.
**Set `desktopSession = true` on any machine somebody logs into.** It ties the host to
`graphical-session.target`, so restarting Plasma or GNOME restarts the host with it; without it the
host keeps running against a compositor that no longer exists and fails capture on every later
session. Leave it off for the headless appliance route (a pinned compositor or a gamescope box),
which may never reach that target — same reasoning as
[Restart the host with your desktop](/docs/running-as-a-service#restart-the-host-with-your-desktop).
## 3. Start it
The host and console user services are defined but not started (set `autoStart = true` for an
appliance). From your graphical session:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
```
The plugin runner needs no such step — the module starts it, because the game-library scanners ship
as plugins (`services.punktfunk.scripting.autoStart = false;` to opt out).
**That's the install.** Continue with the [Quick Start from step 3](/docs/quickstart#3-open-the-web-console)
— open the console, pair a client, stream.
## When you want more
- **Run it without NixOS** (other distros, wrapped in [nixGL](https://github.com/nix-community/nixGL)
so the GPU drivers resolve): `nix run git+https://git.unom.io/unom/punktfunk#punktfunk-host -- serve`.
- The full option reference — client, console and scripting options, GPU driver notes, headless
appliance setup — is in
[packaging/nix](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/nix/README.md).
- Updating: `nix flake update punktfunk` in your flake directory, then `sudo nixos-rebuild switch`
([Updating](/docs/updating)). Removing: [Uninstall](/docs/uninstall#nixos).
+45 -145
View File
@@ -1,165 +1,65 @@
---
title: Pairing & Trust
description: How a client and host establish trust — PIN pairing once, pinned reconnects after.
description: Admit a new device once — approve it from the web console or type a PIN — and it reconnects on its own from then on.
---
Punktfunk has no accounts and no cloud. Trust is established directly between a client and a host,
on your network, with a one-time pairing — either an **approval click in the host's
[web console](/docs/web-console)** or a **PIN ceremony**. After that, the device reconnects
automatically on a pinned cryptographic identity.
Punktfunk has no accounts and no cloud. A new device is let in **once**, by you, on your network;
after that it reconnects automatically on a pinned identity, and the host lists it until you remove
it. There are two ways to let a device in.
## How it works
## Approve it from the console (no PIN)
- Each host has a stable **identity** (a certificate). Clients remember its fingerprint, so they know
they're talking to the same host next time.
- The first time a client connects, you **pair** it: with the native protocol the **host** shows a
short **4-digit PIN** and you type it into the client. (With Moonlight it runs the other way round
— Moonlight shows the PIN and you type it into the host's console.) Either way a secure exchange
(SPAKE2) binds the two identities, and an attacker who doesn't know the PIN gets a single online
guess — no offline cracking.
- After pairing, the host stores the client's identity in its allow-list, and the client stores the
host's fingerprint. Reconnects are automatic — no PIN. Having seen the host on your network while
it was awake also teaches the client its MAC address, so a later connect can
[wake it from sleep](/docs/wake-on-lan).
The fastest way: just **connect** from the new device. The attempt shows up in the
[web console](/docs/web-console) under **Pairing → Waiting for approval**, with the device's name
and fingerprint. Click **Approve** — optionally give it a label like "Living Room TV" — and it's
paired on the spot; its next connect goes straight through.
## Approving a device from the console (no PIN)
**Deny** only dismisses the request (it can knock again; it isn't a blocklist). Requests expire on
their own after 10 minutes.
The fastest way to admit a new device: just **try to connect** from it. On a pairing-required host,
the attempt shows up in the web console's Pairing page under **Waiting for approval** — with the
device's name and identity fingerprint. Click **Approve** (and optionally give it a label like
"Living Room TV"), and the device is paired on the spot: its next connect goes straight through. No
PIN to read or type.
## Pair with a PIN
**Deny** just dismisses the request (the device can knock again later — it's "not now", not a
blocklist). Requests expire on their own after **10 minutes**.
When you're at the device and the console isn't handy, or for the very first device: in the console
open **Pairing** and click **Pair a device**. The host shows a **4-digit PIN** and counts down a
2-minute window — type the PIN into the client:
This works because approval happens on the host's authenticated management surface — only someone
with console access can admit a device.
- **Native apps (Apple, Linux, Windows, Android):** select the host, or *Pair with PIN…* from its
menu, and enter it.
- **Steam Deck (Decky plugin):** pick the host in the Quick Access panel — an unpaired one offers
**Request access** (the console approval above) or **Use a PIN instead**.
- **Moonlight:** it runs the other way round — Moonlight shows a PIN, and you type it into the
console's **Moonlight (GameStream) pairing** card. Arming doesn't apply. (Moonlight needs
[GameStream compat on](/docs/moonlight) first.)
If the window lapses, arm it again. A `punktfunk://` link can't pair for you — it only starts a
stream on a host this device already trusts.
## Choosing access when you admit a device
Admitting a device and deciding what it may do are **one dialog**, not two trips. The Approve
dialog carries, alongside the device's name:
Approving and deciding what the device may do are one dialog. Alongside the name:
- **Access level****Full control**, **Controller only**, or **View only**, with an **Advanced**
expander for the individual toggles. See [Access levels](/docs/access-levels) for exactly what
each covers.
- **Expires****Forever**, or **1 h / 4 h / 8 h / custom**.
- **Access level****Full control**, **Controller only**, or **View only** (an **Advanced**
expander has the individual toggles [Access levels](/docs/access-levels)).
- **Expires****Forever**, or 1 h / 4 h / 8 h / custom.
The defaults are **Full control · Forever** — approving your *own* new laptop is the common case,
and it should behave like pairing always has. The guest case gets its own button instead: a
distinct secondary action, **"Approve as guest"**, admits the device as **Controller only for
4 hours** in one click. The whole co-play flow is: your friend installs a client, connects, the
knock appears in your console, you tap **Approve as guest** — and their pad lights up as
controller #2. When the evening's over, the access expires on its own.
The same two controls appear on the **arm pairing** card for the PIN path: whatever access and
expiry you choose while arming applies to whichever device completes the PIN ceremony in that
window.
Everything here can be changed later from the Paired devices table — see
[Managing paired devices](#managing-paired-devices) below.
## Pairing with a PIN
PIN pairing is the **default and required** path for any new host: unless the host has explicitly
opted into trust-on-first-use (see below), a client connecting to an unknown host must complete the
PIN ceremony — or be approved from the console, as above — before it can stream. It's the right path
for the *first* device (before the console has admitted anything) or when you're at the client and
the console isn't handy.
Pairing has to be **armed** on the host before a client can pair (so a random device can't pair
itself). On the production host (`serve`), this is done from the **web console**: open the
host's management console, click to arm pairing, and the host displays a 4-digit PIN along with the
list of paired devices. This works on a headless host over the network — there is no command-line flag
to arm pairing on `serve`.
The armed window lasts **2 minutes** — the console counts it down under the PIN and offers a
**Cancel** button. Arm it once you're standing at the device; if it lapses, just arm it again.
Pairing from the console needs the console running. On Linux that's the separate `punktfunk-web`
systemd user unit, which you enable once — see [The Web Console](/docs/web-console).
Then, on the client:
- **[Native clients](/docs/clients) (Apple, Linux, Windows, Android):** select the host (or use
*Pair with PIN…* from its menu) and enter the PIN the host displays.
- **[Steam Deck](/docs/steam-deck) (the Decky plugin):** open Punktfunk from the Quick Access menu
and pick the host — an unpaired one opens a sheet offering **Request access** (no PIN: somebody
approves the Deck at the host) or **Use a PIN instead**, which opens the 4-digit pad.
- **[Moonlight](/docs/moonlight):** choose **Pair**; Moonlight shows a 4-digit PIN, and you type
that PIN into the console's **Moonlight (GameStream) pairing** card and press **Submit PIN**.
(This direction is the reverse of the native flow, and arming doesn't apply to it.)
A link can't stand in for any of this. A
[`punktfunk://` link](/docs/profiles-and-links#what-a-link-can-and-cant-do) starts a stream on a
host this device already trusts; `punktfunk://pair/…` is refused outright, and a link naming a host
you've never paired with can at most open the app's own trust prompt.
### Pairing from a terminal
On Linux the client package also installs `punktfunk`, a headless CLI. Arm pairing in the console,
read the PIN, then run:
```sh
punktfunk pair 192.168.1.50 --pin 1234 --name "Living Room"
```
It prints `paired <addr>:<port> fp=<fingerprint>` and saves the host in the same store the desktop
client uses, so later connects are silent. `--name` is the label the host files this device under
(default: this machine's name), and the port defaults to **9777** — write `host:port` to use another
one. Without `--pin` the command asks for one; in a script with no terminal it exits **6** rather
than hanging, and exit **3** means the host refused or the PIN was wrong.
The GTK client can do the same thing without opening a window:
```sh
punktfunk-client --connect 192.168.1.50:9777 --pair 1234 --name "Living Room"
```
## Requiring pairing (the default)
By default, the native host **requires** pairing — only devices that have paired can stream. This is
the right setting on a shared network: a device has to complete the PIN ceremony once before it can
connect.
If you're on a fully trusted single-user network and want to skip pairing, run the host open with
`serve --open` — it then advertises `pair=optional` and accepts unpaired clients. Requiring pairing
is strongly recommended.
## Trust-on-first-use (host opt-in)
Trust-on-first-use (TOFU) is **off by default** and is an explicit *host* opt-in for fully trusted
networks. A host enables it by running open — `serve --open` — which makes it advertise
`pair=optional` over mDNS and accept unpaired clients. Only then does a client offer the
TOFU path: connecting to such a host for the first time shows the host's fingerprint and asks you to
confirm it (compare it with the one the host logged at startup), then pins it. The client presents
this clearly as the reduced-security option, alongside **Pair with PIN**.
> **Warning:** TOFU cannot detect an impostor on the first connection — if someone is impersonating
> the host the very first time you connect, you'll pin the attacker's fingerprint. PIN pairing closes
> that gap (the SPAKE2 ceremony binds both identities), which is why it's the default. Use TOFU only
> on a network you fully trust — see [Security & Safe Use](/docs/security).
For every other case — a host advertising `pair=required` (the default), a host you typed in by hand,
or a discovered host whose pair policy is unknown — TOFU is not offered and the client routes straight
to the PIN ceremony.
Once a host is pinned, a fingerprint change is treated as the impostor signal: the client forces
re-pairing through the PIN ceremony rather than offering to re-trust the new identity.
Defaults are *Full control · Forever* — right for your own new laptop. For a friend's device there
is a one-click **Approve as guest**: Controller only, for 4 hours, then it expires on its own. The
same two controls sit on the **Pair a device** card, and apply to whichever device completes the PIN.
## Managing paired devices
The [web console](/docs/web-console) lists every paired device and lets you remove one (revoking its
access). Each row also shows the device's **Access** the preset and, for temporary grants, a live
countdown ("Controller · 2 h left") — and an edit sheet to change the level, extend or cut the
expiry, or expire it right now; edits reach a live session immediately. See
[Access levels](/docs/access-levels). Re-pairing is just the PIN ceremony again — and it keeps the
device's existing access rather than widening it.
The console lists every paired device with its access (and a live countdown for temporary grants).
From there you can change the level, extend or cut the expiry, or **remove** the device — removing
revokes it immediately, even mid-session. Re-pairing a removed device is just the PIN ceremony again.
If a client can't pair at all, see [Troubleshooting → Pairing is
rejected](/docs/troubleshooting#pairing-is-rejected--the-client-cant-connect).
Can't pair at all? [Troubleshooting → Pairing is rejected](/docs/troubleshooting#pairing-is-rejected--the-client-cant-connect).
(There is also a developer/measurement host, `punktfunk-host punktfunk1-host` — a subcommand of the
same binary, not the host you install. It has its own `--allow-tofu` / `--pairing-pin` flags for test
harnesses; nothing on this page applies to it.)
## How it works, briefly
Each host has a stable identity (a certificate); clients pin its fingerprint, the host stores the
client's. The PIN ceremony is SPAKE2, so someone who doesn't know the PIN gets one online guess and
no offline attack. A host whose fingerprint changes is treated as an impostor — the client forces a
fresh PIN ceremony rather than re-trusting it. Pairing is **required by default**; the reduced-security
alternatives (`serve --open`, trust-on-first-use) exist for fully trusted single-user networks and
are covered in [Security & Safe Use](/docs/security#pairing-policy-open-hosts-and-trust-on-first-use).
Scripts can pair from a terminal — [the `punktfunk` CLI](/docs/clients#scripting-the-punktfunk-cli).
+52 -63
View File
@@ -7,8 +7,8 @@ Plugins extend the host through the **scripting runner** (see [Events & hooks](/
plugin runs alongside the host, reconciles titles into your [game library](/docs/game-library) as a
provider — so they appear in the grid on every client — and can add its own page to the [web console](/docs/web-console).
**Your game library is built from plugins.** From v0.28.0 the host scans no launcher on its own, so
install the one for each launcher you use — that is what puts games in the grid.
**Your game library is built from plugins.** From v0.28.0 the host scans no launcher on its own;
install the one for each launcher you use.
| Plugin | Host | What it does |
|---|---|---|
@@ -23,37 +23,33 @@ install the one for each launcher you use — that is what puts games in the gri
## Installing from the console
Every plugin runs inside the **plugin runner**, a separate service that is off until you switch it
on. Installing a plugin while it's off succeeds and starts nothing, so do this first:
Every plugin runs inside the **plugin runner**, a separate service. Installing a plugin while it's
off succeeds and starts nothing, so check it first:
1. Open the [web console](/docs/web-console) → **Plugins** → **Installed** and look at the **Plugin
runner** card. If it says *Not installed*, install the runner package first (see
[Troubleshooting](#troubleshooting) below). If it says *Disabled*, press **Enable runner** —
once per host. *Running* means you're set; *Stopped* means the runner is enabled but not up right
now, and its log says why (see [Troubleshooting](#troubleshooting)).
runner** card. *Not installed*: install the runner package first (see
[Troubleshooting](#troubleshooting)). *Disabled*: press **Enable runner** — once per host.
*Running* means you're set; *Stopped* means the runner is enabled but not up right now, and its
log says why (see [Troubleshooting](#troubleshooting)).
2. Go to **Browse**, pick a plugin from the catalog and confirm. The host installs it and restarts
the runner, and the plugin's own page appears in the console's nav.
**Sources** is the third tab: where catalogs come from.
The rest of this page covers the CLI, which does the same thing, and the trust model behind the
badges — worth reading once, because a plugin runs with the same privileges as the host.
### What "Verified" means
Every catalogued plugin pins **one exact version** and that version's package hash. A verified entry
means somebody at unom reviewed *that exact package* — not the project in general, and not whatever
it publishes next. When a plugin releases a new version, the store keeps offering the reviewed one
until the new release is reviewed too. Before anything is downloaded, the host re-checks the pinned
hash against the registry, so a package that was quietly republished under the same version number
is refused rather than installed.
Every catalogued plugin pins **one exact version** and its package hash. **Verified** means somebody
at unom reviewed *that exact package* — not the project in general, not whatever it publishes next;
when a plugin releases a new version, the store keeps offering the reviewed one until the new release
is reviewed too. Before anything is downloaded, the host re-checks the pinned hash against the
registry, so a package quietly republished under the same version number is refused.
A catalog can also **revoke** a version. When an advisory covers an entry, the console shows the
reason against it and won't install that version — on **Browse** as a red-ringed panel with the
install button disabled, on **Installed** as a warning against the plugin you already have. It
never removes running code for you; that stays your decision.
reason and won't install that version — on **Browse** as a red-ringed panel with the install button
disabled, on **Installed** as a warning against the plugin you already have. It never removes
running code for you.
Three things a plugin can be:
The three badges:
| Badge | Where it came from |
|---|---|
@@ -67,8 +63,8 @@ Three things a plugin can be:
`ed25519:…` public key — with a key set, the host refuses any index from that source that isn't
correctly signed, rather than falling back to an unsigned one.
Adding a source is a trust decision you make once: its plugins become installable on this host.
They are always attributed to it and never carry the Verified badge, which belongs to the built-in
Adding a source is a trust decision you make once: its plugins become installable on this host,
always attributed to it and never carrying the Verified badge, which belongs to the built-in
catalog alone.
To publish a plugin to the built-in catalog, open a pull request against
@@ -111,15 +107,13 @@ the full path: `& "$env:ProgramFiles\punktfunk\punktfunk-host.exe" plugins add p
</Tab>
</Tabs>
Open the [web console](/docs/web-console) and the plugin's page appears in the nav automatically
that's the whole install.
The plugin's page then appears in the [web console](/docs/web-console)'s nav automatically.
The runner is **on by default** on a new install — your game sources are plugins, so a host without
it would show an empty library. (On a host that predates this, it stays however you left it; turn it
on with `punktfunk-host plugins enable`, which you only need once.) The runner discovers plugins
when it starts, so one installed later needs a restart to come up
(`systemctl --user restart punktfunk-scripting`, or `Restart` the `PunktfunkScripting` task) — the
console does that restart for you as part of installing.
it would show an empty library. (A host that predates this stays however you left it; turn it on
with `punktfunk-host plugins enable`, once.) The runner discovers plugins when it starts, so one
installed later needs a restart (`systemctl --user restart punktfunk-scripting`, or `Restart` the
`PunktfunkScripting` task) — the console does that restart for you as part of installing.
Don't want it? It is a normal service you can switch off: `systemctl --user mask punktfunk-scripting`
on Linux, or disable the `PunktfunkScripting` scheduled task on Windows. Your host keeps streaming;
@@ -143,8 +137,7 @@ and it carries its catalog badge instead.
A bare name resolves to the first-party package — `playnite` installs `@punktfunk/plugin-playnite`,
always from Punktfunk's own package registry. Any other name (`punktfunk-plugin-*`, a foreign
`@scope/pkg`) would install from the **public npm registry** and is refused unless you add
`--allow-public-registry` — a guard against typos and look-alike packages pulling untrusted code
onto your host.
`--allow-public-registry` — a guard against typos and look-alike packages.
> Plugins are operator-installed code with operator privileges — they can launch games and run
> commands. Install only plugins you trust, from a registry you control.
@@ -155,17 +148,16 @@ onto your host.
catalog pins a newer version than the one you have; it installs through the same confirmation as a
fresh install, and the console restarts the runner for you. There is no `plugins update` command —
from a terminal, re-run `punktfunk-host plugins add <name>`. That installs the newest version the
package registry has rather than the version the catalog pins, and it does **not** restart the
runner: restart it yourself so the new code is picked up
(`systemctl --user restart punktfunk-scripting`, or `Restart` the `PunktfunkScripting` task).
package registry has rather than the version the catalog pins, and does **not** restart the runner:
restart it yourself (`systemctl --user restart punktfunk-scripting`, or `Restart` the
`PunktfunkScripting` task) so the new code is picked up.
**Remove.** The **Uninstall** (bin) button on the **Installed** tab removes the package *and*
restarts the runner, so the plugin stops straight away. `punktfunk-host plugins remove <name>`
removes the package only — restart the runner yourself, as above, to stop a plugin that is still
running.
removes the package only — restart the runner yourself, as above, to stop a plugin still running.
Uninstalling removes the package and nothing else. A plugin's own config and cache stay where it
wrote them — `~/.config/punktfunk/plugin-state/<plugin>/` on Linux,
Uninstalling removes the package only; a plugin's own config and cache stay where it wrote them —
`~/.config/punktfunk/plugin-state/<plugin>/` on Linux,
`%ProgramData%\punktfunk\plugin-state\<plugin>\` on Windows — so re-installing later picks your
settings back up. Delete that directory yourself if you want it gone.
@@ -233,8 +225,8 @@ GOG, Epic, Xbox, itch, emulators, manually-added games — everything Playnite m
library. Launching a title hands it back to Playnite, which performs the real launch, so there are no
per-store launch commands to maintain. Covers are served by the host, so it scales to large libraries.
Playnite is Windows-only, so both halves of this one live on the **Windows host**. Because Playnite
keeps its library locked while running, there are **two parts**:
Playnite is Windows-only, so both halves live on the **Windows host**. Because Playnite keeps its
library locked while running, there are **two parts**:
1. **The plugin** — from an elevated PowerShell:
@@ -255,9 +247,9 @@ seconds of any library change. Filters (installed-only, per-store, hidden) live
## VirtualHere (USB passthrough)
`@punktfunk/plugin-virtualhere` — hands a **physical USB device** on your couch machine to the
host while you play, and gives it back afterwards. The game sees the real device, so this is the
answer for a racing wheel, a HOTAS, pedals, an arcade stick, or any controller whose value is that
it is not emulated.
host while you play, and gives it back afterwards. The game sees the real device — the answer for a
racing wheel, a HOTAS, pedals, an arcade stick, or any controller whose value is that it is not
emulated.
<Callout type="warn">
This plugin drives [VirtualHere](https://www.virtualhere.com/), a commercial USB-over-IP product
@@ -273,8 +265,7 @@ You need both halves of VirtualHere running before the plugin is any use:
- **The USB Client on the host**, ideally installed as a service so it survives logging out.
Servers exist for Windows, Linux, macOS and Android couches. **There is no VirtualHere server for
iOS or tvOS**, so iPhones, iPads and Apple TVs cannot pass devices through — nothing on the
Punktfunk side can change that.
iOS or tvOS**, so iPhones, iPads and Apple TVs cannot pass devices through.
```sh
punktfunk-host plugins add virtualhere
@@ -288,8 +279,8 @@ starts and returned when it stops, so the couch keeps its own controller the res
you can widen that to the whole session, or to the entire time a client is connected.
If nothing happens, the **Diagnostics** tab walks the whole two-sided setup and tells you which
part to fix. The same checks are available as `punktfunk-plugin-virtualhere doctor`, which is the
useful thing to paste into a support thread.
part to fix. The same checks run as `punktfunk-plugin-virtualhere doctor` — the useful thing to
paste into a support thread.
Full configuration is in
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-virtualhere).
@@ -311,10 +302,10 @@ default whenever the host is). On Windows, re-run the installer and keep the scr
If the runner *is* installed and the host still says it isn't, the host could not find the
`punktfunk-scripting` executable. It looks beside its own binary, then on `PATH`, then in the
packaged `/usr` and `~/.local` layouts — so a runner installed somewhere else needs
`PUNKTFUNK_SCRIPTING` pointed at it (see [Configuration](/docs/configuration)). Note that the
console installs plugins from inside the host *service*, whose `PATH` is usually much shorter than
your shell's: a runner that `punktfunk-host plugins add` finds and the console does not is that
difference, and the env var is the fix.
`PUNKTFUNK_SCRIPTING` pointed at it (see [Configuration](/docs/configuration)). The console installs
plugins from inside the host *service*, whose `PATH` is usually much shorter than your shell's: a
runner that `punktfunk-host plugins add` finds and the console does not is that difference, and the
env var is the fix.
**Where a plugin's log output goes** — the console's **Logs** page, under the **Plugins** filter.
The runner ships everything your plugins print to the host, so a plugin's own lines sit next to the
@@ -327,12 +318,11 @@ with `punktfunk-host plugins status`.
<Callout>
Nothing is lost if the host is down: the runner keeps buffering and sends the backlog when the host
comes back. It says so in the log if the buffer overflowed, rather than presenting a gap as
continuity.
comes back, and says so in the log if the buffer overflowed.
</Callout>
**Reading the runner's log directly** — rarely needed now, but it is the ground truth if the runner
can't reach the host at all:
**Reading the runner's log directly** — rarely needed, but the ground truth if the runner can't
reach the host at all:
<Tabs items={['Linux', 'Windows']}>
<Tab value="Linux">
@@ -345,9 +335,9 @@ journalctl --user -u punktfunk-scripting -f
<Tab value="Windows">
The runner task writes its output to `%ProgramData%\punktfunk\plugin-state\runner.log` (the
previous run is kept as `runner.log.1`). This is the file to read — or send — when the console's
Plugins view stays empty although the runner is running: everything the runner and its plugins
printed lands here even when they can't reach the host.
previous run is kept as `runner.log.1`). Read — or send — this file when the console's Plugins view
stays empty although the runner is running: everything the runner and its plugins printed lands
here even when they can't reach the host.
```powershell
Get-Content "$env:ProgramData\punktfunk\plugin-state\runner.log" -Tail 100
@@ -367,10 +357,9 @@ foreground (stop it with <kbd>Ctrl</kbd>+<kbd>C</kbd>):
**A plugin can't reach a service running on the same box (Linux)** — plugins that drive a local
daemon usually talk to it over a socket or FIFO in `/tmp`. The runner's unit shipped with
`PrivateTmp=yes` in earlier releases, which gave it a private `/tmp` and hid all of it: the plugin would
launch the vendor's binary happily and then time out reaching the daemon behind it, while the same
command worked perfectly in your own shell. If you are on an older host, or you have a drop-in that
reinstates it, put the real `/tmp` back:
`PrivateTmp=yes` in earlier releases, hiding the real `/tmp`: the plugin would launch the vendor's
binary and then time out reaching the daemon, while the same command worked in your own shell. On an
older host, or with a drop-in that reinstates it, put the real `/tmp` back:
```sh
systemctl --user edit punktfunk-scripting
+55
View File
@@ -0,0 +1,55 @@
---
title: Ports & firewall
description: Every port a Punktfunk host and its console use, which firewall profile opens each, and what to do with a host behind ufw or firewalld.
---
A Linux package **never edits your firewall** — it installs service definitions for **firewalld** and
application profiles for **ufw**, and you enable the ones you need once. The table is the single
list; the install guides quote the one or two lines that apply to each distro.
<Ports />
- **`punktfunk-native`** is all a default host needs: the packaged unit runs the secure native-only
host, so paired Punktfunk clients, discovery and the management API are covered by this one
profile.
- **`punktfunk-web`** only matters if you open the console from *another* device (both console
ports are in it; plugin interfaces are served on the second one — [Two ports, not
one](/docs/web-console#two-ports-not-one)).
- **`punktfunk-gamestream`** is needed only once you turn on Moonlight compat
(`PUNKTFUNK_GAMESTREAM=1` in `host.env` — [Moonlight](/docs/moonlight)).
- **Video needs nothing opened.** The data plane uses an ephemeral UDP port the *client* opens with a
hole-punch; the host streams back through the path the client opened, so only outbound UDP has to
be allowed (the default in both ufw and firewalld).
## Enabling the profiles
```sh
# firewalld (Fedora, Bazzite, EndeavourOS, …)
sudo firewall-cmd --reload # load the definitions the package installed
sudo firewall-cmd --permanent --add-service=punktfunk-native --add-service=punktfunk-web
sudo firewall-cmd --permanent --add-service=punktfunk-gamestream # only with Moonlight compat on
sudo firewall-cmd --reload
# ufw (CachyOS; Ubuntu and Debian if you enabled it)
sudo ufw allow punktfunk-native
sudo ufw allow punktfunk-web
sudo ufw allow punktfunk-gamestream # only with Moonlight compat on
```
Stock Arch and Debian ship no firewall; Ubuntu installs ufw but leaves it inactive; Fedora, Bazzite
and most Fedora-family spins run firewalld; CachyOS enables ufw. On **NixOS** the module's
`openFirewall = true` does all of this; on **Windows** the installer registers the rules.
## Moving a port
Two are configurable, and both are how you share a machine with another streaming host — see
[Switching from Sunshine](/docs/switching-from-sunshine):
- The **management API** via `PUNKTFUNK_MGMT_BIND` (`IP:PORT`; `127.0.0.1:…` also keeps it off the
LAN, at the cost of paired clients browsing your library). Clients relearn the port from
discovery; the console, the plugin runner and the tray read it from
`~/.config/punktfunk/mgmt-endpoint`, which the host rewrites on every start.
- The **native control port** via `PUNKTFUNK_NATIVE_PORT`. Clients discover it over mDNS; a host
added by hand keeps the port it was added with.
Both live in `host.env` — [Configuration](/docs/configuration).
+90 -97
View File
@@ -3,87 +3,80 @@ title: Profiles and links
description: How settings profiles override your client defaults per host or per connect, and how punktfunk:// links start a stream from a shortcut, a script or a browser.
---
Two features that landed together in 0.22.0 and work with each other: **settings profiles**, named
bundles of stream settings you can attach to a host, and **`punktfunk://` links**, URLs that start a
stream you have already set up.
Two features that landed together in 0.22.0: **settings profiles** named bundles of stream
settings you attach to a host and **`punktfunk://` links** URLs that start a stream you have
already set up.
Both live in the client apps — the Apple app, the Linux GTK client, the Windows client and the
Android app. Neither exists in the host's [web console](/docs/web-console).
Both live in the client apps (Apple, Linux GTK, Windows, Android), not in the host's
[web console](/docs/web-console).
The controller-driven surfaces are a half-exception: Apple TV, the Android app's console mode and
the Steam Deck console the Decky plugin launches all *use* the profile a host is bound to and can
pin one as its own card, but none of them can create or edit one. Do that on a desktop or a phone
first. The Decky panel itself only *shows* those pins, nested under their host as one-tap cards.
The controller-driven surfaces Apple TV, the Android app's console mode and the Steam Deck console
the Decky plugin launches *use* the profile a host is bound to and can pin one as its own card,
but cannot create or edit one; do that on a desktop or phone first. The Decky panel only *shows*
those pins, nested under their host as one-tap cards.
## What a profile is
A profile is a *sparse* set of overrides on top of your normal client settings. Only the rows you
actually touch are stored. Everything else keeps following your defaults **live**, so changing a
default later also moves every profile that never overrode it.
A profile is a *sparse* set of overrides on your normal client settings. Only the rows you touch are
stored; everything else follows your defaults **live**, so changing a default later also moves every
profile that never overrode it.
Touching a row records the override even when you pick the value the default already has. That is a
deliberate *pin*: the profile keeps that value when the default later moves. The only way back to
inheriting is the row's explicit **Reset**.
Touching a row records the override even when you pick the value the default already has a
deliberate *pin* that holds when the default later moves. Only the row's **Reset** returns it to
inheriting.
Where the catalog is kept:
| Client | Stored in |
| Client | Catalog stored in |
|---|---|
| Linux | `~/.config/punktfunk/client-profiles.json` |
| Windows | `%APPDATA%\punktfunk\client-profiles.json` |
| Apple | the app-group store, beside your saved hosts |
| Android | app-private storage |
The catalog is per device, and nothing syncs it, so a profile you make on your laptop doesn't appear
on your phone.
The catalog is per device and nothing syncs it: a profile made on your laptop isn't on your phone.
## Creating and editing one
Profiles are created and edited in the client's own **Settings** screen — there is no second editor,
so a profile can never drift from the surface it overrides.
Profiles are created and edited in the client's own **Settings** screen — no second editor, so a
profile can't drift from the surface it overrides.
1. Open Settings. At the top is a scope switcher listing **Default settings**, your profiles, and a
**New profile** entry. Linux, Windows and iOS label the control **Editing**; macOS heads the
preferences window with the layer's name; Android shows the choices as a row of chips.
2. Create a new profile. Linux, Apple and Android ask for a name (and a colour) first; Windows
creates one called *Profile 1* and opens its edit sheet, where you rename it. Names must be
unique, ignoring case.
3. Change the rows you want. Every row shows the *effective* value — the inherited default until you
touch it.
4. A row you have overridden grows a marker and a **Reset** control. Reset drops that one override
and puts the row back to following your defaults.
1. Open Settings. A scope switcher at the top lists **Default settings**, your profiles, and **New
profile**. Linux, Windows and iOS label it **Editing**; macOS heads the preferences window with
the layer's name; Android shows the choices as a row of chips.
2. Create a profile. Linux, Apple and Android ask for a name (and colour) first; Windows creates
*Profile 1* and opens its edit sheet, where you rename it. Names must be unique, ignoring case.
3. Change the rows you want. Every row shows the *effective* value — the inherited default until
you touch it.
4. An overridden row grows a marker and a **Reset** control, which drops that one override.
Each profile can carry a colour from a small preset palette (the exact swatches differ slightly
between apps). It tints the profile's chip on host cards, so a grid of hosts is readable at a
glance.
Each profile can carry a colour from a small preset palette (swatches differ slightly between
apps); it tints the profile's chip on host cards, so a grid of hosts reads at a glance.
Renaming, duplicating (overrides and colour included) and deleting sit next to the switcher on
Linux and in the same menu on Apple. Windows puts them in the sheet the switcher's **Edit** entry
opens; Android puts them on the selected profile's own chip — tap it a second time.
Rename, duplicate (overrides and colour included) and delete sit next to the switcher on Linux and
in the same menu on Apple; Windows puts them in the sheet the switcher's **Edit** entry opens;
Android on the selected profile's chip — tap it a second time.
While a stream runs with a profile, the profile's name closes the first line of the
While a stream runs with a profile, its name closes the first line of the
[stats overlay](/docs/stats) — on the Apple client, from the Normal tier up.
## What a profile can't change
In profile scope, rows that aren't profileable simply don't render. They are facts about *this
device* — the video decoder and GPU it uses, its audio endpoints, which physical controller you
hold, whether it wakes hosts on connect and whether it shows a game library — not about how you
want a stream to look. **Share clipboard** is out for the neighbouring reason: it's a per-host trust
decision stored on the host record rather than a client setting at all — see
[Clipboard](/docs/clipboard).
In profile scope, rows that aren't profileable don't render. They are facts about *this device*
its video decoder and GPU, its audio endpoints, which physical controller you hold, whether it
wakes hosts on connect, whether it shows a game library — not about how a stream should look.
**Share clipboard** is out for a neighbouring reason: it's a per-host trust decision stored on the
host record, not a client setting — see [Clipboard](/docs/clipboard).
The row-by-row list, and why each row stays global, is on
The row-by-row list, and why each stays global, is on
[Client settings](/docs/client-settings#settings-that-are-facts-about-your-device).
## Three ways to use a profile
**Bind it to a host.** Open a saved host's edit sheet and set **Profile**. Every plain click on that
host's card now uses it. This is the only sticky choice. (The Apple app also offers **Connect with ▸
Set Default Profile** on the card itself.)
**Bind it to a host.** In a saved host's edit sheet, set **Profile**. Every plain click on that
host's card now uses it the only sticky choice. (The Apple app also offers **Connect with ▸ Set
Default Profile** on the card.)
**Use it once.** A card's menu has **Connect with** pick a profile for this connect only. It never
rebinds the host. **Default settings** in that menu is a real choice: on a bound host it forces your
**Use it once.** A card's menu has **Connect with** — a profile for this connect only; it never
rebinds the host. **Default settings** there is a real choice: on a bound host it forces your
globals for one session. (Android lists the same choices flat, as *Connect with: …*.)
**Pin it as its own card.** A pinned profile gets its own card beside the host — one click, no menu.
@@ -93,23 +86,23 @@ second host: unpinning changes neither the profile nor the host's binding.
## Deleting a profile
The confirmation tells you what breaks: how many hosts will fall back to **Default settings**, and
how many pinned cards will disappear. Bindings and pins are deliberately left pointing at the gone
profile rather than rewritten; everywhere they are read, a dangling reference resolves as "no
profile", which is exactly your defaults. Nothing errors and no connect is blocked.
The confirmation says what breaks: how many hosts fall back to **Default settings** and how many
pinned cards disappear. Bindings and pins are left pointing at the gone profile rather than
rewritten; wherever they are read, a dangling reference resolves as "no profile" — your defaults.
Nothing errors and no connect is blocked.
## `punktfunk://` links
A link starts a stream on a host this device already trusts. All four apps register the scheme with
the operating system, so a link works from a browser (behind the browser's own "open this app?"
prompt), a desktop shortcut, a home-automation rule or a script:
the operating system, so a link works from a browser (behind its "open this app?" prompt), a
desktop shortcut, a home-automation rule or a script:
```text
punktfunk://connect/<host-ref>[?fp=<64-hex>][&host=<addr[:port]>][&launch=<id>][&profile=<ref>][&name=<label>]
```
`<host-ref>` is a saved host's stable record id, its name (unique, ignoring case), or `addr[:port]`.
Resolution goes in that order, and a name matching two saved hosts is refused rather than guessed.
`<host-ref>` is a saved host's stable record id, its name (unique, ignoring case), or `addr[:port]`,
resolved in that order; a name matching two saved hosts is refused rather than guessed.
| Parameter | Means |
|---|---|
@@ -119,17 +112,17 @@ Resolution goes in that order, and a name matching two saved hosts is refused ra
| `profile` | a settings profile, by id or unique name — for this connect only |
| `name` | a display label, shown as *claimed*, never trusted |
The scheme and the route word are case-insensitive, a trailing slash is fine, a `#fragment` is
dropped, unknown parameters are ignored, an empty value means "not given", and if a parameter
appears twice the first one wins. `pf://` parses as an input alias, but nothing emits it and no app
registers it with the operating system, so write `punktfunk://`.
Scheme and route word are case-insensitive, a trailing slash is fine, a `#fragment` is dropped,
unknown parameters are ignored, an empty value means "not given", and a repeated parameter's first
value wins. `pf://` parses as an input alias, but nothing emits it and no app registers it with the
operating system, so write `punktfunk://`.
`connect` works everywhere. `browse` — the same grammar with no parameters acted on — opens the
host's game library instead of streaming, on the Apple apps today (it backs their library widget
and the Open Game Library shortcut); every other client answers it with a notice, as they all do
for `wake`. Values are capped (2048 for the whole URL, 128 for the host reference
and `launch`, 64 for `profile` and `name`), and `launch` must be printable ASCII with no spaces,
quotes, backslashes, `$` or backticks.
`connect` works everywhere. `browse` — the same grammar, no parameters acted on — opens the host's
game library instead of streaming, on the Apple apps today (it backs their library widget and the
Open Game Library shortcut); every other client answers it with a notice, as they all do for
`wake`. Values are capped (2048 for the whole URL, 128 for the host reference and `launch`, 64 for
`profile` and `name`), and `launch` must be printable ASCII with no spaces, quotes, backslashes, `$`
or backticks.
Worked examples:
@@ -141,58 +134,58 @@ punktfunk://connect/Living%20Room%20PC?profile=Work
## What a link can and can't do
The rule the grammar exists to keep: **a link may only do what clicking a card you already have
could do, minus every trust decision.**
The rule the grammar keeps: **a link may only do what clicking a card you already have could do,
minus every trust decision.**
- It carries *references*, never values. There is no resolution, bitrate, codec or HDR parameter,
so a web page cannot shape your session beyond choosing among your own configurations.
- It carries *references*, never values. No resolution, bitrate, codec or HDR parameter, so a web
page cannot shape your session beyond choosing among your own configurations.
- There is no `pair` route and never will be. `punktfunk://pair/...` is refused outright;
[pairing](/docs/pairing) stays something you do with the fingerprint on screen.
- A link naming a host you don't know is never connected. When it carries an address — as
`<host-ref>` or as `host=` — Linux and Android open the app's normal trust prompt, pre-filled with
`<host-ref>` or `host=` — Linux and Android open the app's normal trust prompt, pre-filled with
that address and any `fp` the link carried, so the first connect is verified rather than blind.
Windows and the Apple apps show a notice naming the host instead, and you pair from the host list
yourself. A link with no address to fall back on — a bare name or a stale record id — is simply
refused with a notice.
- If the link's `fp` contradicts the fingerprint already pinned for that host, it is a hard refusal
with a notice. Nothing connects.
Windows and the Apple apps show a notice naming the host, and you pair from the host list
yourself. A link with no address to fall back on — a bare name or a stale record id — is refused
with a notice.
- An `fp` that contradicts the fingerprint already pinned for that host is a hard refusal with a
notice. Nothing connects.
- A `profile=` that names nothing on this device, or two profiles at once, refuses **before**
anything is dialled — a shortcut that can't honour its profile says so rather than streaming with
the wrong settings. A link with no `profile=` honours the host's binding, exactly like a click.
the wrong settings. A link with no `profile=` honours the host's binding, like a click.
- A link never preempts a running session. Linux and Windows say "A session is already running — end
it first". Apple and Android do the same, except when the link points at the host you are already
streaming, which just brings the app forward.
it first". Apple and Android do the same, except a link to the host you are already streaming just
brings the app forward.
## Getting a link, and making a shortcut
On Linux and Windows a host card's menu has **Copy link** and **Create shortcut…**. On macOS and iOS
the card menu has **Copy Link**; tvOS has no clipboard, so it isn't offered there. Android has
**Copy link** in both of its homes — the touch grid's card menu, and the controller home's host
options (press Up on a host's tile).
**Copy link** in both homes — the touch grid's card menu, and the controller home's host options
(press Up on a host's tile).
On Linux, Apple and Android a pinned card has its own menu, and the link it hands out carries that
card's profile. Windows pinned tiles have no menu, and neither Windows action adds a `profile=`, so a
Windows link always uses the host's binding until you edit the URL yourself.
On Linux, Apple and Android a pinned card has its own menu, and its link carries that card's
profile. Windows pinned tiles have no menu, and neither Windows action adds a `profile=`, so a
Windows link uses the host's binding until you edit the URL yourself.
A copied link carries the host's stable record id, plus `host=` and `fp=` (the fingerprint only when
one is pinned). That is what keeps a shortcut written today working after the host changes address
or you reinstall the client.
one is pinned), so a shortcut written today survives the host changing address or you reinstalling
the client.
**Create shortcut…** writes a launcher wrapped around that URL:
**Create shortcut…** writes a launcher around that URL:
- **Linux** — a desktop entry in `~/.local/share/applications/`, which shows up in your app menu.
Under Flatpak the sandbox can't write there, so the app offers you the URL to place yourself.
- **Linux** — a desktop entry in `~/.local/share/applications/`, visible in your app menu. Under
Flatpak the sandbox can't write there, so the app offers you the URL to place yourself.
- **Windows** — a `.lnk` on your Desktop that runs `punktfunk-client.exe` with the URL as its
argument, so it keeps working across updates.
Any other launcher works too, as long as it hands the URL to the client. From a script, or on a
headless box, use the `punktfunk` CLI:
Any other launcher works as long as it hands the URL to the client. From a script, or on a headless
box, use the `punktfunk` CLI:
```bash
punktfunk profiles list # ids, names, how many settings each overrides
punktfunk open 'punktfunk://connect/Desk?profile=Work'
```
It ships in the Linux client packages and in the Windows MSIX. The Flatpak build has it too, but
inside the sandbox — reach it with `flatpak run --command=punktfunk io.unom.Punktfunk`. See
[Clients](/docs/clients) for the rest of its verbs.
It ships in the Linux client packages and the Windows MSIX. The Flatpak has it too, inside the
sandbox — `flatpak run --command=punktfunk io.unom.Punktfunk`. See [Clients](/docs/clients) for the
rest of its verbs.
+51 -113
View File
@@ -1,140 +1,78 @@
---
title: Quick Start
description: From nothing to streaming — set up a host and connect your first client.
description: From nothing to streaming in five steps — install a host, open its console, pair a client, play.
---
This is the shortest path to a working stream. Each step links to the details.
Five steps, each linking to the detail only if you need it. Punktfunk is built for a **trusted home
network** — keep the host on your LAN or a VPN, never open it to the internet
([why](/docs/security)).
> A streaming host is remote control of the machine, so it's built for **trusted local networks** — keep
> it on your LAN or a VPN and don't expose it to the internet. Two minutes on
> [Security & Safe Use](/docs/security) before you start is worth it.
## 1. Install the host
## 1. Set up the host
On the PC you want to stream *from*, follow the one-page guide for its system — each one is the
install command, the group to join, and nothing else:
On your gaming machine (NVIDIA, AMD, or Intel GPU), follow the install guide for your system:
| Linux | Windows |
|---|---|
| [Ubuntu](/docs/ubuntu) · [Debian](/docs/debian) · [Fedora](/docs/fedora) · [Arch / CachyOS](/docs/arch) · [Bazzite](/docs/bazzite) · [SteamOS](/docs/steamos-host) · [NixOS](/docs/nixos) | [Windows 11](/docs/windows-host) |
- [Ubuntu](/docs/ubuntu)
- [Fedora](/docs/fedora)
- [Arch](/docs/arch)
- [Bazzite](/docs/bazzite)
- [SteamOS](/docs/steamos-host)
- [Windows host](/docs/windows-host)
Not sure your machine qualifies? [Requirements](/docs/requirements) is the checklist.
Each one covers the GPU driver, the dependencies, and how to install and run the host. After
installing, configure for your desktop ([KDE](/docs/kde) / [GNOME](/docs/gnome) /
[gamescope](/docs/gamescope) / [Hyprland](/docs/hyprland) / [Sway](/docs/sway)). Check the
[Requirements](/docs/requirements) first if you're not sure your machine is a fit.
## 2. Start it
## 2. Start the host
- **Windows and SteamOS:** nothing to do — the installer started the host and the web console, and
they come back on every boot.
- **Linux packages:** from a terminal inside your desktop session, start the host and the console
once; they restart at every login from then on:
**On Windows there's nothing to start.** The installer registered and started the `PunktfunkHost`
service, so the host is already running and comes back on every boot. Confirm it:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
```
```powershell
Get-Service PunktfunkHost
```
On Arch install `punktfunk-web` first (your install page says how). Running a firewall? Your
install page has the one line that opens it.
Don't run `punktfunk-host serve` on top of it — a second host process refuses to touch the
virtual-display driver and collides with the service on its ports. Details:
[Running as a Service → Windows](/docs/running-as-a-service#windows).
**On Linux you don't run the host by hand either.** Every Linux package ships a systemd **user** unit.
That unit reads `~/.config/punktfunk/host.env` and won't start until the file exists, so put it in
place first — your distro and desktop pages (step 1) have the template to copy and what to put in it.
Then enable the unit once, from a terminal **inside your desktop session**, and it comes back at
every login:
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f # watch it come up and print its identity fingerprint
```
Once up, the host advertises itself on your local network, so clients find it by name. It works out
where your compositor is by itself, so there is nothing to export.
That unit runs the **secure native-only host** (Punktfunk clients + the management API). Want stock
[Moonlight](/docs/moonlight) clients too? GameStream compat is **opt-in** — add
`PUNKTFUNK_GAMESTREAM=1` to `~/.config/punktfunk/host.env` and restart the unit (trusted LANs only;
its pairing runs over plain HTTP) — see
[What the unit starts](/docs/running-as-a-service#what-the-unit-starts).
If the host runs a firewall (Fedora enables firewalld, CachyOS enables ufw), open its ports — the
firewall step in your distro guide has the exact commands: `punktfunk-native` always, plus
`punktfunk-gamestream` only if you enabled Moonlight compat.
On **SteamOS** even that is done for you — the install script wrote its own `punktfunk-host` user
service and started it (GameStream on by default there too; pass `--no-gamestream` to the install
script for a native-only host). Check it with `systemctl --user status punktfunk-host`.
The host announces itself on your network, so clients find it by name. It works out which desktop you
run by itself — there is nothing to configure for a first stream.
## 3. Open the web console
The console is a **separate** process from the host, and you need it in the next step — arming PIN
pairing is done there.
The console is where you admit new devices. Open **`https://<host-ip>:47992`** in a browser (the
certificate is the host's own, so your browser warns once — continue) and log in:
- **Windows:** the installer already set it up and starts it at boot. Open
`https://<host-ip>:47992`. The login password is the one the installer showed you on its final
page; it's stored in `%ProgramData%\punktfunk\web-password`.
- **Linux:** on Ubuntu, Fedora and Bazzite the `punktfunk-web` package comes in with the host
but isn't enabled for you. On Arch it's an optional dependency, so install it first
(`sudo pacman -Syu punktfunk-web` — a full `-Syu`, never a bare `-S`). Either way, start it as
your desktop user, then open `https://<host-ip>:47992`:
- **Linux:** the password was generated on first start — print it with
`sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password`
(SteamOS: the install script printed it; it's in `~/.config/punktfunk/web.env`).
- **Windows:** the installer showed it on its last page.
```sh
systemctl --user enable --now punktfunk-web
sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password
```
Lost it? [Forgot your password](/docs/forgot-password). Everything else about the console:
[The Web Console](/docs/web-console).
The second command prints the login password generated on first start.
- **SteamOS:** the install script already started the console and printed its URL when it finished.
It generated the login password into `~/.config/punktfunk/web.env`.
## 4. Install a client and pair it
The certificate is the host's own self-signed one, so your browser warns once — trust it and
continue. Full details: [The Web Console](/docs/web-console).
On the device you want to stream *to*, install the app — [Install a Client](/docs/install-client)
has the link for every device (Mac, iPhone/iPad/Apple TV, Linux, Windows, Android, Steam Deck), and
any Moonlight client works too once you [turn GameStream on](/docs/moonlight).
## 4. Connect and pair a client
On the device you want to stream to, use a [native Punktfunk client](/docs/clients) for the lowest
latency, or any Moonlight client:
- **Native client (Apple, Linux, Windows, Android):** install it first —
[Install a Client](/docs/install-client) has the download for every device (Steam Deck: the
[Decky plugin](/docs/steam-deck)). Then open the Punktfunk app — your host appears in the list of
hosts found on your network. Select it, and when prompted, **pair**.
- **Anything with Moonlight:** add the host (it should be discovered automatically), then pair.
To pair, the host needs to show a PIN. [Arm pairing](/docs/web-console#arm-pairing) from the web
console you opened in step 3 — the host displays a 4-digit PIN, you type it into the client, and they
trust each other from then on. Pairing is required by default. Full details:
[Pairing & Trust](/docs/pairing).
Open the app: your host is already in the list. Select it and **connect**. Back in the console, the
device appears under **Pairing → Waiting for approval** — click **Approve** and it's in, no PIN to
type. (Prefer a PIN? Click **Pair a device** in the console and type the 4-digit code into the
client.) Pairing happens once; the device reconnects on its own from then on.
Details: [Pairing & Trust](/docs/pairing).
## 5. Stream
Once paired, select the host and start streaming. The host creates a virtual display at your device's
resolution and refresh, and the picture comes up. Mouse, keyboard, and controllers flow back to the
host.
Worth knowing before you need it: on the desktop clients the stream *takes* your mouse and keyboard
when it starts, and again whenever you click into it. **Ctrl+Alt+Shift+Q** (**⌃⌥⇧Q** on macOS) hands
them back. The other in-stream shortcuts, and the mouse, touch and pen modes, are on
[Mouse, touch and pen](/docs/input).
Select the host, start streaming. The host creates a display at your device's exact resolution and
refresh rate; mouse, keyboard and controllers flow back. On a desktop client the stream takes your
mouse and keyboard — **Ctrl+Alt+Shift+Q** (⌃⌥⇧Q on a Mac) hands them back.
## Now that it works
Punktfunk does more than mirror a screen:
- Browse the host's installed games and launch one straight into the stream —
[Game library](/docs/game-library).
- Save named stream settings, bind them to a host, and start a session from a shortcut or a script —
[Profiles and links](/docs/profiles-and-links).
- Copy on one machine and paste on the other — [Shared clipboard](/docs/clipboard).
- Connect to a host that's asleep — [Wake-on-LAN](/docs/wake-on-lan).
- Get a 10-bit HDR picture where the whole chain allows it — [HDR](/docs/hdr).
## Keep it running
- Tune the picture — resolution, refresh, bitrate, codec and HDR are all
[client settings](/docs/client-settings); the host's own knobs are in
[Configuration](/docs/configuration).
- Make it always-on — no login, no monitor: [Running as a Service](/docs/running-as-a-service).
- Keep it current with [Updating](/docs/updating); changed your mind? [Uninstall](/docs/uninstall).
- Hit a snag? See [Troubleshooting](/docs/troubleshooting).
- Launch installed games straight into the stream — [Game library](/docs/game-library) (install the
[plugin](/docs/plugins) for each launcher you use).
- Tune resolution, bitrate, codec and HDR per device — [Client settings](/docs/client-settings).
- Copy here, paste there — [Shared clipboard](/docs/clipboard). Wake a sleeping host —
[Wake-on-LAN](/docs/wake-on-lan).
- Run the host headless, with nobody logged in — [Running as a service](/docs/running-as-a-service).
- Already running Sunshine or Apollo? [Switching from Sunshine](/docs/switching-from-sunshine).
- Stuck? [Troubleshooting](/docs/troubleshooting) starts from the symptom.
+38 -1
View File
@@ -35,7 +35,43 @@ so one package spans the range — which makes the gap easy to mistake for a bug
ships no compositor new enough, and no `gamescope` (the patched
[`punktfunk-gamescope`](/docs/gamescope) cannot run there either — 24.04 is too old on wayland,
libinput, libavif and pixman). The same gap is why
[Linux Mint 22.x cannot host](/docs/debian#linux-mint-22x-cannot-host-yet).
[Linux Mint 22.x cannot host](#cinnamon-linux-mint-and-lmde).
### Cinnamon, Linux Mint and LMDE
**A Cinnamon desktop cannot host a virtual display, and no setting changes that.** Punktfunk gives
each client its own screen at that device's exact resolution by asking the compositor to create a
virtual output. Cinnamon's compositor, **Muffin**, has no such API: it forked from Mutter 3.36, and
its `org.cinnamon.Muffin.ScreenCast` interface offers only `RecordMonitor` and `RecordWindow`
never the `RecordVirtual` that Mutter gained in 42. Its portal backend
(`xdg-desktop-portal-xapp`) implements no ScreenCast either, so the route that serves Sway and
Hyprland is closed too. This is upstream's to fix, not a Punktfunk setting.
Which Mint you run decides whether there is any route at all:
| Edition | Base | Can it host? |
|---|---|---|
| **LMDE 7 "Gigi"** | Debian 13 | ✅ Yes — via gamescope ([Debian page](/docs/debian#4-start-it)) |
| **Linux Mint 22.x** ("Wilma"…"Zena") | Ubuntu 24.04 | ❌ No — see below |
| **Linux Mint 23** | Ubuntu 26.04 | ✅ Expected — due December 2026 |
On **LMDE 7** what works is **gamescope**: the host starts its own headless gamescope for each
connecting client and runs the game inside it, so it needs no desktop compositor at all. Your
Cinnamon session keeps running untouched; the stream is the game, not the desktop. Install
`punktfunk-gamescope` (Debian ships no gamescope of its own; the patched build is what gives the
stream HDR, a visible cursor and the client's real refresh rate) and pin
`PUNKTFUNK_COMPOSITOR=gamescope` — auto-detection reads the live session, finds Cinnamon, and stops
with an error rather than guessing. To stream the *desktop* from an LMDE box, log into a GNOME or
Sway session instead — Debian 13 ships GNOME 48.7 and sway 1.10, both above the floors (its KDE is
KWin 6.3.6, below the 6.5.6 floor, so Plasma is not an option there yet).
**Linux Mint 22.x cannot host.** `punktfunk-host` installs, which makes this easy to miss, but
nothing on the box can produce a stream: Cinnamon can't (above); gamescope isn't packaged for Ubuntu
24.04 and the patched `punktfunk-gamescope` can't run there either (24.04 is short on wayland ≥ 1.23.1,
libinput ≥ 1.26, libavif ≥ 1.2.1, pixman ≥ 0.44, and has no `libdisplay-info2` or `libxcb-errors0`);
and switching desktop doesn't rescue it — 24.04's KWin 5.27 and GNOME Shell 46 are below the
floors, leaving only sway 1.9 as a candidate. Use **LMDE 7** on Mint hardware today, or wait for
**Mint 23**.
**Distros — install the package:**
@@ -45,6 +81,7 @@ libinput, libavif and pixman). The same gap is why
- [Arch](/docs/arch)
- [Bazzite](/docs/bazzite)
- [SteamOS](/docs/steamos-host)
- [NixOS](/docs/nixos)
**Desktops — configure and quirks:**
+83 -88
View File
@@ -3,9 +3,9 @@ title: Running as a Service
description: Start the host at boot — for a desktop you log into, or a fully headless always-on machine.
---
Running `serve` in a terminal is fine for trying Punktfunk out. To make a machine an
always-available host, run it as a service. First, what that service starts then the two cases, a
desktop you log into and a fully headless box.
Running `serve` in a terminal is fine for trying Punktfunk out; an always-available host runs as a
service. First what the service starts, then the two cases: a desktop you log into, and a headless
box.
## What the unit starts
@@ -30,8 +30,8 @@ PUNKTFUNK_GAMESTREAM=1
systemctl --user restart punktfunk-host
```
Then also open the `punktfunk-gamestream` firewall service alongside `punktfunk-native`the
firewall step in your distro guide has the exact commands.
Then open the `punktfunk-gamestream` firewall service alongside `punktfunk-native`your distro
guide's firewall step has the commands.
> **Upgrading?** Earlier releases baked `--gamestream` into the unit's `ExecStart`, so a packaged
> host served Moonlight by default. The upgrade replaces that unit with the native-only one — if
@@ -39,18 +39,19 @@ firewall step in your distro guide has the exact commands.
> `systemctl --user edit` drop-in that sets its own `ExecStart` keeps winning either way —
> `systemctl --user cat punktfunk-host` shows what is in effect.)
Windows works the same way: an install from the setup `.exe` leaves GameStream **off** unless you
tick it, configured through its own mechanism — see [Windows](#windows) below.
Windows is the same by default — GameStream **off** unless you tick it in the installer, through
its own mechanism — see [Windows](#windows).
## A. A desktop you log into
If you sit at the machine (or it auto-logs-in to a desktop), run the host as a **systemd user
service** that starts with your session.
**Put your `host.env` in place first.** The unit reads `~/.config/punktfunk/host.env` and won't start
until that file exists — no package creates it for you, they only ship a template to copy. The
defaults in it are right for an ordinary desktop; your distro and desktop guides say if yours wants
a different template (on Bazzite it's `host.env.bazzite`):
**`host.env` is optional.** The unit reads `~/.config/punktfunk/host.env` if it exists (no package
creates it — they ship templates under `/usr/share`) and runs with sane defaults without it: the host
auto-detects the live session, so an ordinary desktop needs no file. Copy a template when you want
to set a knob (the Bazzite one is `host.env.bazzite`; every knob is in
[Configuration](/docs/configuration)):
```sh
mkdir -p ~/.config/punktfunk
@@ -59,8 +60,8 @@ cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env
```
**Installed from a package** (apt, dnf, pacman, or the Bazzite sysext) — the unit is already at
`/usr/lib/systemd/user/punktfunk-host.service`, with its `ExecStart` pointing at the installed
binary. There's nothing to copy:
`/usr/lib/systemd/user/punktfunk-host.service`, its `ExecStart` pointing at the installed binary.
Nothing to copy:
```sh
systemctl --user daemon-reload # the sysext route needs this; harmless elsewhere
@@ -69,8 +70,8 @@ systemctl --user enable --now punktfunk-host
**Built from source** — install the unit from your checkout, and take `host.env` from there too
(`cp scripts/host.env.example ~/.config/punktfunk/host.env`). The unit's `ExecStart` points at
`%h/punktfunk/target/release/punktfunk-host` (`%h` is your home directory), so edit the copy if your
checkout lives somewhere else:
`%h/punktfunk/target/release/punktfunk-host` (`%h` is your home directory); edit the copy if your
checkout lives elsewhere:
```sh
mkdir -p ~/.config/systemd/user
@@ -80,19 +81,19 @@ systemctl --user enable --now punktfunk-host
```
Don't do the copy on a packaged install: a unit in `~/.config/systemd/user/` shadows the packaged
one, and the source unit points at a build tree you don't have — the service then fails with
one, and the source unit points at a build tree you don't have — the service fails with
`status=203/EXEC`.
The host now starts whenever you log in. Check it with `systemctl --user status punktfunk-host`.
The host now starts whenever you log in. Check with `systemctl --user status punktfunk-host`.
**You don't need to export anything for it.** The host finds the live compositor session itself on
every connect and works out where to reach it (`WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR`, the session bus,
sway's `SWAYSOCK`, Hyprland's instance signature) from the running compositor — so `host.env` is for
sway's `SWAYSOCK`, Hyprland's instance signature) from the running compositor — `host.env` is for
policy, not session plumbing, and `systemctl --user import-environment` is not a prerequisite.
### Restart the host with your desktop
Add one drop-in so the host follows your session's lifetime:
One drop-in makes the host follow your session's lifetime:
```sh
mkdir -p ~/.config/systemd/user/punktfunk-host.service.d
@@ -106,9 +107,8 @@ systemctl --user restart punktfunk-host
```
Without it, restarting Plasma or GNOME — a crash, a log out and back in, "restart the shell" — leaves
the host running against a compositor that no longer exists. It keeps listening and answering, and
every session after that fails at capture, which is a confusing way to find out. The drop-in makes a
compositor restart a host restart.
the host running against a compositor that no longer exists: it keeps listening and answering, and
every session after that fails at capture. The drop-in makes a compositor restart a host restart.
On **NixOS** don't copy anything — the module has the option:
@@ -118,25 +118,24 @@ services.punktfunk.host.desktopSession = true;
Skip it on the headless/appliance route below (which has its own session unit), and on **Sway or
Hyprland**, which don't hand their session to systemd: they never reach `graphical-session.target`, so
the drop-in is harmless there but does nothing. To make the host come and go with the session on
those, start it from the compositor's own config instead of enabling the unit — add
`exec systemctl --user start punktfunk-host` to your sway config, or
`exec-once = systemctl --user start punktfunk-host` to Hyprland's — and leave the unit itself
disabled (`systemctl --user disable punktfunk-host`), so it isn't also started at login.
the drop-in is harmless but does nothing. There, start the host from the compositor's own config
instead of enabling the unit — `exec systemctl --user start punktfunk-host` in your sway config, or
`exec-once = systemctl --user start punktfunk-host` in Hyprland's — and leave the unit disabled
(`systemctl --user disable punktfunk-host`), so it isn't also started at login.
## B. A headless, always-on host
To run with **no monitor and no login** — a machine in a closet that's always ready — you need two
things: a desktop session that comes up at boot, and the host service started without a login.
**No monitor and no login** — a machine in a closet that's always ready — needs two things: a
desktop session that comes up at boot, and the host service started without a login.
Start by making the host service start at boot even when nobody logs in:
First let the host service start at boot with nobody logged in:
```sh
sudo loginctl enable-linger "$USER"
```
Then bring up a session automatically. How you do that is desktop-specific — auto-login, lock
disable, and the session unit differ per compositor, so each is documented on its own page:
Then bring up a session automatically. Auto-login, lock disable and the session unit differ per
compositor, so each has its own page:
- GNOME: [GNOME → Headless session](/docs/gnome#headless-session).
- KDE Plasma: [KDE → Headless session](/docs/kde#headless-session).
@@ -147,41 +146,40 @@ disable, and the session unit differ per compositor, so each is documented on it
so every connect quietly mirrors the box's own screen — which, headless, is a black one. See
[gamescope → autologin display managers](/docs/gamescope#nobara-and-other-autologin-display-managers).
Once a session comes up at boot, enable the host user service (section A) and reboot. The host comes up
on that session.
Once a session comes up at boot, enable the host user service (section A) and reboot. The host comes
up on that session.
### Headless Bazzite
On Bazzite, the host launches its own gamescope/Steam session per client, so you don't need a separate
session unit — see [Bazzite](/docs/bazzite) and [gamescope](/docs/gamescope).
On Bazzite the host launches its own gamescope/Steam session per client, so no separate session
unit is needed — see [Bazzite](/docs/bazzite) and [gamescope](/docs/gamescope).
## Windows
> Punktfunk has first-class **Linux and Windows** hosts. On Windows it ships as a signed installer
> with an SCM service and a virtual-display driver — including Punktfunk's own **indirect display
> driver** the host pushes frames straight into. The Windows host is newer than the Linux host. (Not
> to be confused with the Windows *client*, which streams *to* a Windows PC.)
> The Windows host (newer than the Linux one; not the Windows *client*, which streams *to* a PC)
> ships as a signed installer with an SCM service and Punktfunk's own **indirect display driver**
> the host pushes frames straight into.
On Windows the host runs as a `LocalSystem` service that launches into the interactive session, so it
captures the secure desktop (UAC / lock screen) and survives reboots with nobody logged in — the same
model Sunshine/Apollo use. Because it runs at that privilege level, keep it on a trusted network and be
deliberate about which machine you host on — see [Security & Safe Use](/docs/security).
The host runs as a `LocalSystem` service that launches into the interactive session, so it captures
the secure desktop (UAC / lock screen) and survives reboots with nobody logged in — the same model
Sunshine/Apollo use. At that privilege level, keep it on a trusted network and be deliberate about
which machine you host on — see [Security & Safe Use](/docs/security).
The easy path is the **signed installer**: download `punktfunk-host-setup-<ver>.exe` from the package
registry ([`punktfunk-host-windows`](https://git.unom.io/unom/-/packages)) and run it. It drops the host
into `C:\Program Files\punktfunk`, installs the bundled **pf-vdisplay** virtual-display driver, and
registers + starts the service for you (`/VERYSILENT` for unattended). Upgrades and uninstall are
handled through Add/Remove Programs.
registers + starts the service (`/VERYSILENT` for unattended). Upgrades and uninstall go through
Add/Remove Programs.
Prefer the CLI? Run `punktfunk-host service install` from an elevated prompt — see
[Windows Host](/docs/windows-host). For hardware encode you need a GPU — NVIDIA (NVENC), AMD (AMF), or
Prefer the CLI? `punktfunk-host service install` from an elevated prompt — see
[Windows Host](/docs/windows-host). Hardware encode needs a GPU — NVIDIA (NVENC), AMD (AMF), or
Intel (QSV); the host falls back to software H.264 without one.
**GameStream on Windows.** Unlike the Linux unit, the installer leaves Moonlight compatibility
**off** — it's a checkbox in the wizard (`/MERGETASKS="gamestream"` to select it unattended). There's
no `ExecStart` to edit here: the service launches whatever `PUNKTFUNK_HOST_CMD` in
`%ProgramData%\punktfunk\host.env` says, which is also where the rest of the Windows host's
configuration lives. To change it later, from an elevated prompt:
**GameStream on Windows.** The installer leaves Moonlight compatibility **off** — a checkbox in the
wizard (`/MERGETASKS="gamestream"` to select it unattended). There's no `ExecStart` to edit: the
service launches whatever `PUNKTFUNK_HOST_CMD` in `%ProgramData%\punktfunk\host.env` says, which is
also where the rest of the Windows host's configuration lives. To change it later, from an elevated
prompt:
```powershell
punktfunk-host service install --gamestream=on # or --gamestream=off
@@ -189,9 +187,8 @@ punktfunk-host service restart
```
Registering the service by hand is the exception. A bare `punktfunk-host service install` writes a
fresh `host.env` with `PUNKTFUNK_HOST_CMD` left commented out, and with no value set the service
falls back to `serve --gamestream` so add `--gamestream=off` to that command if you want the
native-only host.
fresh `host.env` with `PUNKTFUNK_HOST_CMD` commented out, and with no value set the service falls
back to `serve --gamestream` — add `--gamestream=off` to that command for the native-only host.
> **Firewall scope.** The installer opens the streaming + console ports on **Private and Domain**
> networks only — not **Public**. If your LAN is (mis)classified Public, clients won't connect until
@@ -211,39 +208,37 @@ punktfunk hosts list --probe # every saved host, online or offline
`punktfunk` is the headless client CLI — it ships in the Linux client packages (`punktfunk-client`)
and with the Windows client. From a source checkout, `punktfunk-probe --discover` browses the LAN
instead; it's a dev tool and isn't packaged. Or just open a native client / Moonlight and look for
the host.
instead; it's a dev tool and isn't packaged. Or open a native client / Moonlight and look for the
host.
If the host answers, it's up. If not, check `journalctl --user -u punktfunk-host` on the host — on
a Windows host, run `punktfunk-host service status` from an elevated prompt on the machine itself.
Windows, `punktfunk-host service status` from an elevated prompt on the machine itself.
## GPU scheduling priority
The [PyroWave](/docs/pyrowave) codec encodes on the same GPU shader cores your game is using, so a
demanding game can crowd it out and the stream's frame rate drops with it. The fix is to ask the
driver to schedule that encode ahead of the game, and every driver we tested gates the request on a
single Linux capability, `CAP_SYS_NICE`. The other codecs use a separate video engine on the GPU and
are unaffected either way.
The [PyroWave](/docs/pyrowave) codec encodes on the same GPU shader cores your game uses, so a
demanding game can crowd it out and the stream's frame rate drops. The fix is to ask the driver to
schedule that encode ahead of the game, and every driver we tested gates the request on a single
Linux capability, `CAP_SYS_NICE`. The other codecs use a separate video engine on the GPU and are
unaffected either way.
That capability cannot live on the host, so it lives next to it. Every way of installing a Linux
host — apt, dnf, pacman, the Bazzite sysext, the NixOS module, the Steam Deck script — ships a
second, deliberately small program alongside it, **`punktfunk-encode-worker`**, and grants
`cap_sys_nice=ep` to *that*. The host starts one per PyroWave session, hands it the captured frames,
and takes the compressed video back; the worker talks to nothing else, not your desktop and not the
network.
`punktfunk-host` itself carries **no capability, on any channel** — the posture it has had since
0.25.0, and the one KDE needs.
second, deliberately small program, **`punktfunk-encode-worker`**, and grants `cap_sys_nice=ep` to
*that*. The host starts one per PyroWave session, hands it the captured frames, and takes the
compressed video back; the worker talks to nothing else, not your desktop and not the network.
`punktfunk-host` itself carries **no capability, on any channel** — its posture since 0.25.0, and
the one KDE needs.
> **Never `setcap` `punktfunk-host`.** Not by hand, not through a systemd `AmbientCapabilities=`
> line, not through a NixOS `security.wrappers` entry. All three put the capability in the same
> place, and all three take KDE desktop streaming away completely. There is no capability the host
> wants: the worker is the thing that needs one, and your packages already gave it one.
> wants: the worker needs one, and your packages already gave it one.
Here is why a privileged host is so much worse than a privileged worker. To hand the host its
virtual display, KWin first has to work out *which* program is asking, which it does by reading the
connecting process's `/proc/<pid>/exe` and matching it against the `.desktop` file the packages
install. Linux refuses that read unless the reader already holds every capability the target holds —
and KWin holds none. So a host carrying a capability is a host KWin cannot identify, its restricted
Why: to hand the host its virtual display, KWin first works out *which* program is asking, by
reading the connecting process's `/proc/<pid>/exe` and matching it against the `.desktop` file the
packages install. Linux refuses that read unless the reader holds every capability the target holds
— and KWin holds none. So a host carrying a capability is one KWin cannot identify, its restricted
protocols are never offered, and every session fails at capture with:
```
@@ -251,11 +246,11 @@ KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to
```
which reads exactly like a missing or mis-installed `.desktop` file and survives reinstalling both
ends. The first 0.26.0 packages granted the host the capability for the reason above and shipped
precisely this, on every Linux channel; the grant was revoked everywhere later the same day, and
0.27.0 is the first release whose version number carries that revocation to a machine that already
installed one of them. If you ever see that error, check the binaries before anything else — the
host's own message names the capability when it finds one:
ends. The first 0.26.0 packages granted the host the capability and shipped precisely this, on every
Linux channel; the grant was revoked everywhere later the same day, and 0.27.0 is the first release
whose version number carries that revocation to a machine that already installed one of them. If
you see that error, check the binaries first — the host's own message names the capability when it
finds one:
```sh
getcap /usr/bin/punktfunk-host # correct output is nothing at all
@@ -266,8 +261,8 @@ sudo setcap -r /usr/bin/punktfunk-host # clear it, then restart the host
On the Bazzite image `/usr` is read-only, so there is nothing to repair in place — take the next
image (`sudo punktfunk-sysext update`). On NixOS the worker *is* wrapped, because a file capability
cannot live on a read-only store path: the module creates the wrapper and points the host at it, and
the host's own `ExecStart` stays on the plain store path.
cannot live on a read-only store path: the module creates the wrapper and points the host at it,
and the host's own `ExecStart` stays on the plain store path.
**The grant is best-effort, and no session depends on it.** A worker without the capability still
encodes — it asks for the elevated priority, is refused, says so once, and runs at the normal one.
@@ -279,10 +274,10 @@ keeps the encode in the host process — both on
## Stopping and removing
After a Linux package update the user service keeps running the old binary until it's restarted, and
a package can't restart another user's `--user` units for you — [Updating](/docs/updating) has the
update command for every install method and the restart that finishes the job. The Windows installer
restarts its own service.
After a Linux package update the user service keeps running the old binary until restarted, and a
package can't restart another user's `--user` units for you — [Updating](/docs/updating) has the
update command for every install method and the restart that finishes the job. The Windows
installer restarts its own service.
To stop the host for now:
@@ -301,7 +296,7 @@ sudo loginctl disable-linger "$USER" # only if you enabled l
```
On Windows, `punktfunk-host service uninstall` from an elevated prompt stops the service, removes it,
and removes the firewall rules it added. To remove the whole install instead, use Add/Remove Programs.
and removes the firewall rules it added. To remove the whole install, use Add/Remove Programs.
Neither removes `~/.config/punktfunk` (Linux) or `%ProgramData%\punktfunk` (Windows) — your
certificate, pairings and console password stay, so a reinstall picks up where you left off. See
+27 -15
View File
@@ -108,6 +108,23 @@ pinned. The layers, from the outside in:
rather than a global one — because letting a machine read and write what you copy is a decision
about that machine (Android is the one client where that switch starts on).
### Pairing policy: open hosts and trust-on-first-use
By default the host **requires** pairing: a device that hasn't completed the PIN ceremony (or been
approved from the console) can't stream. That's the right setting on any shared network.
`serve --open` turns it off. The host then advertises `pair=optional` and accepts unpaired clients,
and clients offer a **trust-on-first-use** (TOFU) path: the first connect shows the host's
fingerprint, you confirm it against the one the host logged at startup, and the client pins it. TOFU
cannot detect an impostor on that first connection — if someone is impersonating the host the very
first time, you pin the attacker. PIN pairing closes that gap (the SPAKE2 ceremony binds both
identities), which is why it's the default. Clients never offer TOFU to a host advertising
`pair=required`, a host typed in by hand, or one whose policy is unknown — those route straight to
the PIN. Once pinned, a fingerprint change forces re-pairing rather than re-trust.
(The developer/measurement host, `punktfunk-host punktfunk1-host`, has its own `--allow-tofu` /
`--pairing-pin` flags for test harnesses; nothing here applies to it.)
### GameStream / Moonlight compatibility is the weak-crypto path
To interoperate with stock [Moonlight](/docs/moonlight) clients, Punktfunk can also speak the legacy
@@ -117,19 +134,14 @@ could sit in the middle of a pairing, or recover input from a session. It is saf
fully trust. The native `punktfunk/1` plane is unaffected and always on; GameStream is a second
plane running beside it.
**Check whether your host has it on — the default differs by install path, and most Linux installs
have it on:**
- **Linux packages: on.** The bundled systemd user unit runs `serve --gamestream`, and the deb, rpm,
Arch and Bazzite sysext packages all install that unit as it ships. If you installed Punktfunk from
a package and enabled the service, GameStream is on.
- **SteamOS / Steam Deck installer: on**, unless you ran it with `--no-gamestream`.
- **NixOS module: on**`services.punktfunk.host.gamestream` defaults to `true`.
- **Windows installer: off.** The wizard's GameStream checkbox is unticked by default, and an upgrade
leaves your existing setting alone. One exception: a service you register by hand with a bare
`punktfunk-host service install` gets the built-in default, which *is* `serve --gamestream`.
- **A host you started yourself** with `punktfunk-host serve`: off. Only `serve --gamestream` (or
`--moonlight`) enables it.
**It is off by default on every install route** — the Linux packages' unit runs the native-only
`serve`, the SteamOS installer, the NixOS module and the Windows installer all leave it off, and a
host you start yourself with `punktfunk-host serve` has it off. It comes on only when you ask:
`PUNKTFUNK_GAMESTREAM=1` in `host.env`, the installer's checkbox on Windows, `--gamestream` on SteamOS,
`services.punktfunk.host.gamestream = true` on NixOS, or `serve --gamestream` by hand — see
[Moonlight](/docs/moonlight). One exception: a Windows service you register by hand with a bare
`punktfunk-host service install` gets the built-in default, which *is* `serve --gamestream`. (Installs from before the opt-in change served Moonlight by default; an
upgrade switches them to native-only until you set the knob.)
The host doesn't hide it: whenever the compat planes come up, it logs a warning at startup naming
exactly this risk.
@@ -241,8 +253,8 @@ Install plugins only from sources you trust, and prefer Verified catalog entries
- **Restrict who can reach TCP 47992** at the host firewall if your LAN is shared — that port is the
console, and the console is remote administration.
- **Stay on a trusted network** — LAN or VPN. Never port-forward to the internet.
- **Turn GameStream off** unless you specifically need Moonlight compatibility — every Linux package
ships it **on**, so on Linux this is something you do, not something you leave alone. See
- **Leave GameStream off** unless you specifically need Moonlight compatibility — it's opt-in on
every route; if you turned it on for a trial, turn it back off. See
[above](#gamestream--moonlight-compatibility-is-the-weak-crypto-path).
- **Only install plugins you trust** — prefer Verified catalog entries.
- **Review paired devices** in the web console periodically; remove anything you don't recognize.
+44 -48
View File
@@ -4,27 +4,26 @@ description: Install the Punktfunk Decky plugin to discover, pair, and stream fr
---
The **Decky plugin** adds a **Punktfunk** panel to the Steam Deck's Quick Access Menu (the `…`
button), so you can find a host, pair, and start streaming **without leaving Gaming Mode**. It's the
couch-friendly front end for the Steam Deck — built from real Steam UI, gamepad-navigable end to end.
button), so you can find a host, pair, and start streaming **without leaving Gaming Mode**
gamepad-navigable end to end.
The plugin is a **launcher**, not a second client. It doesn't decode video, browse your library or
hold settings of its own — it starts the regular
[Linux client](/docs/clients#linux-desktop-client-gtk4) (usually the `io.unom.Punktfunk` Flatpak)
the way gamescope needs so it fullscreens correctly. Everything the panel doesn't do is one tap
away in that client's own gamepad UI. So the Deck has two ways to stream, and they share one
client + one paired identity:
the way gamescope needs so it fullscreens correctly; everything the panel doesn't do is one tap
away in that client's own gamepad UI. So the Deck has two ways to stream, sharing one client and
one paired identity:
- **Gaming Mode** → the **Decky plugin** (this page).
- **Desktop Mode** → run the [Flatpak](/docs/install-client#steam-deck) directly, like any Linux app.
## Before you start
You need three things on the Deck:
Three things on the Deck:
1. **Decky Loader**the plugin loader. Install it from [decky.xyz](https://decky.xyz/) if you
haven't already.
2. **A Punktfunk client on the Deck** — the plugin doesn't decode video itself, it launches a
client. On a normal Deck that's the Flatpak, installed once in **Desktop Mode**:
1. **Decky Loader**install it from [decky.xyz](https://decky.xyz/) if you haven't already.
2. **A Punktfunk client on the Deck** — the plugin launches a client rather than decoding video
itself. On a normal Deck that's the Flatpak, installed once in **Desktop Mode**:
```sh
flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref
@@ -34,18 +33,18 @@ You need three things on the Deck:
no Flatpak but a native `punktfunk-client` — a sysext, a distro package, a nix profile, your own
build — the plugin uses that instead; with both installed the Flatpak wins, unless
`PF_DECKY_CLIENT=native` (or `flatpak`) is set in the plugin backend's environment. Both kinds
share `~/.config/punktfunk`, so your identity, known hosts and settings are the same either way.
share `~/.config/punktfunk`, so identity, known hosts and settings are the same either way.
**The client must be v0.22.0 or newer.** The panel drives everything through the client's
headless `punktfunk` command, which shipped in that release. An older client says so in the
panel, with the update button that fixes it right there.
3. **A Punktfunk host** running on your LAN — see [Install the Host](/docs/install). The Deck finds
it automatically over mDNS, so nothing to configure here.
3. **A Punktfunk host** on your LAN — see [Install the Host](/docs/install). The Deck finds it
over mDNS, so nothing to configure.
## Install the plugin
The plugin is published as a ready-to-install zip on every build. You don't need the Decky CLI or a
developer toolchain just paste a URL into Decky:
The plugin is published as a ready-to-install zip on every build — no Decky CLI or developer
toolchain, just a URL pasted into Decky:
1. On the Deck, open the **Quick Access Menu** (`…`) → the **plug** icon (Decky) → the **gear**
(Settings) → enable **Developer Mode**.
@@ -73,9 +72,8 @@ stream — plus a door into the client's own gamepad UI for everything else.
- **Hosts** — hosts on your network appear automatically (mDNS), alongside the ones you've already
saved. A saved host is also probed directly, so a box reached over a VPN or Tailscale shows as
online even though it never advertises. Tap **Refresh** to rescan. The list sorts online hosts
first, then whichever you streamed most recently. A lock icon means the host still has to let
this Deck in.
online even though it never advertises. Tap **Refresh** to rescan. Online hosts sort first, then
whichever you streamed most recently. A lock icon means the host still has to let this Deck in.
- **Let a host in** — tapping a locked host opens a small sheet with two ways through:
- **Request access** — no PIN at all. See [Request access](#request-access) below.
- **Use a PIN instead** — [arm pairing on the host](/docs/pairing) (its console or web console
@@ -83,27 +81,27 @@ stream — plus a door into the client's own gamepad UI for everything else.
Either way the host is remembered, so the next connection is silent.
- **Stream** — tap a host and the stream launches fullscreen in Gaming Mode. The plugin drives a
hidden Steam shortcut behind the scenes so gamescope focuses and fullscreens it.
hidden Steam shortcut so gamescope focuses and fullscreens it.
- **Sleeping host?** Streaming sends a [Wake-on-LAN](/docs/wake-on-lan) packet and waits for the
host to actually come back before dialling, so a stream survives a resume from sleep. Nothing to
enable — it's a no-op until the client has learned that host's MAC address, and the packet only
lands if the host machine is armed to wake in its BIOS and its network card.
- **Pinned cards** — a host with pinned [settings profiles](/docs/client-settings) shows them
nested underneath it as `▸ <Profile name>`. Tapping one streams that host with that profile
applied — your "4K on the TV" and "battery saver" presets, one tap each. Pins are made in the
applied — "4K on the TV" and "battery saver" presets, one tap each. Pins are made in the
Punktfunk app (or any other client) and shared across all of them; the panel shows them, it
doesn't create them.
- **Open Punktfunk** — opens the client's console home: the host picker, adding a host by address,
pairing, browsing a host's [game library](/docs/game-library), and the **full settings screen**.
This is where resolution, bitrate, codec, audio, controllers and the stats overlay live.
- **Library entry** — a visible, branded **Punktfunk** app also appears in your Steam library, and
pairing, browsing a host's [game library](/docs/game-library), and the **full settings screen**
resolution, bitrate, codec, audio, controllers and the stats overlay.
- **Library entry** — a visible, branded **Punktfunk** app also appears in your Steam library;
launching it opens that same console home — it does not resume a stream. If it ever disappears,
the Quick Access Menu panel has a button to put it back.
> **Where did the plugin's settings tab go?** Into the app, at **Open Punktfunk → Settings** — the
> same rows over the same settings, gamepad-navigable, and one tap from the same panel. The plugin
> used to carry its own copy of that screen, which meant two places to change one setting and a
> copy that fell behind. There is now one.
> same rows over the same settings, gamepad-navigable, one tap from the same panel. The plugin's
> own copy of that screen meant two places to change one setting, and one fell behind; there is
> now one.
With **Controller type** on *Automatic* the Deck's built-in controller is forwarded as a **Steam
Deck** pad (paddles, both trackpads, gyro) — that needs Steam Input set to **Off** for Punktfunk
@@ -118,25 +116,24 @@ screen.
Tap the host → **Request access**. The Deck says *"Approve this Deck in <host>'s console — the
stream starts by itself"*, and the stream opens and waits. The moment somebody approves it, the
picture comes up — no going back to the panel, nothing else to tap. If nobody approves within
about three minutes, it gives up like any failed connection and you can try again or use a PIN.
picture comes up — nothing else to tap. If nobody approves within about three minutes, it gives up
like any failed connection and you can try again or use a PIN.
It's the better option when you're not the person sitting at the host, or when reading a PIN off
another screen is awkward. Two things to know:
- The host must be **advertising on your network** for this to be offered. A host you added by
address (a VPN box, another subnet) has no advertised identity for the Deck to pin, so the sheet
offers the PIN path only and says so. That's a safety rule, not a limitation to work around:
pinning the advertised identity is what stops something else answering in the host's place while
the Deck waits.
offers the PIN path only and says so. That's a safety rule: pinning the advertised identity is
what stops something else answering in the host's place while the Deck waits.
- Once approved, the host shows as **paired** and every later stream connects silently.
> **Steam Input off is a trade-off, not a free win.** The plugin installs a Steam Input layout
> called **Punktfunk** and points its shortcuts at it, and that layout's whole job is making the
> Deck's touchscreen arrive at the stream as *real touch*. Leaving Steam Input **On** with that
> layout gives you native touch plus a standard gamepad; setting it **Off** gives you the full Steam
> Deck pad — paddles, both trackpads, gyro — but the touchscreen stops working as touch. Pick per
> game, on the game page → ⚙ → **Controller Settings**.
> called **Punktfunk** and points its shortcuts at it; that layout's whole job is making the Deck's
> touchscreen arrive at the stream as *real touch*. Leaving Steam Input **On** with that layout
> gives you native touch plus a standard gamepad; setting it **Off** gives you the full Steam Deck
> pad — paddles, both trackpads, gyro — but the touchscreen stops working as touch. Pick per game,
> on the game page → ⚙ → **Controller Settings**.
To **leave a stream**: **hold [L1 + R1 + Start + Select](/docs/input#leaving-with-a-controller)**
for about a second and a half, or close the "game" from the Steam overlay. Either ends the session
@@ -144,25 +141,24 @@ and drops you straight back to Gaming Mode. A quick press of the same four only
input, so it is safe to hit by accident.
**The Steam and `…` buttons stay with the Deck while streaming.** SteamOS opens its own menus for
them no matter what, so forwarding the raw press as well opened *both* menus at once — the Deck's
covering the stream. To reach the **host's** menus instead: **hold Select** for the host's Steam
menu ([how it works](/docs/input#the-guide-button-xbox--ps--steam-and-quick-access)), or open the
them no matter what, so forwarding the raw press too opened *both* menus at once — the Deck's
covering the stream. To reach the **host's** menus: **hold Select** for the host's Steam menu
([how it works](/docs/input#the-guide-button-xbox--ps--steam-and-quick-access)), or open the
Punktfunk panel — while a stream runs it grows a **Host menus** section whose two buttons,
**Steam menu on host** and **Quick access on host**, press the button on the host and close the
Deck's own menu so the host's shows through. Want the raw forwarding back? **Open Punktfunk →
Settings → Steam / guide button** → *Send to host*.
Deck's own menu so the host's shows through. Raw forwarding back: **Open Punktfunk → Settings →
Steam / guide button** → *Send to host*.
## Updating
The plugin **checks for updates itself** — no Decky store needed. It covers **both** the plugin *and*
the streaming client (they version independently), so when either has a newer build the panel shows an
**Update** button at the top of the panel. Tap it: the client updates in place, and if the plugin
itself changed it downloads, verifies, replaces itself, and reloads — all without leaving Gaming
Mode.
the streaming client (they version independently): when either has a newer build the panel shows
an **Update** button at the top. Tap it: the client updates in place, and if the plugin itself
changed it downloads, verifies, replaces itself, and reloads — without leaving Gaming Mode.
One exception: if your client isn't one the plugin can install for you (a sysext, a nix profile, a
source build), the panel shows you the update **command** instead of a button — tap-to-install would
only fail. A pending plugin update still gets its button.
One exception: if your client isn't one the plugin can install (a sysext, a nix profile, a source
build), the panel shows the update **command** instead of a button. A pending plugin update still
gets its button.
The plugin check follows the [channel](/docs/channels) you installed from: a plugin installed from the
**stable** link tracks stable releases; one installed from the **canary** link tracks `main` builds.
@@ -53,19 +53,11 @@ Vulkan encode behind that flag, and without it sessions quietly fall back to VAA
gamescope clone). Budget around 25 minutes for a first install. Later rebuilds are incremental,
and the gamescope step is a no-op unless its sources changed.
## 1. Get the source
## 1. Get the source and run the installer
In Desktop Mode open **Konsole** (or ssh in), then:
```sh
git clone https://git.unom.io/unom/punktfunk ~/punktfunk
```
## 2. Run the installer
```sh
bash ~/punktfunk/scripts/steamdeck/install.sh
```
<Install platform="steamos" />
It is idempotent — safe to re-run. In one pass it:
@@ -88,19 +80,17 @@ Useful flags:
| Flag | Effect |
|------|--------|
| `--open` | Accept **unpaired** clients (trust-on-first-use) — convenient on a fully trusted LAN. Default is PIN pairing required. |
| `--no-gamestream` | Run a **secure native-only** host — skip the GameStream/Moonlight-compat planes (see below). Default keeps them on so stock Moonlight works. |
| `--gamestream` | Also serve stock [Moonlight](/docs/moonlight) clients (GameStream compat, off by default — see below). `--no-gamestream` is the explicit off, kept for old command lines. |
| `--no-web` | Skip the management web console. |
| `--src=DIR` | Build from source at `DIR` instead of `~/punktfunk`. **Only if you must** — the post-OS-update rebuild check and the web console's one-click update both look for `~/punktfunk/scripts/steamdeck/update.sh`, so with a relocated source you have to update by hand: `PUNKTFUNK_SRC=DIR bash DIR/scripts/steamdeck/update.sh --pull` (without `PUNKTFUNK_SRC` the script still looks for `~/punktfunk`). A symlink (`ln -s DIR ~/punktfunk`) restores both. |
When it finishes it prints the web-console URL and how to pair.
> **GameStream/Moonlight compat is on by default.** The native `punktfunk/1` plane (used by
> Punktfunk's own clients — SPAKE2 PIN pairing, per-direction AEAD) is **always on** and is the secure
> path. The installer also enables the **GameStream/Moonlight-compat planes** so stock
> [Moonlight](/docs/moonlight) works — but those carry inherent on-path weaknesses (pairing over plain
> HTTP; legacy control encryption that can reuse GCM nonces), so enable them only on a **trusted LAN**.
> If you only ever use native clients, install with `--no-gamestream` for a host with no GameStream
> surface at all.
> **GameStream/Moonlight compat is opt-in.** The native `punktfunk/1` plane (used by Punktfunk's
> own clients — SPAKE2 PIN pairing, per-direction AEAD) is **always on** and is the secure path. Pass
> `--gamestream` to also enable the **GameStream/Moonlight-compat planes** so stock
> [Moonlight](/docs/moonlight) works — they carry inherent on-path weaknesses (pairing over plain
> HTTP; legacy control encryption that can reuse GCM nonces), so do that only on a **trusted LAN**.
> **First install — reboot once before streaming.** KWin only authorizes Desktop-mode screen capture
> on a fresh session, and the new `input` and `punktfunk` groups (native Steam Deck controller
@@ -111,11 +101,11 @@ When it finishes it prints the web-console URL and how to pair.
> session ends with `KWin does not expose zkde_screencast_unstable_v1` or the pad shows up as an Xbox
> 360 controller, you haven't rebooted yet.
## 3. Pair a device
## 2. Pair a device
By default the host **requires PIN pairing** (secure). Two ways to pair:
- **Web console** (printed at the end of step 2): open `https://<device-ip>:47992` (self-signed host
- **Web console** (printed at the end of step 1): open `https://<device-ip>:47992` (self-signed host
cert — your browser warns once; trust it and continue),
[arm pairing](/docs/web-console#arm-pairing), and enter the PIN on your client.
- **From the client directly**: pick this host (it advertises over mDNS as `_punktfunk._udp`) and
@@ -125,7 +115,7 @@ On a trusted home LAN you can instead install with `--open` and skip pairing ent
### Console login password
The installer generates a random console login password (printed at the end of step 2) and writes it
The installer generates a random console login password (printed at the end of step 1) and writes it
to `~/.config/punktfunk/web.env`. To read it back or set your own, see
[The Web Console](/docs/web-console#login-password).
@@ -136,15 +126,15 @@ to `~/.config/punktfunk/web.env`. To read it back or set your own, see
> already readable. Change `PUNKTFUNK_UI_PASSWORD` in `~/.config/punktfunk/web.env`, then
> `systemctl --user restart punktfunk-web`.
## 4. Verify
## 3. Verify
```sh
systemctl --user status punktfunk-host # active (running)
journalctl --user -u punktfunk-host -f # watch a client connect
```
Connect from a [native client](/docs/clients), or from [Moonlight](/docs/moonlight) (unless you
installed with `--no-gamestream`). In Game Mode the host attaches to the running gamescope session and
Connect from a [native client](/docs/clients), or from [Moonlight](/docs/moonlight) (if you
installed with `--gamestream`). In Game Mode the host attaches to the running gamescope session and
streams it at your client's resolution; in Desktop Mode it streams the KDE desktop. The host
auto-detects which session is live per connection. See [Steam / gamescope](/docs/gamescope) for the
attach-vs-managed detail and known limits.
+1 -1
View File
@@ -114,7 +114,7 @@ mechanics. The one exception is a gamescope the host only *attaches* to, which k
`RecordMonitor` / `RecordWindow` and never Mutter 42's `RecordVirtual`, and its portal backend
(`xdg-desktop-portal-xapp`) implements no ScreenCast at all. Nothing in Punktfunk can change
this. A Mint or LMDE box can still stream **games** through a headless gamescope, which needs no
desktop compositor — see [Debian → Cinnamon](/docs/debian#cinnamon-linux-mint-and-lmde).
desktop compositor — see [Requirements → Cinnamon](/docs/requirements#cinnamon-linux-mint-and-lmde).
### Input, cursor and HDR
@@ -0,0 +1,83 @@
---
title: Switching from Sunshine, Apollo or Vibeshine
description: Run Punktfunk next to your existing Sunshine-family host while you try it — one port to move — then what maps to what, and how to migrate for good.
---
Sunshine and its forks (**Apollo**, **Vibeshine**, Vibepollo, LuminalShine, …) are Moonlight hosts.
Punktfunk is a different protocol with a Moonlight-compatible mode on the side — so the two can
live on one machine while you decide, as long as you know which port they both want.
## Can I keep Sunshine installed while I try it?
**Yes, with one setting.** Out of the box Punktfunk runs **native-only**: Punktfunk clients, its own
discovery, and a management API. In that mode the only port it shares with a Sunshine-family host
is **TCP 47990** — Sunshine's web UI, Punktfunk's management API. Whoever starts first gets it, and
the loser isn't symmetric: Sunshine merely loses its config page, Punktfunk treats it as fatal and
exits. That's why a shared box can "work until one day it doesn't" — it's a boot race. Move ours:
```sh
# ~/.config/punktfunk/host.env (Windows: %ProgramData%\punktfunk\host.env)
PUNKTFUNK_MGMT_BIND=0.0.0.0:47991
```
Then restart the host (`systemctl --user restart punktfunk-host`; Windows: `punktfunk-host service
restart`). Nothing else changes: clients learn the port from discovery; the web console, the plugin
runner and the tray read it from `mgmt-endpoint`, which the host rewrites on every start. (Or move
the other host instead — Sunshine and its forks derive every port from one base setting.) Two
caveats: a host you added to a client **by IP address** assumes the default port, so re-add it from
discovery; and if you run a firewall, the `punktfunk-native` profile opens the default port, so allow
the new one too ([Ports & firewall](/docs/ports)).
**On Windows** there's a second overlap: Punktfunk's default display topology is *exclusive* — while
streaming it switches the other displays off so its virtual one is the whole desktop, and re-asserts
that every two seconds. Apollo-family forks are virtual-display-driven, so *their* monitor is what
keeps getting switched off. Pick a different topology in the console (**Host → Virtual displays**) or
set `PUNKTFUNK_NO_ISOLATE=1` — [Virtual displays](/docs/virtual-displays).
**Leave Moonlight compat off while both are installed.** With `PUNKTFUNK_GAMESTREAM=1` Punktfunk
binds the same fixed GameStream ports as Sunshine and advertises the same mDNS name — only one
GameStream host can run at a time. Your Moonlight clients keep talking to Sunshine until you switch;
use a [native Punktfunk client](/docs/install-client) to try Punktfunk meanwhile.
To see what's there and who holds the port:
```sh
punktfunk-host detect-conflicts # lists Sunshine-family installs; exit 1 only if one runs or autostarts
ss -lptn 'sport = :47990' # Linux — who has the port right now
netstat -ano | findstr :47990 # Windows
```
A dormant leftover (files on disk, a disabled service) is listed but doesn't fail the check — the
Windows installer and the host's startup log use the same rule. Running both at once is tolerated
for a trial, not supported: if something's odd, stop the other host first.
## What maps to what
| In Sunshine / Apollo | In Punktfunk |
|---|---|
| Web UI on 47990 | [Web console](/docs/web-console) on 47992 — pairing, status, library, displays, logs, updates |
| PIN pairing from the web UI | Same, plus **Approve** without a PIN: connect from the device, approve it in the console ([Pairing](/docs/pairing)) |
| Moonlight clients | Still work, once you turn GameStream compat on ([Moonlight](/docs/moonlight)); the native apps for Mac, iPhone/iPad/Apple TV, Linux, Windows, Android and the Steam Deck are faster and get every feature |
| A virtual display driver (SudoVDA, Vibeshine's, …) | Built in — a display per client at its exact resolution and refresh, on Linux via the compositor, on Windows via Punktfunk's own driver ([Virtual displays](/docs/virtual-displays)) |
| `apps.json` | The [game library](/docs/game-library): launchers come in through [plugins](/docs/plugins) (Steam, Lutris, Heroic, Epic, GOG, Playnite, ROMs), custom titles from the console |
| Per-app prep/undo commands | [Per-app prep/undo](/docs/automation#per-app-prepundo), plus events and hooks |
| `sunshine.conf` | `host.env` for host knobs ([Configuration](/docs/configuration)), the console for display and library policy |
| HDR via the VDD | [HDR](/docs/hdr) — Windows out of the box, Linux on gamescope / GNOME 50 |
| Clipboard, wake-on-LAN | [Shared clipboard](/docs/clipboard), [Wake-on-LAN](/docs/wake-on-lan) |
## Migrating for good
1. **Install** Punktfunk for your system ([Install the Host](/docs/install)) and move the port as
above. Pair a native client and stream for a while.
2. **Bring your library over** — install the [plugin](/docs/plugins) for each launcher you had in
`apps.json`; add anything custom from the console's **Library** page.
3. **Stop and uninstall the other host** — e.g. `sudo systemctl disable --now sunshine` on Linux,
`sc stop SunshineService` then its uninstaller on Windows. On Windows also remove its virtual
display driver, so only one driver claims the desktop.
4. **Optionally turn on Moonlight compat** for clients that don't have a native app
(`PUNKTFUNK_GAMESTREAM=1`, open the `punktfunk-gamestream` firewall profile — [Moonlight](/docs/moonlight)).
Your Moonlight clients pair again, against Punktfunk this time.
5. **Undo the port move** if you like — delete the `PUNKTFUNK_MGMT_BIND` line and restart; clients
relearn the port from discovery.
Something not behaving? [Troubleshooting → Another streaming host is installed](/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed).
+105 -60
View File
@@ -5,63 +5,26 @@ description: Common problems setting up or using a Punktfunk host, and how to fi
## Another streaming host (Sunshine, Apollo, …) is installed
Punktfunk is a Moonlight-compatible host. So are **Sunshine** and its forks (**Apollo**,
**Vibeshine**, **Vibepollo**, **LuminalShine**, …). Running one of them **at the same time** as
Punktfunk is **not supported**: they bind the *same* GameStream ports (47984/47989 and
4799848010, plus a web UI on 47990 that collides with Punktfunk's management API), advertise the
*same* `_nvstream` mDNS name, and often install a *conflicting virtual-display driver*. The result
is `address already in use` errors, pairing that silently fails, the wrong host answering a client,
and capture/display glitches.
Punktfunk's Moonlight-compatible mode and **Sunshine** / its forks (**Apollo**, **Vibeshine**,
Vibepollo, LuminalShine, …) bind the *same* GameStream ports, advertise the *same* `_nvstream` mDNS
name, and on Windows often install a *conflicting virtual-display driver* — and even native-only,
Punktfunk's management API and their web UI both want **TCP 47990**. The symptoms: `address already
in use` in the host log, pairing that silently fails, the wrong host answering a client, display
glitches, or a host that "worked until one day it didn't" (a boot race for 47990: Punktfunk exits
when it loses, Sunshine just loses its config page).
- Punktfunk detects this automatically. It warns in the host's startup log — so it's on the web
console's **Logs** page — and carries the finding in the status summary the management API
serves. The Windows installer additionally warns before installing, but only when a competing
host's *service* is set to start on its own; a dormant install isn't flagged. The tray icon
doesn't flag it at all. To check on demand, run:
```sh
punktfunk-host detect-conflicts
```
It lists any conflicting host found (installed or running) and exits non-zero if there is one.
- **Fix:** stop and uninstall the other host, then start Punktfunk — e.g. stop the service
(`sudo systemctl disable --now sunshine` / on Windows `sc stop SunshineService`) and uninstall it.
If you only want to try Punktfunk without removing the other host, at least make sure the other
host is fully **stopped** first (they cannot both run at once).
### If you must run both anyway
Still unsupported, and you are on your own for the parts below — but if you keep Punktfunk's
GameStream compat **off** (the default), the overlap narrows to two things you can move.
1. **The port.** With compat off, the Punktfunk *host* binds only UDP 9777, UDP 5353 and TCP
**47990** — the web console is a separate service on 47992/47993, which nothing else wants — and
47990 is the only one the other host wants, as its web UI. Whoever starts first takes it; the
loser is not symmetric, because Punktfunk treats the failure as fatal and exits (the streaming
plane goes with the console), while Sunshine merely loses its config UI. That is why it can look
like it "worked until one day it didn't" — it is a boot race, not a setting. Move ours:
```sh
# ~/.config/punktfunk/host.env
PUNKTFUNK_MGMT_BIND=0.0.0.0:47991
```
Nothing else needs changing: clients learn the port from discovery, and the web console, the
plugin runner (so every library plugin) and the status tray read it from
`~/.config/punktfunk/mgmt-endpoint` (`%ProgramData%\punktfunk\mgmt-endpoint` on Windows), which
the host rewrites on every start. A host added
manually **by IP address** is the exception — it assumes 47990 and its library will stop loading,
so re-add it from discovery. (You can move the other host instead: Sunshine and its forks derive
every port from one base setting.)
2. **The display, on Windows.** Punktfunk defaults to an *exclusive* topology — while streaming it
disables the other displays so its virtual one is the whole desktop, and re-asserts that every
two seconds. Apollo-family forks are virtual-display-driven, so their monitor is what gets
switched off, repeatedly. Set `PUNKTFUNK_NO_ISOLATE=1`, or pick a different topology in the web
console, before blaming the other host.
To see who currently holds the port: `ss -lptn 'sport = :47990'` on Linux,
`netstat -ano | findstr :47990` on Windows.
- **Check:** `punktfunk-host detect-conflicts` lists every Sunshine-family host it finds and exits
**1 only if one is running or set to start on its own** — a dormant leftover (files on disk, a
disabled service) prints but exits 0. The host logs the same finding at startup (so it's on the
console's **Logs** page) and carries it in the status summary; the Windows installer warns
before installing on the same rule. `ss -lptn 'sport = :47990'` (Linux) /
`netstat -ano | findstr :47990` (Windows) shows who holds the port right now.
- **Fix:** stop and uninstall the other host (`sudo systemctl disable --now sunshine`; on Windows
`sc stop SunshineService`, then its uninstaller and its display driver), then start Punktfunk.
- **Keeping both for a while?** Leave GameStream compat off and move Punktfunk's management port
(`PUNKTFUNK_MGMT_BIND`), and on Windows pick a non-exclusive display topology — the whole recipe,
and what maps to what when you migrate, is on
[Switching from Sunshine](/docs/switching-from-sunshine).
## The host isn't found on the network
@@ -193,10 +156,66 @@ accept the one-time ~2.5 s punch-timeout, or not run a host firewall on a truste
## `nvidia-smi` says it can't communicate with the driver
- The NVIDIA kernel module didn't load. With **Secure Boot** enabled, enrol the module's signing key:
`sudo mokutil --import /var/lib/shim-signed/mok/MOK.der`, reboot, **Enrol MOK** at the blue screen
(or disable Secure Boot). On Fedora, follow RPM Fusion's Secure Boot steps.
- After a kernel update the module may need a rebuild — reinstall the driver package.
The NVIDIA kernel module didn't load. With **Secure Boot** enabled (`mokutil --sb-state`), the
module is signed with a locally generated key that must be enrolled once — or disable Secure Boot in
firmware, fine for a dedicated box. Import the key, reboot, and on the blue **MOK Manager** screen
(on the machine's own console, not over SSH) choose *Enroll MOK → Continue → Yes → (the password)
→ Reboot*:
```sh
sudo mokutil --import /var/lib/shim-signed/mok/MOK.der # Ubuntu
sudo mokutil --import /var/lib/dkms/mok.pub # Debian (DKMS-built module)
sudo akmods --force && sudo mokutil --import /etc/pki/akmods/certs/public_key.der # Fedora (akmod)
```
After a kernel update the module may need a rebuild — reinstall the driver package. Then confirm
`nvidia-smi` loads and `cat /sys/module/nvidia_drm/parameters/modeset` prints `Y` (Wayland needs
KMS; if it doesn't, `echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf`,
regenerate the initramfs, reboot).
## No video on Fedora: NVENC fails (ffmpeg-libs is missing)
Fedora's own `ffmpeg-free` is built **without NVENC**, and RPM Fusion's `ffmpeg-libs` is only a
*recommended* dependency of the `punktfunk` RPM (`Recommends: ffmpeg-libs`) — so the package installs
happily without it, and NVENC then fails at runtime with no video. Enable RPM Fusion and swap the
FFmpeg (the [Fedora guide](/docs/fedora#1-gpu-driver), step 1), then check the encoders are there:
```sh
sudo dnf install --allowerasing ffmpeg ffmpeg-libs
ffmpeg -hide_banner -encoders | grep nvenc # expect hevc_nvenc / av1_nvenc / h264_nvenc
```
The same applies on a layered Bazzite / Fedora Atomic install; the sysext image carries its own.
## pacman: error: could not register 'punktfunk' database (database already registered)
The repo block got appended to `/etc/pacman.conf` twice — the add line is an append, so running it
a second time leaves two `[punktfunk]` sections, and every later pacman run opens with this line.
It's harmless (pacman ignores the duplicate), but to silence it delete the extra block from
`/etc/pacman.conf`. (The [current add line](/docs/arch#2-install-the-host) checks first and won't
append a second copy.)
## pacman: unable to satisfy dependency 'libavcodec.so=…'
```
:: unable to satisfy dependency 'libavcodec.so=62-64' required by punktfunk-host
```
`punktfunk-host` links FFmpeg and depends on the exact libav sonames it was built against — FFmpeg 8
provides `libavcodec.so=62`, FFmpeg 9 `libavcodec.so=63`. The package on offer was built against a
*different* FFmpeg major than your box has, and because pacman prepares the whole transaction at
once, it stops your entire `pacman -Syu`. The bound is deliberate: without it the upgrade succeeds
and leaves a host that cannot start at all (exit 127 before `main()`, in a restart loop, with
nothing in its log — `ldd /usr/bin/punktfunk-host | grep 'not found'` is the one-line diagnosis).
1. `sudo pacman -Syyu` — a forced refresh, in case the matching build is already published.
Compare `pacman -Si punktfunk-host` against `pacman -Q ffmpeg`.
2. Still refused? We published a build against the wrong FFmpeg — please report it. The repair is a
higher **pkgrel** of the same version (`0.25.0-2`), so a later `-Syu` picks it up with nothing to undo.
3. Meanwhile, to let the rest of the system upgrade: `sudo pacman -Syu --ignore punktfunk-host`. If
pacman still refuses (your *installed* copy carries the bound), `sudo pacman -Rdd punktfunk-host`,
upgrade, and install it again once the rebuild lands. The host stays down until then — that is
the soname break itself, not a second fault.
## The desktop won't start, or "GPU … not supported by EGL"
@@ -411,6 +430,32 @@ It is not only the pad, though: the same group authorizes the helper that stops
for a managed **Gaming Mode** takeover, so on a box that autologins into Game Mode, skipping it also
costs you [the takeover](#game-mode-black-screen-on-connect-or-the-stream-is-stuck-at-the-boxs-resolution).
## Stream lags, then freezes, with a DualSense pad (Bazzite, SELinux)
On Bazzite (and other SELinux-enforcing Fedora Atomic spins), a **DualSense / DualShock 4**-type
virtual pad can make the stream lag and then freeze — gamescope at 0 fps, `tx_mbps` collapsing —
measured live on Bazzite 43. The virtual pad binds the kernel's `hid-playstation` driver, and Valve's
`ds_inhibit` (inside `steamos-manager`) reacts to *any* such hidraw by walking `/proc/*/fd/` on every
open/close. SELinux denies `steamos_manager_t` that walk — **~324 `avc: denied` per second** — and
`setroubleshootd` amplifies the flood into a box-wide fork storm that starves the stream.
Two traps while diagnosing: the AVC lines read `comm="tokio-rt-worker"` — that is **steamos-manager,
not punktfunk** (check `scontext=…steamos_manager_t…`); and once started, the `setroubleshootd`
storm **outlives the denials by 15+ minutes**, so the box stays starved after the pad is gone.
- **Fix:** punktfunk ships a `dontaudit` SELinux drop-in that silences the flood (ds_inhibit then
simply leaves the pad uninhibited — harmless). The sysext installs it on install/update; on an
existing install run `sudo punktfunk-sysext reapply`. On a layered or bootc host:
`sudo semodule -i /usr/share/punktfunk/selinux/punktfunk-ds-inhibit.cil` (remove with
`sudo semodule -r punktfunk-ds-inhibit`).
- **Hardening, recommended on any streaming host:** `sudo systemctl mask --now setroubleshootd`.
It is purely a desktop alert daemon — nothing depends on it (`systemctl list-dependencies
--reverse setroubleshootd` returns only itself) — and masking it makes the box robust against
*any* AVC burst. Reversible with `unmask`.
- **Workaround with a feature loss:** set the *client's* controller type to Xbox 360 (uinput, no
`hid-playstation`) — costs adaptive triggers, lightbar and touchpad. The host-side
`PUNKTFUNK_GAMEPAD` knob does **not** help: an explicit client choice outranks it.
## A Steam Controller 2 is captured, but Steam's controller list stays empty
The client says everything is fine — the Controllers screen shows **Steam Controller 2, captured,
-265
View File
@@ -1,265 +0,0 @@
---
title: Ubuntu
description: Install the Punktfunk host on Ubuntu with apt.
---
Install a Punktfunk host on **Ubuntu** (Desktop or Server) from the apt registry. This
page covers the distro-level setup — GPU driver, package, gamepad access. It works with either GNOME
or KDE; how the host creates its virtual display and injects input is desktop-specific, so pick your
desktop on the [configure pages](#configure-your-desktop) afterward rather than here.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
> **Which releases.** There is one universal host package. It bundles FFmpeg 8 and needs **glibc 2.39
> or newer** — that's **Ubuntu 24.04 LTS through 26.04**, the range it's built and tested for. Check
> yours with `ldd --version`; below 2.39 the install fails and you build from source instead
> ([appendix](#appendix--build-from-source)). The desktop *client* package is built on Ubuntu 26.04
> and needs GTK4 ≥ 4.20 and SDL3, so it installs on **26.04 or newer** only — the host has no such
> limit.
> ⚠ **On 24.04 LTS, the package installs but the distro gives it no compositor to drive.** The
> host `.deb` is built for 24.04 and installs cleanly — but streaming needs a compositor that meets
> the [version floors](/docs/requirements), and stock 24.04 meets none of them: KWin **5.27** (floor
> 6.5.6), GNOME Shell **46** (floor 48), and no `gamescope` package at all (nor can the patched one
> run there — 24.04 is too old on wayland, libinput, libavif and pixman). `sway` 1.9 is the only
> candidate. **For a working host, use 26.04**, where the patched gamescope and current KDE/GNOME
> are all available. This is why [Linux Mint 22.x cannot host](/docs/debian#linux-mint-22x-cannot-host-yet).
> **On Debian**, see [Debian](/docs/debian) — the host, console and plugin runner are supported and
> CI-tested on **Debian 13**; the desktop client is not packaged for it yet. Debian 12 (bookworm) is
> below the glibc floor and cannot install anything here. Note that the `debian` in the repository
> URL below is the *package format*, and is the same URL for both distros.
## 1. GPU driver
On **NVIDIA**, install the recommended driver.
```sh
sudo ubuntu-drivers install # or: sudo apt install nvidia-driver-<version>
```
Then make sure the **GL/EGL userspace** is present — Wayland compositors on NVIDIA need it, and the
base driver package doesn't always pull it in. Install the `libnvidia-gl` package matching your
driver version:
```sh
sudo apt install libnvidia-gl-<version> # e.g. libnvidia-gl-550
```
Reboot, then confirm the driver and KMS modeset:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y
```
If modeset is not `Y`:
```sh
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
```
> **Secure Boot:** on a machine with Secure Boot **enabled**, the NVIDIA kernel module won't load
> until you enrol its signing key. If `nvidia-smi` reports it can't talk to the driver, run
> `sudo mokutil --import /var/lib/shim-signed/mok/MOK.der` (set a one-time password), reboot, and
> choose **Enrol MOK** at the blue screen. Or disable Secure Boot in firmware.
On **AMD/Intel** none of the NVIDIA steps apply. Encode runs on the Mesa stack: **Vulkan Video** for
HEVC and AV1 (`mesa-vulkan-drivers`), with **VAAPI** for H.264 and as the fallback —
`mesa-va-drivers` on AMD, `intel-media-va-driver` on Intel, both of which the `punktfunk-host`
package recommends, so apt pulls the right one in with the host. Install `mesa-vulkan-drivers` too
if it isn't already on the box.
## 2. Install the host (apt)
`punktfunk-host` is published as a `.deb` to the public Gitea apt registry, so the box installs and
updates with plain `apt`. The registry is public — no auth needed, just trust its signing key:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
`punktfunk-host` `Recommends` the browser console (`punktfunk-web`), so apt pulls it in by default.
The desktop *client* (`punktfunk-client`) is a separate package for the machine you stream *to* — not
installed on a host. The NVIDIA driver is **not** a dependency — you installed it out of band in
step 1. Later updates are just `sudo apt update && sudo apt upgrade`, or, to move only Punktfunk,
`sudo apt update && sudo apt install --only-upgrade punktfunk-host`. Either way, restart the running
host afterwards so it picks up the new binary:
```sh
systemctl --user restart punktfunk-host
```
A plain `apt upgrade` also moves `punktfunk-web` when a new console is out — restart that one too
(`systemctl --user restart punktfunk-web`) if you run it.
[Updating the Host](/docs/updating) covers the rest — the web console's **Updates** card, and the
opt-in one-click update button whose `punktfunk-update` group this package creates.
The `stable` component above is the stable channel. To track pre-release builds instead, see
[Release Channels](/docs/channels).
## 3. Grant gamepad access
Virtual gamepads inject through `/dev/uinput`, which is gated by the `input` group. Add yourself and
re-login so the new group membership takes effect:
```sh
sudo usermod -aG input "$USER" # re-login to apply
```
Also join `punktfunk` if **either** applies — you want the **virtual Steam Deck controller**
(paddles, trackpads, gyro), or this box autologins into Steam **Gaming Mode** and you want the host
to take that session over at the client's resolution. That pad reaches games as a real USB device
over usbip — which is what makes Steam Input adopt it — and the same group authorizes the helper
that stops the display manager for a takeover. It is deliberately separate from `input`, because
writing the usbip `attach` file can materialise arbitrary emulated USB hardware:
```sh
sudo usermod -aG punktfunk "$USER" # re-login to apply
```
Join it only on a machine you trust. On a plain desktop host, skipping it costs you nothing but
that one pad type; on a Gaming Mode box the takeover silently degrades to mirroring the box's own
screen — see [gamescope](/docs/gamescope#nobara-and-other-autologin-display-managers).
## 4. Check it installed
Before moving on, confirm the binary is there and nothing else is competing for the same job:
```sh
punktfunk-host --version # the binary is on PATH
punktfunk-host detect-conflicts # exits 1 if Sunshine/Apollo is also installed
```
If `detect-conflicts` reports another streaming host, remove it before going further — two hosts on
one machine is the most common reason a clean install never streams. See
[Troubleshooting → another streaming host is installed](/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed).
Once you've enabled the service on your desktop page below, these are how you watch it:
```sh
systemctl --user status punktfunk-host # active
journalctl --user -u punktfunk-host -f # watch a client connect
```
## 5. Open the firewall (if you have one)
Ubuntu installs `ufw` but leaves it **inactive**, so out of the box
there is nothing to open. If you did enable one — common on Ubuntu Server — the package ships the
openers for both, because a package never edits your firewall itself.
The packaged unit runs the **secure native-only host** (`serve`, no GameStream), so a host you
enabled with `systemctl --user enable --now punktfunk-host` needs only the native opener:
```sh
# ufw:
sudo ufw allow punktfunk-native
# firewalld:
sudo firewall-cmd --reload # load the installed definitions
sudo firewall-cmd --permanent --add-service=punktfunk-native
sudo firewall-cmd --reload
```
Enabled **GameStream/Moonlight compat** (`PUNKTFUNK_GAMESTREAM=1` in `host.env` — see
[What the unit starts](/docs/running-as-a-service#what-the-unit-starts)), or you pass
`--gamestream` by hand? Then also open its service:
```sh
sudo ufw allow punktfunk-gamestream # ufw
sudo firewall-cmd --permanent --add-service=punktfunk-gamestream && sudo firewall-cmd --reload
```
`punktfunk-native` opens UDP 9777 (QUIC control), UDP 5353 (mDNS discovery) and TCP 47990 (the
mgmt/library API — HTTPS + mTLS, read-only off loopback). `punktfunk-gamestream` opens the fixed
Moonlight ports — TCP 47984, 47989 and 48010, UDP 4799848000 — plus the same mDNS. The media
**data plane** uses an ephemeral UDP port the client opens with a hole-punch, so there is nothing
fixed to open for video.
Running the web console (`punktfunk-web`) and want to reach it from another device? Open it too —
that's **TCP 47992**:
```sh
sudo ufw allow punktfunk-web # ufw
sudo firewall-cmd --permanent --add-service=punktfunk-web && sudo firewall-cmd --reload # firewalld
```
Full port lists are in
[`packaging/debian/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md#firewall).
## Configure your desktop
How the host creates its virtual display and injects input depends on your desktop, not your distro.
Continue on the page for the desktop you run — it covers your `host.env`, any compositor quirks, and
starting the host:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Hyprland](/docs/hyprland)
- [Sway / wlroots](/docs/sway)
Then bring up [The Web Console](/docs/web-console) to arm pairing and connect your first
[client](/docs/clients). To run the host at boot — including fully **headless** — see
[Running as a Service](/docs/running-as-a-service).
## Next steps
- **Keep it current** — [Updating the Host](/docs/updating).
- **Remove it again** — [Uninstalling](/docs/uninstall).
- **Something not working?** — [Troubleshooting](/docs/troubleshooting).
## Appendix — build from source
If your release is older than the supported range above, or you want to track `main` directly,
compile the host yourself (no clean updates / no packaged units — you wire those up by hand).
Install the build toolchain and runtime libraries. The packaged host is built against **FFmpeg 8**:
Ubuntu 26.04's `libavcodec-dev` is FFmpeg 8, but 24.04's is FFmpeg 6.1 — on 24.04 build FFmpeg 8
yourself first (that's what `ci/rust-ci-noble.Dockerfile` does, and the shipped `.deb` bundles the
result) or stick with the packaged host.
```sh
sudo apt install build-essential pkg-config cmake clang libclang-dev nasm git curl \
pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev \
libwayland-dev wayland-protocols libxkbcommon-dev libopus-dev \
libdrm-dev libgbm-dev libgl-dev libegl-dev libgles-dev mesa-common-dev libva-dev \
ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \
libnvidia-egl-wayland1 libnvidia-egl-gbm1 libei-dev
```
Install Rust if you don't have it, then build:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk
cargo build --release --locked \
--features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host
```
Those two features are what the packaged build uses — without them the host has no direct NVENC
(NVIDIA) and no Vulkan Video encode (AMD/Intel), and falls back to the slower libav backends.
The host binary lands at `target/release/punktfunk-host`. Configure your desktop as above, then run
it from inside your session:
```sh
cargo run --release --locked \
--features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host -- serve --gamestream
```
(The native plane is always on; `--gamestream` adds the Moonlight-compat surface — trusted LAN only.
Drop it for a secure native-only host.)
+74
View File
@@ -0,0 +1,74 @@
---
title: Ubuntu
description: Install the Punktfunk host on Ubuntu 26.04 or newer with apt — four steps.
---
For **Ubuntu 26.04 or newer**, Desktop or Server, GNOME or KDE.
> **Ubuntu 24.04 LTS installs the package but cannot stream** — its desktop is too old to create a
> virtual display, and there is no gamescope for it. Use 26.04.
> [Why, in detail](/docs/requirements#the-floor-for-a-working-host).
## 1. GPU driver
- **NVIDIA:** install the recommended driver and its GL/EGL userspace (Wayland needs it), then
reboot:
```sh
sudo ubuntu-drivers install
sudo apt install libnvidia-gl-<version> # the number ubuntu-drivers picked, e.g. libnvidia-gl-580
```
If `nvidia-smi` can't talk to the driver afterwards, Secure Boot is in the way — see
[Troubleshooting](/docs/troubleshooting#nvidia-smi-says-it-cant-communicate-with-the-driver).
- **AMD / Intel:** nothing to install. The host package pulls in the VAAPI driver for your GPU, and
Vulkan Video comes with Mesa.
## 2. Install the host
The repo is public and signed; this adds it and installs the host (the browser console,
`punktfunk-web`, comes along automatically):
<Install platform="debian" />
Updates ride along with `sudo apt upgrade` from now on; restart the host afterwards
(`systemctl --user restart punktfunk-host`) — or let the [console do it](/docs/updating).
## 3. Let it use your controllers
Virtual gamepads go through `/dev/uinput`, which the `input` group owns. Join it, then **log out and
back in**:
```sh
sudo usermod -aG input "$USER"
```
Want the **virtual Steam Deck controller** (paddles, trackpads, gyro), or does this PC boot into Steam
**Gaming Mode**? Also join the `punktfunk` group —
[what it gates and why it's separate](/docs/gamescope#nobara-and-other-autologin-display-managers).
## 4. Start it
From a terminal inside your desktop session:
```sh
systemctl --user enable --now punktfunk-host punktfunk-web
```
Ubuntu ships `ufw` switched **off**, so there is nothing to open. If you turned it on (common on
Server): `sudo ufw allow punktfunk-native`, plus `sudo ufw allow punktfunk-web` to reach the console
from another device — [Ports & firewall](/docs/ports) has every port and the GameStream ones.
**That's the install.** Continue with the [Quick Start from step 3](/docs/quickstart#3-open-the-web-console)
— open the console, pair a client, stream.
## When you want more
- Something off? `punktfunk-host detect-conflicts` tells you if Sunshine or Apollo is also running;
[Troubleshooting](/docs/troubleshooting) starts from the symptom.
- Your desktop's particulars — headless sessions, quirks — [KDE](/docs/kde), [GNOME](/docs/gnome),
[gamescope](/docs/gamescope), [Hyprland](/docs/hyprland), [Sway](/docs/sway).
- Stream with nobody logged in — [Running as a service](/docs/running-as-a-service).
- Track `main` instead of releases — [Release channels](/docs/channels). Older Ubuntu, or want to
hack on it — [Build from source](/docs/build-from-source).
- Debian, LMDE and Linux Mint — [Debian](/docs/debian).
+44 -49
View File
@@ -3,17 +3,16 @@ title: Uninstalling
description: Remove the Punktfunk host or client for every install method — and what each one deliberately leaves behind.
---
Every install method has a clean removal path. This page walks through each one, and — just as
important — says what stays on the machine afterwards: the Linux packages run no removal scripts of
their own, and the Windows uninstaller leaves a few things in place on purpose.
Every install method has a clean removal path. This page gives each one and what stays on the
machine afterwards: the Linux packages run no removal scripts of their own, and the Windows
uninstaller leaves a few things in place on purpose.
> **Your configuration always survives.** Removing Punktfunk never deletes its config directory —
> `~/.config/punktfunk` on Linux, `%ProgramData%\punktfunk` for the Windows host. It holds the
> host's identity certificate and key, its management token, your paired devices, the web-console
> login password, `host.env`, the game library, the logs, and any installed
> [plugins](/docs/plugins) and their state. Keeping it is what lets a reinstall pick up where you
> left off — each section below gives the one command that clears it, for when you want a clean
> slate instead.
> [plugins](/docs/plugins) and their state — which is what lets a reinstall pick up where you left
> off. Each section below gives the one command that clears it for a clean slate.
Jump to what you installed:
@@ -34,7 +33,7 @@ you'll be left with dangling links and a unit that fails at every login:
systemctl --user disable --now punktfunk-host punktfunk-web
```
Add `punktfunk-scripting` to that line if you enabled the [plugin runner](/docs/plugins), and
Add `punktfunk-scripting` if you enabled the [plugin runner](/docs/plugins), and
`punktfunk-kde-session` if you set up the [headless KDE session](/docs/kde#headless-session).
If you turned on linger so the host ran without a login, and nothing else on the box needs it:
@@ -43,6 +42,11 @@ If you turned on linger so the host ran without a login, and nothing else on the
sudo loginctl disable-linger "$USER"
```
Every Linux package also leaves two system groups it created: `punktfunk-update` (empty; for
[one-click updates](/docs/updating)) and `punktfunk` (the virtual Steam Deck pad's usbip nodes).
Drop `punktfunk` rather than keeping it: it can present arbitrary emulated USB hardware, and with
the host gone nothing uses it. The per-method sections below give the commands.
### Ubuntu (apt)
```sh
@@ -50,17 +54,15 @@ sudo apt purge punktfunk-host punktfunk-web punktfunk-client punktfunk-scripting
sudo apt autoremove
```
Name only the packages you actually installed — the others are simply reported as not installed.
Then drop the repository and its key, so `apt update` stops contacting it:
Name only the packages you installed — the others are reported as not installed. Then drop the
repository and its key, so `apt update` stops contacting it:
```sh
sudo rm -f /etc/apt/sources.list.d/punktfunk.list /etc/apt/keyrings/punktfunk.asc
sudo apt update
```
**Left behind:** `~/.config/punktfunk`, and the two system groups the package created — the empty
`punktfunk-update` for [one-click updates](/docs/updating), and `punktfunk` for the virtual Steam
Deck pad's usbip nodes. Clear them with:
**Left behind:** `~/.config/punktfunk` and the two groups. Clear them with:
```sh
rm -rf ~/.config/punktfunk
@@ -68,12 +70,10 @@ sudo groupdel punktfunk-update
sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk
```
Your `input` group membership is harmless to keep (it is a stock Ubuntu group). Drop it with
`sudo gpasswd -d "$USER" input` if you'd rather not have it. The `punktfunk` group above is worth
dropping rather than keeping: it can present arbitrary emulated USB hardware, and with the host
gone nothing uses it. If you opened the firewall, close it
again: `sudo ufw delete allow punktfunk-native` (and `punktfunk-gamestream` / `punktfunk-web` if you
allowed those too).
Your `input` group membership is harmless to keep (a stock Ubuntu group); drop it with
`sudo gpasswd -d "$USER" input` if you'd rather not have it. If you opened the firewall, close it
again: `sudo ufw delete allow punktfunk-native` (and `punktfunk-gamestream` / `punktfunk-web` if
you allowed those too).
### Fedora (dnf)
@@ -84,13 +84,11 @@ sudo dnf remove punktfunk punktfunk-web punktfunk-client punktfunk-scripting
sudo rm -f /etc/yum.repos.d/punktfunk.repo
```
**Left behind:** `~/.config/punktfunk`, the `punktfunk-update` and `punktfunk` groups, and the
signing key dnf
imported into the rpm keyring when it first installed a Punktfunk package. Clear the first two with
`rm -rf ~/.config/punktfunk` and `sudo groupdel punktfunk-update`; drop `punktfunk` too
(`sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk`) — it can present arbitrary
emulated USB hardware and nothing uses it once the host is gone. The key is harmless to leave — on
its own it only marks packages from our registry as trusted, and nothing fetches them once the repo
**Left behind:** `~/.config/punktfunk`, the two groups, and the signing key dnf imported into the
rpm keyring when it first installed a Punktfunk package. Clear the first two with
`rm -rf ~/.config/punktfunk`, `sudo groupdel punktfunk-update` and
`sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk`. The key is harmless to leave — on its
own it only marks packages from our registry as trusted, and nothing fetches them once the repo
file is gone.
On firewalld, close the ports you opened:
@@ -116,7 +114,7 @@ The change only takes effect in the new deployment, so the reboot is part of the
### Bazzite / Fedora Atomic (systemd-sysext)
This is the supported Bazzite path and the tidiest one — the whole install is a single image under
The supported Bazzite path and the tidiest one — the whole install is a single image under
`/var/lib/extensions/`. Stop the services **before** you unmerge, because once the image is gone
their binaries are gone and the units just keep failing:
@@ -138,7 +136,7 @@ sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk
```
And your config, if you want it gone: `rm -rf ~/.config/punktfunk`. See
[Bazzite](/docs/bazzite#install) for the same sequence in context.
[Bazzite](/docs/bazzite#1-install-the-host) for the same sequence in context.
### Arch / CachyOS (pacman)
@@ -151,18 +149,16 @@ Name only what you installed. `-Rns` also takes the dependencies nothing else ne
packages' own configuration files.
Then delete the `[punktfunk]` section (or `[punktfunk-canary]`) from `/etc/pacman.conf` — the two
lines you appended when you [added the repo](/docs/arch#2-add-the-signed-repo). Optionally drop the
lines you appended when you [added the repo](/docs/arch#2-install-the-host). Optionally drop the
repo's signing key from pacman's keyring:
```sh
sudo pacman-key --delete E0CA04465C99C936E0B0C6510A317015A34DDD69
```
**Left behind:** `~/.config/punktfunk` and the `punktfunk-update` and `punktfunk` groups —
`rm -rf ~/.config/punktfunk`, `sudo groupdel punktfunk-update`, and
`sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk` clear them. Drop that last one
rather than keeping it: it can present arbitrary emulated USB hardware. On CachyOS, close the
ufw rules you opened: `sudo ufw delete allow punktfunk-native`.
**Left behind:** `~/.config/punktfunk` and the two groups — `rm -rf ~/.config/punktfunk`,
`sudo groupdel punktfunk-update`, and `sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk`
clear them. On CachyOS, close the ufw rules you opened: `sudo ufw delete allow punktfunk-native`.
### SteamOS / Steam Deck host (on-device build)
@@ -178,14 +174,14 @@ systemctl --user daemon-reload
Then follow [SteamOS (Host) → Uninstalling](/docs/steamos-host#uninstalling) for the build
container, the files under your home, and the root-owned tuning. Don't skip the last of those: the
atomic-update keep list is what carries those files through every SteamOS update, so left alone they
stay on the device indefinitely.
atomic-update keep list carries those files through every SteamOS update, so left alone they stay
on the device indefinitely.
**Left behind:** `~/.config/punktfunk` (`rm -rf ~/.config/punktfunk` for a clean slate), your
`input` and `punktfunk` group memberships, and — if the installer seeded it because you had none —
the KDE RemoteDesktop portal grant at `~/.local/share/flatpak/db/kde-authorized`. Drop the second
group once the host is gone — it can present arbitrary emulated USB hardware and nothing else on a
Deck uses it: `sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk`.
the KDE RemoteDesktop portal grant at `~/.local/share/flatpak/db/kde-authorized`. Drop the
`punktfunk` group once the host is gone — nothing else on a Deck uses it:
`sudo gpasswd -d "$USER" punktfunk; sudo groupdel punktfunk`.
### NixOS
@@ -209,9 +205,9 @@ if you installed with winget:
winget uninstall unom.PunktfunkHost
```
Both run the same uninstaller, which takes the `PunktfunkHost` service, the scheduled tasks, the
virtual-display and gamepad drivers and every firewall rule it added back off the machine — the
full inventory is on [Windows Host → Uninstalling](/docs/windows-host#uninstalling).
Both run the same uninstaller, which removes the `PunktfunkHost` service, the scheduled tasks, the
virtual-display and gamepad drivers and every firewall rule it added — the full inventory is on
[Windows Host → Uninstalling](/docs/windows-host#uninstalling).
Three things are left on purpose:
@@ -234,23 +230,23 @@ Three things are left on purpose:
**Trusted Root Certification Authorities**. (This is *not* the driver certificate above, which the
uninstaller does remove.)
If you registered the winget source, drop it too — in an **admin** PowerShell, the same as
registering it:
If you registered the winget source, drop it too — in an **admin** PowerShell, as when registering
it:
```powershell
winget source remove -n punktfunk
```
**If a Punktfunk display or gamepad survives in Device Manager** — an older build could leave one
behind — run the host's own cleanup from an elevated prompt, which is exactly what the uninstaller
calls. Do this **while the host is still installed**:
behind — run the host's own cleanup from an elevated prompt (exactly what the uninstaller calls),
**while the host is still installed**:
```powershell
punktfunk-host driver uninstall
punktfunk-host driver uninstall --gamepad
```
If you have already uninstalled, `punktfunk-host.exe` went with it. Install the current version
If you have already uninstalled, `punktfunk-host.exe` went with it: install the current version
again and uninstall it — its uninstaller runs both commands for you.
See [Windows Host → Install](/docs/windows-host#install) for the installer's side of the same story.
@@ -272,7 +268,7 @@ directory (that is what lets the Flatpak, a native package and the Decky plugin
identity). Remove it with `rm -rf ~/.config/punktfunk`.
The remote it was installed from also stays. Its name depends on how you installed: if you added the
repo by hand it is **`unom`**, and if you installed straight from the `.flatpakref` — the route
repo by hand it is **`unom`**; if you installed straight from the `.flatpakref` — the route
[Install a Client](/docs/install-client#linux-desktop-flatpak) gives — Flatpak named its own origin
remote for it. List them and delete the one that served Punktfunk, if no other app uses it:
@@ -364,8 +360,7 @@ Windows it is part of the host installer and goes with it.
## Removing the pairing, not the software
If you only want to undo a pairing, you don't need to uninstall anything. The two halves are
separate:
To undo only a pairing, you don't need to uninstall anything. The two halves are separate:
- **On the host** — unpair the device from the [web console](/docs/web-console); it stops being
trusted immediately.
+65 -44
View File
@@ -8,7 +8,7 @@ showing the version you run, the channel you follow (stable or canary), how this
installed, and — once a newer release exists — the exact command that updates it. The
"update available" state also fires an `update.available` event on the host
[event stream](/docs/automation), and a successful update fires `update.applied` (with `from`
and `to`) once the host is back up so hooks and scripts can react to both.
and `to`) once the host is back up, so hooks and scripts can react to both.
Your channel comes from the repository this host installs from — see
[Release Channels](/docs/channels) for what each track means and how to move a host between
@@ -16,7 +16,7 @@ them. The Updates card never switches channels for you.
The check is a small signed manifest the host fetches from the Punktfunk release feed and
verifies against keys built into the host itself — a tampered or replayed feed is rejected, and
the console will tell you when a check failed rather than silently showing stale facts.
the console says when a check failed rather than silently showing stale facts.
## Updating, per install method
@@ -42,9 +42,8 @@ canary installer again — `…/generic/punktfunk-host-windows/canary/punktfunk-
`rpm-ostree upgrade` upgrades the **base image** and only re-resolves layered packages when the
base actually changes — so on a base that sits still (a pinned tag, a paused rebase) it keeps
reporting "No updates available" while newer Punktfunk RPMs are sitting in the repo. Force
rpm-ostree to re-resolve just the Punktfunk layer, removing and re-adding the same names in one
transaction:
reporting "No updates available" while newer Punktfunk RPMs sit in the repo. Force it to
re-resolve just the Punktfunk layer, removing and re-adding the same names in one transaction:
```bash
sudo rpm-ostree refresh-md --force
@@ -57,11 +56,36 @@ systemctl reboot
Name only the packages you actually layered — `rpm-ostree status` lists them. The new version is
staged; it activates on the next boot.
Two things to know. The re-resolve picks the highest version across **every enabled**
`/etc/yum.repos.d/punktfunk*.repo`, so if the canary repo is enabled alongside the stable one,
canary wins and the box quietly tracks canary — enable exactly the channel you want (see
[Release Channels](/docs/channels)). And if this box runs the Bazzite **sysext**, the sysext
shadows any layered copy: update with `sudo punktfunk-sysext update` instead.
The re-resolve picks the highest version across **every enabled** `/etc/yum.repos.d/punktfunk*.repo`,
so if the canary repo is enabled alongside the stable one, canary wins and the box quietly tracks
canary — enable exactly the channel you want (see [Release Channels](/docs/channels)). And if this
box runs the Bazzite **sysext**, the sysext shadows any layered copy: update with
`sudo punktfunk-sysext update` instead.
### Bazzite sysext: channels, rollback and rebases
`sudo punktfunk-sysext update` fetches and merges the newest build of whatever channel the last
install wrote to `/etc/punktfunk-sysext.conf` (`status` shows channel, installed and latest
version). To **switch channel**, re-run the install: `sudo punktfunk-sysext install --channel canary`
(or `--channel stable`) — `update` takes no channel flag. To be able to **go back** to a build that
worked, keep a copy of the image before updating, and re-install that file afterwards:
```sh
sudo cp /var/lib/extensions/punktfunk.raw ~/punktfunk-known-good.raw # before updating
sudo punktfunk-sysext install --from-file ~/punktfunk-known-good.raw # to go back to it
```
- **After a Bazzite major rebase** (Fedora 43 → 44) the old image **refuses to load** rather than run
against mismatched system libraries — run `sudo punktfunk-sysext update` once and it fetches the
image built for the new base.
- **If it refuses the feed.** `refusing to install from an unsigned feed` means that Fedora major's
feed predates signing; it gets sealed on the next publish. To install from it anyway, accepting
unauthenticated images, `sudo env PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1 bash punktfunk-sysext.sh install`.
The other message, `the feed's SHA256SUMS is NOT signed by packages@unom.io`, is not the same
thing — don't install; re-download the script and try again.
- The feed's checksum manifest is OpenPGP-signed by packages@unom.io (key `AF245C506F4E4763`, the
same one that signs the RPMs) and `punktfunk-sysext` verifies it against a key baked into the
script, so it needs `gpg` on the box.
### Restart after a Linux package update
@@ -71,9 +95,8 @@ Restart the host to pick up the new binary:
systemctl --user restart punktfunk-host
```
If the update also brought a new `punktfunk-web` (the console itself — it ships as a separate
package and a separate user service), restart that too, and do it first; the page blinks and
reconnects:
If the update also brought a new `punktfunk-web` (the console — a separate package and a separate
user service), restart that too, and first; the page blinks and reconnects:
```bash
systemctl --user restart punktfunk-web
@@ -92,49 +115,48 @@ restart hint when it's needed.)
## One-click updating (Windows)
On a Windows host the card shows an **Update now** button instead of a command. It asks for the
console password again (a saved login alone can't restart your host), then the host downloads
the installer, verifies it against the signed release manifest **and** its code signature, and
runs it silently — the service restarts at the end and the page reconnects by itself. If a
stream is live you'll be warned first: updating drops it.
console password again (a saved login alone can't restart your host), then the host downloads the
installer, verifies it against the signed release manifest **and** its code signature, and runs it
silently — the service restarts at the end and the page reconnects by itself. If a stream is live
you're warned first: updating drops it.
Every attempt leaves a result in the card (and an installer log under
`C:\ProgramData\punktfunk\logs\update-<version>.log`) — including across the restart, so a
failed update is never silent.
`C:\ProgramData\punktfunk\logs\update-<version>.log`) — including across the restart, so a failed
update is never silent.
If the newly installed host crash-loops, the service puts the previous installer back on its own
(the last two are kept) and says so in the card — you end up on the version you started from,
not on a dead host. That rollback writes
(the last two are kept) and says so in the card — you end up on the version you started from, not
on a dead host. That rollback writes
`C:\ProgramData\punktfunk\logs\update-rollback-from-<version>.log`.
## One-click updating (Linux — opt-in)
The apt, dnf, Bazzite-sysext, and rpm-ostree installs can one-click update too, via a small
root helper the packages ship (`pf-update` + a `punktfunk-update.service` oneshot). It's **off
until you opt in**, because a web button that ends in root deserves an explicit decision:
The apt, dnf, Bazzite-sysext, and rpm-ostree installs can one-click update too, via a small root
helper the packages ship (`pf-update` + a `punktfunk-update.service` oneshot). It's **off until you
opt in** — a web button that ends in root deserves an explicit decision:
```bash
sudo usermod -aG punktfunk-update $USER # takes effect within a minute — no re-login needed
```
The console re-checks group membership every minute, so the **Update now** button replaces the
opt-in hint on its own — there's no need to log out and back in.
opt-in hint on its own.
That group membership is the entire grant — a polkit rule lets its members start exactly that
one service, whose only job is "run this system's normal package update for the Punktfunk
packages, then prove the new binary runs". The button never chooses versions or URLs; your
package manager's own signed repositories stay the source of truth. The card shows the opt-in
command until you've done this, and the manual command always keeps working.
That group membership is the entire grant — a polkit rule lets its members start exactly that one
service, whose only job is "run this system's normal package update for the Punktfunk packages,
then prove the new binary runs". The button never chooses versions or URLs; your package manager's
own signed repositories stay the source of truth. The manual command always keeps working.
Notes per method: on **rpm-ostree** the update is staged and the card will say so — reboot to
finish (the console never reboots your machine). On **Arch/pacman** the button additionally
requires `PACMAN_FULL_SYSUPGRADE=1` in `/etc/punktfunk/update.conf`, because the only safe
pacman update is a full `pacman -Syu`partial upgrades are how Arch boxes break, and we
won't run one. After a successful update the host restarts itself and the page reconnects.
Per method: on **rpm-ostree** the update is staged and the card says so — reboot to finish (the
console never reboots your machine). On **Arch/pacman** the button additionally requires
`PACMAN_FULL_SYSUPGRADE=1` in `/etc/punktfunk/update.conf`, because the only safe pacman update is
a full `pacman -Syu`we won't run a partial upgrade. After a successful update the host restarts
itself and the page reconnects.
The **Steam Deck on-device build** gets the button too, with no opt-in (it's your own user's
install, no root involved): it runs the same `update.sh` rebuild the docs describe, which
compiles on the Deck — expect it to take a while; the card keeps showing progress and the log
lands in `~/.config/punktfunk/logs/update-steamos.log`.
install, no root involved): it runs the same `update.sh` rebuild the docs describe, which compiles
on the Deck — expect it to take a while; the card shows progress and the log lands in
`~/.config/punktfunk/logs/update-steamos.log`.
## Updating a client
@@ -143,14 +165,13 @@ normal `apt upgrade` / `dnf upgrade` for the packaged Linux client, a newer `.dm
old app on macOS, TestFlight on iPhone/iPad/Apple TV, Google Play on Android, and the Decky panel's
own **Update** button on a Steam Deck. The per-platform table is in
[Install a Client → Keeping a client up to date](/docs/install-client#keeping-a-client-up-to-date).
A host and a client don't have to be on the same version, but keeping them close is the least
A host and a client don't have to be on the same version, but keeping them close is least
surprising.
## Turning the check off
The check contacts `git.unom.io` (the Punktfunk forge) and nothing else, and sends nothing but a
normal download request. If you'd rather the host never checks, add this line to the host's
`host.env`:
normal download request. To stop the host checking, add this line to its `host.env`:
```bash
PUNKTFUNK_UPDATE_CHECK=0
@@ -166,10 +187,10 @@ Windows or Linux; the card shows the manual command instead.
## If the card says the feed is stale
"Feed hasn't changed in over 45 days" means checks *succeed* but nothing new arrives. Usually
that just means no release happened for a while; if the [releases page](https://git.unom.io/unom/punktfunk/releases)
"Feed hasn't changed in over 45 days" means checks *succeed* but nothing new arrives. Usually no
release happened for a while; if the [releases page](https://git.unom.io/unom/punktfunk/releases)
shows something newer than the card does, something between this host and the feed is pinning old
data — worth a look at proxies or DNS on the way to `git.unom.io`. That comparison only works on a
data — look at proxies or DNS on the way to `git.unom.io`. That comparison only works on a
**stable** host: the releases page is stable-only, so a canary host being "behind" it means
nothing.
+143 -163
View File
@@ -3,81 +3,68 @@ title: Virtual displays
description: Control how Punktfunk creates, keeps alive, and arranges the virtual displays it streams — presets, keep-alive, exclusive vs. extend, and persistent per-client scaling.
---
When a client connects, Punktfunk creates a **virtual display** sized to exactly that client's
resolution and refresh, renders your desktop or game onto it, and streams it. This page is about the
**policy** for that display: how long it survives a disconnect, whether it takes over your physical
monitors, what happens when a second client connects, and how desktop environments remember
per-client settings like scaling.
When a client connects, Punktfunk creates a **virtual display** at exactly that client's resolution
and refresh, renders your desktop or game onto it, and streams it. This page covers the **policy**
for that display: how long it survives a disconnect, whether it takes over your physical monitors,
what happens when a second client connects, and how desktop environments remember per-client
settings like scaling.
You set this policy in the **web console** (the **Virtual displays** page), or by editing
`~/.config/punktfunk/display-settings.json` directly (`%ProgramData%\punktfunk\display-settings.json`
on Windows). A change applies to the **next** connection — a running session keeps the display it
Set it in the **web console** (the **Virtual displays** page), or edit
`~/.config/punktfunk/display-settings.json` (`%ProgramData%\punktfunk\display-settings.json` on
Windows). A change applies to the **next** connection — a running session keeps the display it
opened on.
> **You rarely need to touch this.** The default behavior matches how Punktfunk has always worked.
> Reach for a preset when you want a specific experience — a dedicated box you only stream from, a
> desktop you also use in person, or a multi-monitor workstation.
> **You rarely need to touch this.** The default matches how Punktfunk has always worked; reach for
> a preset when you want a specific experience.
>
> Here for one specific problem — monitors that stayed dark, or a streamed screen showing nothing
> but wallpaper? Go straight to [Troubleshooting](#troubleshooting) at the bottom.
> Monitors that stayed dark, or a streamed screen showing only wallpaper? Go to
> [Troubleshooting](#troubleshooting).
If you'd rather stream a monitor the host **already has** than have Punktfunk make one, that's a
different setting and it turns most of this page off — see
[Stream a real monitor instead](#stream-a-real-monitor-instead) right below.
To stream a monitor the host **already has** instead, see
[Stream a real monitor instead](#stream-a-real-monitor-instead) — it turns most of this page off.
> **What's live today:** **keep-alive** (linger, or **forever**), **topology** (extend / primary /
> exclusive), **conflict handling**, **per-client identity + persistent scaling** (Windows, KDE/KWin
> *and* GNOME/Mutter), and **multi-monitor layout** (several clients as monitors of one desktop) are
> all enforced. A reconnect always resumes the kept display — even a fast one — instead of spawning a
> second. The remaining gaps are noted inline: the Linux `primary` physical-keep *effect*, and
> multi-display for a *single* client (that last is the next stage).
> *and* GNOME/Mutter) and **multi-monitor layout** (several clients as monitors of one desktop) are
> all enforced. A reconnect — even a fast one — always resumes the kept display instead of spawning
> a second. Gaps, noted inline: the Linux `primary` physical-keep *effect*, and multi-display for a
> *single* client (the next stage).
## Stream a real monitor instead
> **Linux only.** A Windows host enumerates its monitors but has no backend that can capture one, so
> the Streamed screen card is read-only there and every Windows session gets a virtual display.
Sometimes you don't want a new screen — you want *that* screen. A shop-floor PC on a wall mount, a
lab bench machine, a media box whose TV output you'd like to watch from the couch: what matters is
seeing the monitor that's already there, exactly as the person in front of it sees it.
For a wall-mounted shop-floor PC, a lab bench machine or a media box's TV output, you want *that*
screen, not a new one. Set **Virtual displays → Streamed screen** in the console to a listed
monitor and Punktfunk streams that physical monitor instead of creating a virtual display; every
client sees it at *its* resolution.
Set **Virtual displays → Streamed screen** in the console to one of the listed monitors and
Punktfunk stops creating a virtual display altogether. It streams that physical monitor instead,
and every client sees it at *its* resolution — you're a viewer of a screen, not the owner of your
own.
- The monitor is **never touched**. Punktfunk doesn't resize it, move it, disable it or restore it —
it only watches. Keep-alive, topology and the multi-monitor layout options above simply don't
apply, because there's no display of ours to apply them to.
- **The resolution is the monitor's**, not yours. A client asking for a different one is politely
told no and scales its own picture — reconfiguring the display someone is sitting at would be
rude, and the mid-stream resize machinery is switched off for this reason.
- **Every client sees the same screen.** Two clients means two viewers of one monitor, not two
desktops.
- The monitor is **never touched** — not resized, moved, disabled or restored. Keep-alive, topology
and multi-monitor layout don't apply: there's no display of ours to apply them to.
- **The resolution is the monitor's**, not yours. A client asking for a different one is told no and
scales its own picture; the mid-stream resize machinery is switched off.
- **Every client sees the same screen** — two clients are two viewers of one monitor.
- Naming a monitor this host **doesn't have, while it has others**, is a **hard error**, not a
fallback: the session fails with `no monitor named "DP-9" — this host has: HDMI-1`. Showing you
the wrong screen would be worse than showing you none. The one exception is a session with **no
physical heads at all** — a nested or headless compositor: there the pin is set aside with a
warning in the log and you get an ordinary virtual display, and it applies again the next time a
session with real heads runs. Streaming the *wrong* screen is refused; streaming a virtual one
when there's no screen to mirror is not.
- Choosing **Virtual screen (default)** in the same card puts you back on the normal path.
fallback: the session fails with `no monitor named "DP-9" — this host has: HDMI-1`. The one
exception is a session with **no physical heads at all** — a nested or headless compositor: the
pin is set aside with a warning in the log, you get an ordinary virtual display, and the pin
applies again the next time a session with real heads runs.
- **Virtual screen (default)** in the same card puts you back on the normal path.
Supported on **KDE/KWin**, **GNOME/Mutter**, **Sway/wlroots**, **Hyprland** and **gamescope Game
Mode** (a Steam Deck / Bazzite couch box, where gamescope drives the screen) — each through the
compositor's own screen-recording API, so there is **no chooser dialog** and nothing to click. That
matters most for a host running unattended as a [service](/docs/running-as-a-service): a background
`systemd --user` daemon has nobody to answer a permission prompt, so the monitor has to be
configuration rather than a question. On gamescope only the one head the session is driving is
listed and mirrorable — mirroring attaches to the session's own composited stream, so that screen
keeps showing exactly what the person in front of it sees and nothing is relaunched. A *nested* or
headless gamescope (the per-client sessions the host spawns itself) has no head of its own, so the
picker is empty there.
compositor's own screen-recording API, so there is **no chooser dialog**. That matters for a host
running unattended as a [service](/docs/running-as-a-service): a background `systemd --user` daemon
has nobody to answer a permission prompt. On gamescope only the head the session is driving is
listed — mirroring attaches to the session's own composited stream, so that screen keeps showing
what the person in front of it sees and nothing is relaunched. A *nested* or headless gamescope (the
per-client sessions the host spawns itself) has no head, so the picker is empty there.
### Naming the monitor from the host
Monitors are named by **connector**`HDMI-A-1`, `DP-2`, `eDP-1`. List what this host has (these
subcommands are Linux-only, like the setting itself):
Monitors are named by **connector**`HDMI-A-1`, `DP-2`, `eDP-1`. List this host's (Linux-only
subcommands, like the setting):
```sh
punktfunk-host list-monitors
@@ -89,34 +76,32 @@ Kwin:
DP-2 2560x1440@144 at +1920,+0 scale 1 ACME 27
```
To pin it **from the host's configuration** instead of the console — the appliance route — set it in
To pin it **from the host's configuration** instead — the appliance route — set it in
[`host.env`](/docs/configuration):
```sh
PUNKTFUNK_CAPTURE_MONITOR=HDMI-A-1
```
The environment variable **wins over the console setting**, deliberately: an operator who declared
the answer in the unit's environment shouldn't have it re-aimed by a click. The console shows the
Streamed screen card as locked while it's set. Leave it unset on a machine you want to steer from the
console.
The environment variable **wins over the console setting**, so an operator's declaration can't be
re-aimed by a click; the console shows the Streamed screen card as locked while it's set.
Check the whole path — mirror, capture, frames — without a client involved:
Check the whole path — mirror, capture, frames — without a client:
```sh
punktfunk-host mirror-test --monitor HDMI-A-1 --seconds 20
```
Compositor screen recording is **damage-driven**: an idle desktop legitimately produces almost no
frames, so move the mouse on the host while it runs or a working mirror reads as a stall.
Compositor screen recording is **damage-driven**: an idle desktop produces almost no frames, so move
the mouse on the host while it runs or a working mirror reads as a stall.
### Absolute input follows the pin
Pinning a monitor also re-aims **absolute** mouse and pen input: the host anchors it to that head's
origin, so a click lands where you point on *that* screen. It's matched by position rather than by
size, because two monitors can be the same size and getting that wrong puts the pointer silently
on the wrong screen. The host resolves the pin at startup and again whenever the console writes it,
so a change needs no restart; the log line to look for is `capture monitor: …`.
Pinning a monitor also re-aims **absolute** mouse and pen input to that head's origin, so a click
lands where you point on *that* screen. Heads are matched by position, not size — two monitors can
be the same size, and getting that wrong puts the pointer silently on the wrong screen. The host
resolves the pin at startup and whenever the console writes it, so no restart is needed; the log
line is `capture monitor: …`.
To check it with no client involved:
@@ -124,44 +109,43 @@ To check it with no client involved:
punktfunk-host anchor-test --monitor HDMI-A-1
```
It lists this host's heads, says whether the box even has the same-size pair the matching exists
for, walks the pointer through the centre and the corners so you can watch which screen it moves
on, and prints the region it mapped into. `--none` runs the same walk unanchored, as an A/B.
It lists this host's heads, says whether the box has the same-size pair the matching exists for,
walks the pointer through the centre and corners so you can watch which screen it moves on, and
prints the region it mapped into. `--none` runs the same walk unanchored, as an A/B.
The anchor rides the **libei** injector — the backend a GNOME/Mutter host uses. On KWin, Sway and
Hyprland the host injects through a different protocol, and `anchor-test` stops with a message
saying so rather than reporting a green run that proves nothing.
Hyprland the host injects through a different protocol, and `anchor-test` stops and says so rather
than reporting a green run that proves nothing.
## Pick a preset
A preset is the easy way in — select one in the console and you're done. Each expands to a bundle of
the individual options documented further down.
Select one in the console and you're done. Each expands to a bundle of the options documented
further down.
| Preset | What it's for |
|---|---|
| **Default** | Good for most setups. Reconnects resume quickly, the streamed output becomes the whole desktop, and extra viewers each get their own screen. |
| **Headless box** | A machine with no monitor that you only ever stream from. The game and its display survive disconnects indefinitely (keep-alive **forever**), and whoever connects next takes the box over. Release it from the console when you're done. |
| **Shared desktop** | A PC you also use in person. Punktfunk never blanks your real monitors and never leaves a leftover display behind; extra viewers each get their own screen. |
| **Hot-desk** | One person at a time — roam between your own devices with an instant reconnect. Anyone else is told the box is busy, and each device+resolution keeps its own scaling. |
| **Workstation** | Your multi-monitor daily driver. Displays come back exactly where you arranged them, each client keeps its own settings, and the desktop is yours alone. |
| **Default** | Most setups. Reconnects resume quickly, the streamed output becomes the whole desktop, extra viewers each get their own screen. |
| **Headless box** | A monitorless machine you only stream from. Game and display survive disconnects indefinitely (keep-alive **forever**); whoever connects next takes the box over. Release it from the console when you're done. |
| **Shared desktop** | A PC you also use in person. Never blanks your real monitors, never leaves a leftover display behind; extra viewers each get their own screen. |
| **Hot-desk** | One person at a time — roam between your own devices with an instant reconnect. Anyone else is told the box is busy; each device+resolution keeps its own scaling. |
| **Workstation** | Your multi-monitor daily driver. Displays come back exactly where you arranged them, each client keeps its own settings, the desktop is yours alone. |
## Save your own preset
The five above are curated starting points. When you've dialed in a setup you like — whether by
picking a preset and tweaking it or by setting every option under **Custom** — you can **save it as
your own named preset** and switch back to it in one click later.
Once you've dialed in a setup — by tweaking a preset or setting every option under **Custom**
**save it as your own named preset** and switch back to it in one click.
- **Save as preset** — names the settings currently in force (all of the options below **plus**
- **Save as preset** — names the settings currently in force (all the options below **plus**
*Dedicated game sessions*) and adds it to the picker alongside the built-ins.
- **Apply** selecting a saved preset writes exactly those settings, the same as picking a built-in.
- **Edit / delete** — rename a saved preset, update it to your current settings, or remove it. Deleting
a preset never changes what's running — it only takes the card out of the picker.
- **Apply** — writes exactly those settings, like picking a built-in.
- **Edit / delete** — rename it, update it to your current settings, or remove it. Deleting never
changes what's running — it only takes the card out of the picker.
Unlike the built-in presets (which deliberately leave *Dedicated game sessions* alone so switching
presets never changes your game-launch routing), a **custom preset captures your full setup**,
including that axis — because it's *your* saved configuration, not a curated behavior bundle. Custom
presets live on the host in `display-presets.json` (next to `display-settings.json`); the catalog and
the active policy are independent, so editing a preset never disturbs a running session.
The built-in presets deliberately leave *Dedicated game sessions* alone, so switching presets never
changes your game-launch routing; a **custom preset captures your full setup**, including that axis
it's *your* saved configuration, not a curated behavior bundle. Custom presets live on the host in
`display-presets.json` (next to `display-settings.json`); the catalog and the active policy are
independent, so editing a preset never disturbs a running session.
## Options reference
@@ -169,41 +153,41 @@ Choose **Custom** in the console to set these directly.
### Keep alive
How long the virtual display survives after your last session disconnects. On a gamescope game host,
this also keeps the **game itself running** so you can reconnect straight back into it.
How long the virtual display survives after your last session disconnects. On a gamescope game host
this also keeps the **game itself running**, so you can reconnect straight back into it.
- **Off** — tear the display down at session end (nothing lingers).
- **A duration** (seconds) — keep it for that long; a reconnect inside the window drops you straight
- **Off** — tear the display down at session end.
- **A duration** (seconds) — keep it that long; a reconnect inside the window drops you straight
back in, with no re-negotiation and no desktop reshuffle.
- **Forever** — keep it until you stop the host or **release it** from the console (**Virtual
displays** → *Release*). This is the headless-box model.
displays** → *Release*). The headless-box model.
Default: **10 seconds**. Windows has always lingered 10 s; the Linux backends previously tore down
immediately — a short linger makes reconnects smoother on both.
**A reconnect always resumes the kept display** — the host recognises your device and hands back the
same display, even if you reconnect a second or two after dropping (before it has noticed you left).
**Deliberately quitting** (closing the client, not a network drop) tears the display down at once,
skipping the linger, so you don't leave a ghost behind. How quickly a *dropped* client is noticed is
the QUIC idle timeout — 8 s by default, tunable with `PUNKTFUNK_IDLE_TIMEOUT_MS` (see
[Legacy environment knobs](#legacy-environment-knobs)) if you want kept displays freed sooner.
same display, even a second or two after dropping (before it has noticed you left). **Deliberately
quitting** (closing the client, not a network drop) tears the display down at once, skipping the
linger. How quickly a *dropped* client is noticed is the QUIC idle
timeout — 8 s by default, tunable with `PUNKTFUNK_IDLE_TIMEOUT_MS` (see
[Legacy environment knobs](#legacy-environment-knobs)) to free kept displays sooner.
> **Keep-alive + Exclusive keeps your physical monitors dark after you disconnect**, until the
> linger expires or you release the display. That's intentional for a dedicated gaming box, but
> don't set a long/forever keep-alive together with Exclusive on a machine whose monitors you also
> use in person — use **Shared desktop** there instead.
> linger expires or you release the display. Intentional for a dedicated gaming box but don't set
> a long/forever keep-alive with Exclusive on a machine whose monitors you also use in person; use
> **Shared desktop** there.
### Topology
What Punktfunk does with your monitor layout while it streams.
- **Extend** — add the virtual display alongside your real monitors; touch nothing else.
- **Primary** — make the virtual display your primary output; your physical monitors stay on.
- **Primary** — make the virtual display your primary output; physical monitors stay on.
- **Exclusive** — the virtual display becomes your **only** enabled output (physical monitors are
disabled, then restored when streaming ends). This is what makes the streamed surface *be* the
desktop, so panels and windows land on it.
- **Automatic** *(default)* — Exclusive on Windows and on an auto-detected KDE/GNOME desktop
("stream this desktop" means the streamed output *is* the desktop); Extend when you've pinned a
disabled, then restored when streaming ends). This makes the streamed surface *be* the desktop,
so panels and windows land on it.
- **Automatic** *(default)* — Exclusive on Windows and on an auto-detected KDE/GNOME desktop;
Extend when you've pinned a
specific compositor with `PUNKTFUNK_COMPOSITOR` (a test/CI posture).
Per-backend support:
@@ -214,26 +198,24 @@ Per-backend support:
| Primary | ✅ | ✅ | ⚠️ treated as Extend | ✅ |
| Exclusive | ✅ | ✅ | ✅ | ✅ |
**Primary** has no equivalent on **Sway/wlroots and Hyprland**, and that is a Wayland fact rather
than a missing feature: there is no primary-output concept to set. What these compositors do have is
a *focused* output, and the host already points that at the streamed display — once at session start,
and again immediately before it launches anything from your library. Both open a new window on the
focused monitor, so that is what puts the game on the display you're streaming. Choosing Primary
therefore behaves as Extend, and the host says so in the log.
**Primary** has no equivalent on **Sway/wlroots and Hyprland** a Wayland fact, not a missing
feature: there is no primary-output concept. They have a *focused* output, and the host points that
at the streamed display — at session start, and again immediately before it launches anything from
your library. Both open new windows on the focused monitor, which is what puts the game on the
display you're streaming. Primary therefore behaves as Extend, and the host says so in the log.
One thing follows from it being focus rather than promotion: a window that opens *later* (a launcher
that spawns a second window, a game that re-parents itself) follows whatever has focus at that
moment, so if you're also sitting at the machine, clicking on a physical monitor mid-launch can still
pull a window over to it.
Because it's focus rather than promotion, a window that opens *later* (a launcher spawning a second
window, a game re-parenting itself) follows whatever has focus then — so if you're also sitting at
the machine, clicking a physical monitor mid-launch can still pull a window over to it.
**Exclusive** does disable your physical monitors on both, and switches them back on when the last
streaming display is torn down. Two details are specific to these compositors:
streaming display is torn down. Two compositor-specific details:
- Punktfunk only ever disables monitors it did not create, so a second client streaming at the same
time never goes dark.
- Punktfunk only disables monitors it did not create, so a second client streaming at the same time
never goes dark.
- On Hyprland the restore is a `hyprctl reload`, because nothing else re-enables a monitor that a
rule disabled — a re-applied monitor rule is accepted and ignored. That re-reads your Hyprland
config, which is what puts your monitors back; the side effect is that any settings you changed at
rule disabled — a re-applied monitor rule is accepted and ignored. The reload re-reads your
Hyprland config, which puts your monitors back; the side effect is that settings changed at
runtime with `hyprctl keyword` are dropped too, and a non-Lua config re-runs its `exec =` lines
(`exec-once` is not re-run). This only happens if a session actually disabled something.
@@ -244,8 +226,8 @@ streaming display is torn down. Two details are specific to these compositors:
box over (**steal**), share the existing display at its current mode (**join**), or refuse it
(**reject**). On Linux, `separate` gives each client its own display on the shared desktop. On
**Windows** a second client is **rejected** (a clean "host busy") even under `separate` — two
clients can't yet share one virtual display's capture there (that's a later stage), so the live
session is protected instead. A same-client *reconnect* never conflicts — it resumes.
clients can't yet share one virtual display's capture there (a later stage), so the live session
is protected instead. A same-client *reconnect* never conflicts — it resumes.
- **Identity** — whether each client gets a **stable display identity** so your desktop environment
remembers its settings (see [Persistent scaling](#persistent-scaling)): one shared identity, one
**per client**, or one **per client + resolution**.
@@ -263,28 +245,27 @@ streaming display is torn down. Two details are specific to these compositors:
Steam Deck / Bazzite couch box, a bare gamescope on a plain distro, or spawned into your live KDE /
GNOME / Sway desktop.
- **Dedicated** — every library launch gets its **own headless gamescope at your exact resolution and
refresh**, with just the game inside. The game boots straight in — no Steam Big Picture to navigate,
no game-mode desktop. Steam titles launch with the client hidden (`steam -silent`); non-Steam titles
start almost instantly (gamescope up in ~1 s, then the game's own boot). Combined with **keep alive**,
the game keeps running when you disconnect and you re-attach straight back into it.
refresh**, with just the game inside — no Steam Big Picture to navigate, no game-mode desktop.
Steam titles launch with the client hidden (`steam -silent`); non-Steam titles start almost
instantly (gamescope up in ~1 s, then the game's own boot). Combined with **keep alive**, the game
keeps running when you disconnect and you re-attach straight back into it.
Dedicated needs `gamescope` installed on the host; if it isn't, a launch falls back to **Auto**
Dedicated needs `gamescope` installed on the host; without it a launch falls back to **Auto**
routing. This axis is independent of the preset — pick it on the **Virtual displays** page. On a box
that's already in Steam game mode, a dedicated Steam launch frees game mode's Steam first and restores
it when the session ends. (GameStream / Moonlight launches follow the same routing.)
already in Steam game mode, a dedicated Steam launch frees game mode's Steam first and restores it
when the session ends. (GameStream / Moonlight launches follow the same routing.)
## When a game ends, and when a session does
A streaming session and the game the host launched for it can share a fate. Two switches, on the
**Virtual displays** page under **When a game or a session ends**. They apply to every store and both
protocols — and only ever to a game **this host launched for the session**: a game you started
yourself is never touched.
Two switches, on the **Virtual displays** page under **When a game or a session ends**, tie a
session to the game the host launched for it. They apply to every store and both protocols — and
only to a game **this host launched for the session**: a game you started yourself is never touched.
### When the game exits
**End the session** (default). Quit the game and your client goes back to its own library instead of
staring at your desktop. This is what a dedicated game session has always done; it now works on
every path — your live KDE/GNOME/Sway desktop, an attached gamescope, and Moonlight.
**End the session** (default). Quit the game and your client goes back to its own library. A
dedicated game session has always done this; it now works on every path — your live KDE/GNOME/Sway
desktop, an attached gamescope, and Moonlight.
**Keep streaming** if you stream the desktop and treat the game as incidental.
@@ -297,12 +278,12 @@ Whether stopping — or losing — a session also closes the game.
- **Close it on Stop** — closing the client, or pressing *Stop* in the console, closes the game.
A network drop does not: you get your game back when you reconnect.
- **Always close it** — a drop closes it too, but only after a **reconnect window** (5 minutes by
default). Reconnect inside the window and nothing happens; the console shows the countdown while
it runs, with an **End now** button if you'd rather not wait.
default). Reconnect inside the window and nothing happens; the console shows the countdown, with
an **End now** button if you'd rather not wait.
Closing a game costs whatever it hadn't saved, which is why nothing closes by default. The host asks
first — a polite close, the same thing clicking the window's X does, so the game runs its own
shutdown — and only forces the issue after ten seconds of being ignored.
first — a polite close, the same as clicking the window's X, so the game runs its own shutdown — and
only forces the issue after ten seconds of being ignored.
> **Keep alive and this setting are different clocks.** Keep-alive decides how long the *display*
> outlives a disconnect (10 s by default); the reconnect window decides how long the *game* does
@@ -312,7 +293,7 @@ shutdown — and only forces the issue after ten seconds of being ignored.
### On a gamescope session, the display has the final say
When a launch gets its **own gamescope** — a dedicated game session, the usual setup on a Steam Deck
or a Bazzite couch box — the game runs *inside* the streamed display. So it lives exactly as long as
or a Bazzite couch box — the game runs *inside* the streamed display, so it lives exactly as long as
that display does, and **Keep alive decides that, not the setting above**:
| you disconnect by | what happens to the game |
@@ -323,8 +304,7 @@ that display does, and **Keep alive decides that, not the setting above**:
So on a gamescope box, "leave the game running after I disconnect" means **keep-alive Forever** (or a
window long enough to come back in), not just this setting. On a desktop session — KWin, GNOME, Sway —
the game is an ordinary process next to your desktop and none of this applies; the setting above is
the whole story.
the game is an ordinary process next to your desktop and the setting above is the whole story.
### Automation
@@ -334,12 +314,12 @@ it or the host closed it), so a hook or plugin can react without polling. See
## Persistent scaling
Set your display **scaling** once and have it stick across reconnects. This works by giving each
client a *stable display identity*, so your desktop environment keys its per-monitor settings to it.
Set your display **scaling** once and have it stick across reconnects. Each client gets a *stable
display identity*, so your desktop environment keys its per-monitor settings to it.
| Host | Supported | How |
|---|---|---|
| **Windows** | ✅ today | Connect, set scaling in Settings while streaming — Windows remembers it per client. |
| **Windows** | ✅ today | Set scaling in Settings while streaming — Windows remembers it per client. |
| **KDE / KWin** | ✅ today | Set scaling in System Settings while streaming; KWin keys it to a stable per-client output name and reapplies it on reconnect. Validated live (150 %/125 % survive a full disconnect + reconnect). |
| **GNOME / Mutter** | ✅ today | GNOME's virtual-monitor API exposes no stable identity to key config on, so the **host persists the scale itself**: set scaling in Settings while streaming — the host captures the change, remembers it per client, and reapplies it on reconnect. |
| **Sway / wlroots** | ❌ | Headless outputs can't carry a stable identity; pin scale in your sway config instead. |
@@ -355,7 +335,7 @@ them — when a settings file exists, it wins.
| `PUNKTFUNK_NO_ISOLATE` | **Topology** → Extend *(Windows)* |
| `PUNKTFUNK_KWIN_VIRTUAL_PRIMARY` / `PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY` | **Topology** → Exclusive (when set) / Extend (when `0`) |
One knob has no console equivalent — it's a transport tuning, not display policy:
One knob has no console equivalent — it's transport tuning, not display policy:
- **`PUNKTFUNK_IDLE_TIMEOUT_MS`** (host, default `8000`) — how long the host waits before declaring a
*dropped* client gone, which is when a kept display starts its linger (or is freed). Lower it (e.g.
@@ -365,13 +345,13 @@ One knob has no console equivalent — it's a transport tuning, not display poli
## Troubleshooting
**My physical monitors stayed off after I disconnected.** You have keep-alive set together with
Exclusive topology — the display (and your isolated desktop) is being kept for the linger window.
Release it from the console (**Virtual displays**), or switch to the **Shared desktop** preset
so streaming never disables your real monitors.
**My physical monitors stayed off after I disconnected.** Keep-alive is set together with Exclusive
topology — the display (and your isolated desktop) is kept for the linger window. Release it from
the console (**Virtual displays**), or switch to the **Shared desktop** preset so streaming never
disables your real monitors.
**The virtual output shows only my wallpaper.** Your topology is Extend, so the streamed display is
an empty extension. Use **Primary** or **Exclusive** so your desktop actually lands on it.
an empty extension. Use **Primary** or **Exclusive** so your desktop lands on it.
**KWin can't create the virtual output.** On a normal Plasma session KWin runs its **DRM backend**,
which creates virtual outputs at any version. The 6.5.6 floor applies only to the **virtual backend**
@@ -387,18 +367,18 @@ follows the switch. If a launched game **exits**, a dedicated session ends and r
library; a game mode / desktop session keeps streaming.
**My keep-alive / topology / layout settings do nothing.** Check whether **Streamed screen** is set
to a real monitor. Those options are all about a display Punktfunk created; when it's mirroring one
of yours there is nothing of ours to keep alive or rearrange. Switch the card back to *Virtual screen
(default)* to get them back.
to a real monitor — those options are about a display Punktfunk created, and when it's mirroring one
of yours there is nothing to keep alive or rearrange. Switch the card back to *Virtual screen
(default)*.
**The console won't let me change Streamed screen.** `PUNKTFUNK_CAPTURE_MONITOR` is set in this
host's [`host.env`](/docs/configuration) and deliberately outranks the console. Unset it (and restart
the host) to choose from the console instead.
host's [`host.env`](/docs/configuration) and outranks the console. Unset it (and restart the host)
to choose from the console instead.
**My session fails with "no monitor named …".** The pinned connector isn't among this host's
monitors — it was renamed, unplugged, or the host is now in a different session. Run
`punktfunk-host list-monitors` on the host to see the real names. Punktfunk will not quietly stream a
different screen instead.
monitors — renamed, unplugged, or the host is now in a different session. Run
`punktfunk-host list-monitors` on the host to see the real names. Punktfunk will not quietly stream
a different screen.
**My couch box's TV stayed on the streamed session after I disconnected.** With the **Headless box**
preset (keep alive = *forever*), a managed Steam session is held indefinitely so a reconnect resumes
+85 -94
View File
@@ -3,25 +3,24 @@ title: Wake-on-LAN
description: How Punktfunk clients wake a sleeping host — what has to have happened first, what a click does, the punktfunk wake command, and how to arm the machine so the packet actually lands.
---
A host that is asleep answers nothing. Punktfunk works around that by remembering the host's network
card address — its **MAC address** — while the host is awake, and sending it a **magic packet**, the
standard Wake-on-LAN wake-up datagram, when you later ask to connect.
A sleeping host answers nothing. Punktfunk remembers the host's network card address — its **MAC
address** — while the host is awake, and sends it a **magic packet** (the standard Wake-on-LAN
datagram) when you later ask to connect.
The Linux, Windows, Apple and Android apps do this by default. There is nothing to enable in
Punktfunk itself. The work is on the *machine*: its BIOS/UEFI and its network card have to be armed
to wake, and that is where Wake-on-LAN usually fails. Jump to
[Arming the machine](#arming-the-machine) if that is what you are here for.
The Linux, Windows, Apple and Android apps do this by default; nothing to enable in Punktfunk. The
work is on the *machine*: its BIOS/UEFI and network card have to be armed to wake, and that is where
Wake-on-LAN usually fails — jump to [Arming the machine](#arming-the-machine) if that is what you
are here for.
## How it works
While it is running, the host advertises itself on the local network over mDNS. One of the things it
publishes is `mac` — the MAC address of the network card that carries the IP clients reach it on
first, then any other non-loopback cards as fallbacks, at most four.
While running, the host advertises itself over mDNS, including `mac` — the address of the card
carrying the IP clients reach it on first, then any other non-loopback cards as fallbacks, at most
four.
Each app stores those addresses on its **saved host** record. The Linux, Windows and Android apps
refresh them whenever they see the host advertise; the Apple app refreshes them when you save the
host and on every connect. When the host later sleeps it stops advertising entirely, but the client
still has the addresses on disk, so it can still aim a packet at the machine.
Each app stores those addresses on its **saved host** record. Linux, Windows and Android refresh
them whenever they see the host advertise; the Apple app when you save the host and on every
connect. A sleeping host stops advertising, but the client still has the addresses on disk.
That ordering is the whole prerequisite:
@@ -32,60 +31,58 @@ That ordering is the whole prerequisite:
The packet goes **out of every one of the client's network interfaces** — from a socket bound to
that interface's own address, aimed at both its subnet broadcast address and `255.255.255.255` — on
UDP ports 9 and 7, repeated three times, plus a unicast to the host's last known address. That
spread is deliberate: a sleeping machine has no ARP entry, so a plain unicast cannot find it, and a
broadcast sent without binding an interface leaves by the default route only, which on a machine
running a VPN or a mesh network is not the LAN the host sleeps on.
UDP ports 9 and 7, repeated three times, plus a unicast to the host's last known address. The spread
is deliberate: a sleeping machine has no ARP entry, so a plain unicast cannot find it, and a
broadcast sent without binding an interface leaves by the default route only on a machine running
a VPN or a mesh network, not the LAN the host sleeps on.
Neither the advert nor a magic packet is authenticated. That is fine here — a wrong address only
makes the wake fail, and the host's certificate fingerprint still gates the actual connection. See
[Security](/docs/security).
Neither the advert nor a magic packet is authenticated. A wrong address only makes the wake fail;
the host's certificate fingerprint still gates the connection. See [Security](/docs/security).
### Over Wi-Fi
A host on Wi-Fi wakes from the same packet. The mechanism is **WoWLAN** (Wake on Wireless LAN):
the adapter stays associated to your access point while the machine sleeps, the access point holds
broadcast frames for its sleeping stations and releases them on the next beacon, and the adapter
wakes the machine when one of them is a magic packet. Punktfunk publishes a Wi-Fi card's address
exactly like a wired one, so there is nothing different to do on the client — but the card has to be
armed for it, which is a different switch from the wired one. See
[Linux (Wi-Fi)](#linux-wi-fi) and [Windows](#windows) below.
A host on Wi-Fi wakes from the same packet via **WoWLAN** (Wake on Wireless LAN): the adapter stays
associated to your access point while the machine sleeps, the access point holds broadcast frames
for sleeping stations and releases them on the next beacon, and the adapter wakes the machine when
one is a magic packet. Punktfunk publishes a Wi-Fi card's address exactly like a wired one, so the
client side is unchanged — but the card has to be armed for it, a different switch from the wired
one. See [Linux (Wi-Fi)](#linux-wi-fi) and [Windows](#windows) below.
Two things can still stop it, and neither is visible from Punktfunk:
Two things can still stop it, neither visible from Punktfunk:
- Some access points and mesh systems drop or rate-limit broadcast traffic to sleeping stations
(often as "multicast enhancement", "broadcast filtering" or IGMP snooping). If wired hosts wake
and a Wi-Fi one never does, that is the first thing to turn off.
- Some laptops and adapters cut power to the Wi-Fi card in deeper sleep states, which drops the
and a Wi-Fi one never does, turn that off first.
- Some laptops and adapters cut power to the Wi-Fi card in deeper sleep states, dropping the
association and with it any chance of a wake.
## Waking from a client
**Auto-wake on connect** is a client setting, and it is **on by default**. You find it in Settings,
in the **Session** group ([Client settings](/docs/client-settings#behavior) covers what sits beside
it); the TV and controller layouts list it among the other general settings. It is a property of the
device and the network, so it is *not* part of a
**Auto-wake on connect** is a client setting, **on by default**, in Settings under **Session**
([Client settings](/docs/client-settings#behavior) covers what sits beside it); the TV and
controller layouts list it among the other general settings. It is a property of the device and
the network, so it is *not* part of a
[settings profile](/docs/profiles-and-links#what-a-profile-cant-change) — "Game" and "Work" cannot
disagree about it.
With auto-wake on, opening a saved host that is not advertising:
1. Fires one magic packet immediately, then **dials anyway**. Missing from mDNS does not mean
unreachable a host reached over a VPN or another subnet never advertises at all.
1. Fires one magic packet immediately, then **dials anyway** — missing from mDNS does not mean
unreachable; a host reached over a VPN or another subnet never advertises at all.
2. If the dial fails, shows a **"Waking…"** screen while it re-sends the packet every **6 seconds**
and watches for the host once a second.
3. Gives up after **90 seconds**. The Apple and Android apps, and Punktfunk Console (the
controller-driven shell), park there with **Try Again** and a cancel, rather than throwing an
error — a cold box that needs another ten seconds is common. The Linux and Windows apps close
the wait and tell you the host didn't come online; start the connect again to retry.
controller-driven shell), park there with **Try Again** and a cancel rather than an error — a
cold box that needs another ten seconds is common. The Linux and Windows apps close the wait and
tell you the host didn't come online; start the connect again to retry.
4. Reconnects when the host answers. In the Linux, Windows and Android apps, a host that came back
on a different DHCP address has its saved record re-pointed at the new one.
Turn auto-wake **off** and a connect goes straight through with no packet and no wait. That is the
setting for hosts behind a VPN, which look offline when they are not.
With auto-wake **off**, a connect goes straight through with no packet and no wait — the setting for
hosts behind a VPN, which look offline when they are not.
There is also an explicit wake action, and it works whether or not auto-wake is on. It sits on a
saved host's own menu, and only appears when that host is offline *and* an address is known:
There is also an explicit wake action, independent of auto-wake, on a saved host's own menu. It
appears only when that host is offline *and* an address is known:
| Client | Explicit wake | Type a MAC in by hand |
|---|---|---|
@@ -96,42 +93,40 @@ saved host's own menu, and only appears when that host is offline *and* an addre
| Punktfunk Console (controller shell) | on an offline host with a known address, the confirm button reads **Wake & Connect** — it waits, then connects | not offered |
Punktfunk Console carries the row too — **Wake hosts automatically**, in the same settings list the
desktop apps write — but its **Wake & Connect** button is an explicit action and appears whatever
that row says. In the Apple apps the same button appears when you drive them with a controller, but
there it does follow the auto-wake setting.
desktop apps write — but its **Wake & Connect** button is explicit and appears whatever that row
says. In the Apple apps the same button appears when you drive them with a controller, but there it
follows the auto-wake setting.
The Apple apps also publish a **Wake Host** action to Shortcuts, so an automation can wake a host
without opening the app. On iPhone and iPad it has a ready-made phrase: *"Wake ⟨host⟩ with
Punktfunk"*. It fails with a message if that host has no saved address yet.
On Android 17 and later the app needs the local-network permission before it can touch anything on
your LAN — discovery, the stream itself and a wake packet alike. It asks for it when you open the
host list, and shows an explanation with a link to system settings if you decline.
your LAN — discovery, the stream and a wake packet alike. It asks when you open the host list, and
shows an explanation with a link to system settings if you decline.
### On the Steam Deck
The [Decky plugin](/docs/steam-deck) has no wake button and no wake setting of its own. It starts
every stream through the client, so the wake is the client's, on exactly the terms above: a packet
the moment the host doesn't answer, re-sent every 6 seconds while the client watches for it once a
second, and the dial only when it really is back. It follows **Wake hosts automatically** in the
client's own settings — **Open Punktfunk → Settings** from the same panel — and is a no-op until the
client has learned that host's MAC address. (The plugin used to fire a packet itself and stretch the
connect budget to 75 seconds to cover the resume; a wait that watches for the host beats a fixed
budget, so that is gone.)
The [Decky plugin](/docs/steam-deck) has no wake button or wake setting of its own. It starts every
stream through the client, so the wake is the client's, on exactly the terms above packet, 6-second
re-send, once-a-second watch, dial only when the host is really back. It follows **Wake hosts
automatically** in the client's own settings (**Open Punktfunk → Settings** from the same panel)
and is a no-op until the client has learned that host's MAC address. (The plugin used to fire a
packet itself and stretch the connect budget to 75 seconds to cover the resume; a wait that watches
for the host beats a fixed budget, so that is gone.)
### From the command line
`punktfunk`, the client-side command, has a wake verb. It ships with the Linux `punktfunk-client`
packages and the Windows client; inside the Flatpak it is
`flatpak run --command=punktfunk io.unom.Punktfunk`. See [Host CLI](/docs/host-cli) for the rest of
it.
`flatpak run --command=punktfunk io.unom.Punktfunk`. See [Host CLI](/docs/host-cli) for the rest.
```bash
punktfunk wake <host-ref> [--wait]
```
A `<host-ref>` is a saved host's id, its name, or its address. Without `--wait` it sends the packet
and returns. With `--wait` it re-sends every 6 seconds and probes the host every second for up to 90
`<host-ref>` is a saved host's id, name, or address. Without `--wait` it sends the packet and
returns. With `--wait` it re-sends every 6 seconds and probes the host every second for up to 90
seconds, returning the moment the host answers.
| Exit code | Meaning |
@@ -146,8 +141,8 @@ first and runs the same wake-and-wait when it doesn't answer. You rarely need to
## Arming the machine
Two things have to be true on the host machine, and Punktfunk changes neither of them — deciding
whether a machine may be woken off the network is yours to make.
Two things have to be true on the host machine, and Punktfunk changes neither — whether a machine
may be woken off the network is your decision.
1. **BIOS/UEFI.** Turn on the setting called **Wake on LAN**, **Wake on PCIe** or similar. Its name
and location vary by vendor.
@@ -155,8 +150,8 @@ whether a machine may be woken off the network is yours to make.
### Check the host log first
This is the fastest diagnosis. On **Linux**, the host inspects the card carrying the address it
advertises, each time it starts advertising, and writes one line about it. A wired card:
The fastest diagnosis. On **Linux**, each time the host starts advertising it inspects the card
carrying the advertised address and writes one line. A wired card:
```text
Wake-on-LAN armed (magic packet) on host NIC
@@ -166,7 +161,7 @@ Wake-on-LAN armed (magic packet) on host NIC
Wake-on-LAN is NOT armed on this host's NIC — clients cannot wake it from sleep.
```
A Wi-Fi card, which is armed through an entirely different mechanism and is asked about separately
A Wi-Fi card, armed through a different mechanism and asked about separately
(`iw phy … wowlan show`, not `ethtool`):
```text
@@ -177,13 +172,13 @@ Wake-on-WLAN armed (magic packet) on host Wi-Fi NIC
Wake-on-WLAN is NOT armed on this host's Wi-Fi NIC — clients cannot wake it from sleep.
```
The warning line goes on to name the interface and the exact command to fix it. The host only
reports; it never changes the card's settings. It stays silent when it cannot tell — `iw` or
`ethtool` missing, a driver that doesn't answer, or not enough privilege — rather than guessing, and
it says nothing at all when mDNS adverts are switched off (`PUNKTFUNK_MDNS=0` or `--no-mdns`),
because then no address is published either.
The warning line names the interface and the exact command to fix it. The host only reports; it
never changes the card's settings. It stays silent when it cannot tell — `iw` or `ethtool` missing,
a driver that doesn't answer, or not enough privilege — rather than guessing, and says nothing at
all when mDNS adverts are off (`PUNKTFUNK_MDNS=0` or `--no-mdns`), because then no address is
published either.
Read the line on the web console's **Logs** page, or in the journal with
Read the line on the web console's **Logs** page, or with
`journalctl --user -u punktfunk-host`. See [Troubleshooting](/docs/troubleshooting#still-stuck).
**Windows and macOS hosts do not run this check**, so there is no log line to look for there.
@@ -203,21 +198,21 @@ Arm it:
sudo ethtool -s enp5s0 wol g
```
On many systems that does not survive a reboot. Re-run `ethtool enp5s0` after the next boot to check,
and make it permanent through your distribution's network configuration if it reset.
On many systems that does not survive a reboot. Re-run `ethtool enp5s0` after the next boot, and
make it permanent through your distribution's network configuration if it reset.
### Linux (Wi-Fi)
`ethtool` is the wrong tool here — most wireless drivers report `Wake-on: d` whether or not they are
armed, because the trigger lives in the wireless stack instead. Ask `iw`, using the *phy* behind the
armed, because the trigger lives in the wireless stack. Ask `iw`, using the *phy* behind the
interface (`/sys/class/net/wlan0/phy80211/name`, usually `phy0`):
```bash
iw phy phy0 wowlan show
```
`WoWLAN is disabled` means no wake. Armed looks like this, and the `* wake up on magic packet` line
is the one that matters:
`WoWLAN is disabled` means no wake. Armed looks like this; the `* wake up on magic packet` line is
the one that matters:
```text
WoWLAN is enabled:
@@ -231,8 +226,7 @@ sudo iw phy phy0 wowlan enable magic-packet
```
That setting is per-phy and NetworkManager re-applies its own on every connection, so on a
NetworkManager system make it stick on the connection instead — this survives reboots and
reconnects:
NetworkManager system set it on the connection instead — this survives reboots and reconnects:
```bash
sudo nmcli connection modify <connection> 802-11-wireless.wake-on-wlan magic
@@ -244,10 +238,9 @@ for what the hardware claims to support.
### Windows
Open **Device Manager**, find the network adapter under **Network adapters**, and open its
properties. On the **Power Management** tab, allow the device to wake the computer; on the
**Advanced** tab, enable the adapter's magic-packet wake property if it has one. Exact wording
depends on the driver.
Open **Device Manager**, find the adapter under **Network adapters**, and open its properties. On
the **Power Management** tab, allow the device to wake the computer; on the **Advanced** tab, enable
the adapter's magic-packet wake property if it has one. Exact wording depends on the driver.
Wi-Fi adapters use the same two tabs. The **Advanced** property is often called **Wake on Magic
Packet** there too, sometimes **Wake on Wireless LAN**; many Wi-Fi drivers expose neither, and those
@@ -257,17 +250,15 @@ to wake the machine — if the adapter is not in it, nothing on the network can
## Limits
- **Wired Ethernet is the sure thing; Wi-Fi works when the adapter supports WoWLAN.** Punktfunk
sends the same packet either way and publishes a Wi-Fi card's address like any other, but whether
a sleeping adapter is still listening is the adapter's and the access point's decision —
see [Over Wi-Fi](#over-wi-fi).
sends the same packet either way, but whether a sleeping adapter is still listening is the
adapter's and the access point's decision — see [Over Wi-Fi](#over-wi-fi).
- **Connect once while the host is awake**, on the same local network, before you rely on waking it.
A host you only ever added by address, on a network where mDNS never reached it, has no learned
address — the CLI will tell you so, and the apps will not offer the wake action. Typing the MAC in
by hand is the way round that everywhere except the Linux app.
- **Magic packets are broadcasts.** They do not cross subnets, and they do not travel over a VPN or a
mesh network. Client and host have to share a LAN segment for this to work at all.
A host only ever added by address, where mDNS never reached the client, has no learned address —
the CLI says so, and the apps don't offer the wake action. Typing the MAC in by hand is the way
round that everywhere except the Linux app.
- **Magic packets are broadcasts.** They do not cross subnets, a VPN or a mesh network. Client and
host have to share a LAN segment.
- **Punktfunk never puts a host to sleep, and never wakes one on a schedule.** A packet goes out
because a connect needs it, or because you asked for one.
- **There is no host-side switch.** The host publishes its MAC address and warns you when its card
is not armed. Everything else — whether to wake, when, and how long to wait is decided on the
client.
- **There is no host-side switch.** The host publishes its MAC address and warns when its card is
not armed. Whether to wake, when, and how long to wait is decided on the client.
+31 -34
View File
@@ -5,10 +5,10 @@ description: Enable the Punktfunk browser console, read or change its login pass
The web console is the browser UI for a Punktfunk host — live status, pairing, display policy, the
game library, logs, plugins and host updates. It ships as the **`punktfunk-web`** systemd user unit
on Linux and runs under the **Punktfunk Host service** on Windows, and serves on **`https://<host-ip>:47992`**
on Linux, runs under the **Punktfunk Host service** on Windows, and serves on **`https://<host-ip>:47992`**
(HTTPS with the host's own self-signed identity cert — your browser warns once; trust it and
continue). It's the surface you expose on the LAN to administer the host; the host's own management
API (47990) keeps every admin action loopback-only and off-loopback serves only read-only status +
API (47990) keeps every admin action loopback-only and off-loopback serves only read-only status and
game-library browsing to paired clients.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
@@ -16,41 +16,40 @@ game-library browsing to paired clients.
## Two ports, not one
The console also listens on **TCP 47993**, and plugin interfaces are served from there — same host,
same certificate, **different port**.
The console also listens on **TCP 47993**, where plugin interfaces are served — same host, same
certificate, **different port**.
That is a deliberate boundary rather than a second console. A plugin's interface is third-party
code, and on the console's own port the browser would treat it as part of the console: it could act
as you, with your logged-in session, against every admin action the console can reach. A different
port is a different *origin*, so the browser itself keeps the two apart — while staying the same
*site*, which is what lets your login still carry over so you don't sign in twice.
That is a deliberate boundary. A plugin's interface is third-party code; on the console's own port
the browser would let it act as you, with your logged-in session, against every admin action the
console can reach. A different port is a different *origin*, so the browser keeps the two apart —
but the same *site*, so your login still carries over.
What this means in practice:
In practice:
- **Open 47993 alongside 47992** on the host's firewall if you browse the console from another
device. The packaged firewall profiles already list both.
- **Trust the certificate twice.** Browsers store a self-signed certificate exception *per port*.
The first time you open a plugin, the console will notice it can't reach 47993 yet and offer a
link to open it in a tab — accept the warning there once, come back, and it works from then on.
The first time you open a plugin, the console notices it can't reach 47993 yet and offers a link
to open it in a tab — accept the warning there once and it works from then on.
- If a plugin's page is an empty panel, see
[A plugin's interface doesn't load](/docs/troubleshooting#a-plugins-interface-doesnt-load).
## Enable the console
- **Linux packages (apt / RPM / Bazzite):** on Ubuntu the host package is `punktfunk-host`
and on Fedora/Bazzite it's `punktfunk`; either way it *recommends* `punktfunk-web`, so your
package manager pulls the console in with the host (the Bazzite sysext image already contains
it). Enable and start it as your desktop user, then open the URL:
- **Linux packages (apt / RPM / Bazzite):** the host package (`punktfunk-host` on Ubuntu,
`punktfunk` on Fedora/Bazzite) *recommends* `punktfunk-web`, so your package manager pulls the
console in with the host (the Bazzite sysext image already contains it). Enable it as your
desktop user:
```sh
systemctl --user enable --now punktfunk-web
# then browse to https://<host-ip>:47992
```
- **Arch / CachyOS (pacman):** the console is an *optional* package here, and pacman never installs
optional dependencies — so install it yourself from the same repo the host came from (see
[Arch Linux](/docs/arch)), then enable it exactly as above. Take it as a full `-Syu`, never a bare
`pacman -S`, so you don't end up on a partial upgrade:
- **Arch / CachyOS (pacman):** the console is an *optional* package and pacman never installs
optional dependencies — install it from the same repo the host came from (see
[Arch Linux](/docs/arch)), then enable it as above. Use a full `-Syu`, never a bare `pacman -S`,
to avoid a partial upgrade:
```sh
sudo pacman -Syu punktfunk-web
@@ -58,20 +57,19 @@ What this means in practice:
```
- **Windows host:** the installer sets up the console and its runtime; the Punktfunk Host service
runs it and automatically brings it back if it ever stops. There is nothing to enable — open
`https://<this-PC>:47992`.
runs it and brings it back if it ever stops. Nothing to enable — open `https://<this-PC>:47992`.
- **SteamOS host:** the install script builds and starts the console as a user service for you. It
prints the URL when it finishes.
- **SteamOS host:** the install script builds and starts the console as a user service and prints
the URL when it finishes.
## Login password
The console is password-protected. Where that password lives and how you change it depends on the
The console is password-protected; where the password lives and how you change it depends on the
host platform.
**Linux packages (apt / RPM / Bazzite).** On first start `punktfunk-web-init` generates a random
password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it
back from the init service's journal or straight from the file:
from the init service's journal or the file:
```sh
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
@@ -93,7 +91,7 @@ Edit that file and `systemctl --user restart punktfunk-web` to change it.
**Windows host.** You choose the password during install — a secure random default is pre-filled and
shown again on the installer's final page. It's stored in `%ProgramData%\punktfunk\web-password` (as
`PUNKTFUNK_UI_PASSWORD=…`), readable only by Administrators and SYSTEM. To change it, edit the file
and restart the Punktfunk Host service (which runs the console) in an **elevated** PowerShell:
and restart the Punktfunk Host service from an **elevated** PowerShell:
```powershell
notepad "$env:ProgramData\punktfunk\web-password" # set PUNKTFUNK_UI_PASSWORD=<your-password>
@@ -104,12 +102,11 @@ Forgot it? See [Forgot your Password?](/docs/forgot-password).
## Arm pairing
The host **requires PIN pairing** by default (secure on a LAN). To connect the first time, open the
console, log in, then open **Pairing** in the sidebar and click **Pair a device**. The host shows a
one-time 4-digit PIN — enter it on your [client](/docs/clients) to pair. If the device already tried
to connect it appears under **Waiting for approval** instead; approving it pairs it immediately, no
PIN needed. See [Pairing & Trust](/docs/pairing) for the full trust model and how to approve or
remove devices later.
The host **requires PIN pairing** by default (secure on a LAN). To connect the first time, log in to
the console, open **Pairing** in the sidebar and click **Pair a device**. The host shows a one-time
4-digit PIN — enter it on your [client](/docs/clients). If the device already tried to connect it
appears under **Waiting for approval** instead; approving it pairs it immediately, no PIN needed.
[Pairing & Trust](/docs/pairing) has the full trust model and how to approve or remove devices later.
## What's in it
@@ -117,7 +114,7 @@ Nine destinations in the sidebar (a **More** tab on a phone holds the last five)
- **Dashboard** — live status: whether video and audio are streaming, the active sessions with
their codec, resolution, frame rate and bitrate, which games are running, and how many clients
are paired. Buttons here stop a session or ask the encoder for a fresh keyframe.
are paired. Buttons stop a session or ask the encoder for a fresh keyframe.
- **Host** — this host's identity (hostname, OS, local IP, version, unique id), the codecs it
advertises, its ports, the **Updates** card (see [Updating the Host](/docs/updating)), the
**GPUs** card — Automatic, or prefer one GPU for capture and encode, applied to the next session
@@ -68,10 +68,7 @@ Packaging internals live in
Punktfunk ships the same signed installer through its own winget source. Register the source once
per machine, from an **elevated** terminal:
```powershell
winget source add -n punktfunk https://winget.punktfunk.unom.io -t Microsoft.Rest
winget install unom.PunktfunkHost
```
<Install platform="windows" />
Before it downloads anything, winget shows the package's agreements — that audio needs Steam
installed on the host, and that Moonlight compatibility is off by default — and asks you to
@@ -101,7 +98,13 @@ bundled third-party pieces keep whatever their own vendors shipped.)
Releases **0.28.1 and earlier** were signed with our own self-signed certificate, and the docs then
asked you to import `punktfunk-host-windows_<ver>.cer`. Current builds no longer produce or need it —
[Install → Windows](/docs/install#windows) shows how to remove it if you imported one.
if you imported it back then, you can remove it from `Cert:\LocalMachine\Root` and
`Cert:\LocalMachine\TrustedPublisher` (the certificate issued to **unom**, thumbprint
`CD1EFDEEEC9743AFC38F56C5AF30C5A3009BE941`).
SmartScreen is a separate mechanism that builds reputation per publisher, so shortly after a new
signing certificate starts being used it can still show *"Windows protected your PC"* on the first
downloads — **More info → Run anyway**. It settles as installs accumulate.
The bundled drivers carry a **second, separate** self-signed certificate (`CN=punktfunk-driver`,
SHA-1 thumbprint `4B8493E7CD565758D335F8F4F05C5A7261A13E02`). The installer adds it to the machine's
+4
View File
@@ -2,12 +2,16 @@ import defaultMdxComponents from 'fumadocs-ui/mdx'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import type { MDXComponents } from 'mdx/types'
import BitrateCalculator from '@/components/BitrateCalculator'
import { Install, Ports } from '@/components/platforms'
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
// Custom components usable in any .md/.mdx without a per-file import.
BitrateCalculator,
// Install commands / port table quoted from data/platforms.json: <Install platform="debian" />, <Ports />
Install,
Ports,
// Per-platform instructions: <Tabs items={['Linux', 'Windows']}><Tab value="Linux">…
Tabs,
Tab,
+78
View File
@@ -0,0 +1,78 @@
// Install commands and port facts rendered from the platforms data file, so a docs page never
// restates a repo URL or a port number (CONTRIBUTING.md "Where facts live"). The canonical file
// is ../../data/platforms.json at the repo root; src/data/platforms.json is a byte-identical
// snapshot (the Docker build context is docs-site/ alone) that scripts/ci/check-docs-drift.sh
// keeps in sync — same arrangement as public/openapi.json.
import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'
import platforms from '@/data/platforms.json'
type Platform = (typeof platforms.platforms)[number]
function find(id: string): Platform {
const p = platforms.platforms.find((x) => x.id === id)
if (!p) throw new Error(`platforms.json has no platform "${id}"`)
return p
}
/** `<Install platform="debian" />` — the canonical install snippet for one platform. */
export function Install({ platform, title }: { platform: string; title?: string }) {
const p = find(platform)
if (!('install' in p) || !p.install) {
throw new Error(`platforms.json: "${platform}" has no install snippet (it has a url instead)`)
}
return (
<CodeBlock title={title}>
<Pre>
<code>
{p.install.map((line, i) => (
<span key={i} className="line">
{line}
</span>
))}
</code>
</Pre>
</CodeBlock>
)
}
/** `<Ports />` — every port the host and console use, with the firewall profile that opens it. */
export function Ports() {
const { ports, firewall } = platforms
const rows: [string, string, string, string][] = [
['Native control', `UDP ${ports.native.port}`, ports.native.what, firewall.native],
['Discovery', `UDP ${ports.mdns.port}`, ports.mdns.what, firewall.native],
['Management API', `TCP ${ports.mgmt.port}`, ports.mgmt.what, firewall.native],
['Video data', 'UDP (ephemeral)', ports.data.what, '—'],
['Web console', `TCP ${[ports.web.port, ...ports.web.also].join(', ')}`, ports.web.what, firewall.web],
[
'GameStream (Moonlight)',
`TCP ${ports.gamestream.tcp.join(', ')} · UDP ${ports.gamestream.udp.join(', ')}`,
ports.gamestream.what,
firewall.gamestream,
],
]
return (
<table>
<thead>
<tr>
<th>Plane</th>
<th>Port</th>
<th>What</th>
<th>firewalld service / ufw profile</th>
</tr>
</thead>
<tbody>
{rows.map(([plane, port, what, svc]) => (
<tr key={plane}>
<td>{plane}</td>
<td>
<code>{port}</code>
</td>
<td>{what}</td>
<td>{svc === '—' ? svc : <code>{svc}</code>}</td>
</tr>
))}
</tbody>
</table>
)
}
+206
View File
@@ -0,0 +1,206 @@
{
"$comment": "Single source for the install/port facts that used to drift across four surfaces (docs-and-onboarding-overhaul WP1). Consumers: docs-site install pages (<Install platform=…/> and <Ports/> in docs-site/src/components/platforms.tsx, reading the byte-identical snapshot docs-site/src/data/platforms.json that check-docs-drift.sh gates), the website download page (WP3), the guided install script (WP4). A port number, repo URL or install command lives HERE and nowhere else — pages quote it, they don't restate it. `install` is the shell snippet that puts the package on the box (lines, in order); `url` is a download/store link where there is no command. Validated by scripts/ci/check-docs-drift.sh (parse + snapshot sync; ponytail: cross-check against code literals when a consumer exists).",
"ports": {
"mgmt": {
"port": 47990,
"proto": "tcp",
"what": "management REST API (HTTPS + token; read-only status/library off loopback)",
"env": "PUNKTFUNK_MGMT_BIND",
"conflict": "Sunshine / Apollo / Vibeshine serve their web UI on 47990 too — the one port still shared with them once GameStream compat is off. Move it via PUNKTFUNK_MGMT_BIND; clients relearn it from discovery."
},
"web": {
"port": 47992,
"proto": "tcp",
"what": "web console (HTTPS, login-gated); plugin interfaces on 47993",
"also": [47993]
},
"native": {
"port": 9777,
"proto": "udp",
"what": "punktfunk/1 QUIC control port",
"env": "PUNKTFUNK_NATIVE_PORT"
},
"data": {
"port": null,
"proto": "udp",
"what": "per-session video data plane — ephemeral port the client hole-punches; nothing fixed to open",
"env": "PUNKTFUNK_DATA_PORT"
},
"mdns": {
"port": 5353,
"proto": "udp",
"what": "mDNS discovery"
},
"gamestream": {
"tcp": [47984, 47989, 48010],
"udp": [47998, 47999, 48000],
"what": "GameStream/Moonlight-compat planes (opt-in, PUNKTFUNK_GAMESTREAM=1)",
"conflict": "Sunshine / Apollo / Vibeshine bind these same fixed ports and advertise the same mDNS name — run only one GameStream host at a time, or keep punktfunk native-only."
}
},
"firewall": {
"$comment": "Service/profile names the Linux packages install for firewalld and ufw — the packages never open a port themselves.",
"native": "punktfunk-native",
"gamestream": "punktfunk-gamestream",
"web": "punktfunk-web"
},
"conflicts": {
"hosts": ["Sunshine", "Apollo", "Vibeshine"],
"detect": "punktfunk-host detect-conflicts",
"detectExit": "1 only when a conflicting host runs or will start on its own; dormant leftovers print but exit 0",
"docs": "/docs/switching-from-sunshine"
},
"platforms": [
{
"id": "debian",
"name": "Debian 13+ / Ubuntu 26.04+",
"installs": "host",
"packageManager": "apt",
"docs": "/docs/debian",
"repo": "https://git.unom.io/api/packages/unom/debian",
"install": [
"sudo install -d -m 0755 /etc/apt/keyrings",
"curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key | sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null",
"echo \"deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main\" | sudo tee /etc/apt/sources.list.d/punktfunk.list",
"sudo apt update",
"sudo apt install punktfunk-host"
]
},
{
"id": "arch",
"name": "Arch Linux / CachyOS",
"installs": "host",
"packageManager": "pacman",
"docs": "/docs/arch",
"repo": "https://git.unom.io/api/packages/unom/arch",
"install": [
"curl -fsS https://git.unom.io/api/packages/unom/arch/repository.key | sudo pacman-key --add -",
"sudo pacman-key --lsign-key E0CA04465C99C936E0B0C6510A317015A34DDD69",
"grep -q '^\\[punktfunk\\]' /etc/pacman.conf || printf '\\n[punktfunk]\\nServer = https://git.unom.io/api/packages/unom/arch/$repo/$arch\\n' | sudo tee -a /etc/pacman.conf >/dev/null",
"sudo pacman -Syu punktfunk-host"
]
},
{
"id": "fedora",
"name": "Fedora 43+",
"installs": "host",
"packageManager": "dnf",
"docs": "/docs/fedora",
"repo": "https://git.unom.io/api/packages/unom/rpm/fedora-44",
"install": [
"sudo tee /etc/yum.repos.d/punktfunk.repo >/dev/null <<'REPO'",
"[punktfunk]",
"name=punktfunk",
"# fedora-44 on Fedora 44; bazzite on Fedora 43 (a plain Fedora 43 build of the same package)",
"baseurl=https://git.unom.io/api/packages/unom/rpm/fedora-44",
"enabled=1",
"gpgcheck=1",
"repo_gpgcheck=1",
"gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key",
" https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk",
"REPO",
"sudo dnf install punktfunk"
]
},
{
"id": "bazzite",
"name": "Bazzite / Fedora Atomic",
"installs": "host",
"packageManager": "sysext",
"docs": "/docs/bazzite",
"install": [
"curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh",
"sudo bash punktfunk-sysext.sh install"
]
},
{
"id": "nixos",
"name": "NixOS",
"installs": "host",
"packageManager": "nix",
"docs": "/docs/nixos",
"install": [
"# flake input: inputs.punktfunk.url = \"git+https://git.unom.io/unom/punktfunk\";",
"# then: imports = [ punktfunk.nixosModules.default ]; services.punktfunk.host.enable = true;"
]
},
{
"id": "steamos",
"name": "SteamOS (Steam Deck as host)",
"installs": "host",
"packageManager": "script",
"docs": "/docs/steamos-host",
"install": [
"git clone https://git.unom.io/unom/punktfunk ~/punktfunk",
"bash ~/punktfunk/scripts/steamdeck/install.sh"
]
},
{
"id": "windows",
"name": "Windows 11 (22H2+)",
"installs": "host",
"packageManager": "winget",
"docs": "/docs/windows-host",
"install": [
"winget source add -n punktfunk https://winget.punktfunk.unom.io -t Microsoft.Rest",
"winget install unom.PunktfunkHost"
]
},
{
"id": "linux-client",
"name": "Linux client (any distro)",
"installs": "client",
"packageManager": "flatpak",
"docs": "/docs/install-client#linux-desktop-flatpak",
"install": [
"flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref"
]
},
{
"id": "steam-deck-client",
"name": "Steam Deck (Gaming Mode)",
"installs": "client",
"packageManager": "decky",
"docs": "/docs/steam-deck"
},
{
"id": "windows-client",
"name": "Windows client",
"installs": "client",
"packageManager": "msix",
"docs": "/docs/install-client#windows",
"install": [
"curl.exe -LO https://git.unom.io/api/packages/unom/generic/punktfunk-client-windows/latest/punktfunk-client-windows_x64.msix",
"Add-AppxPackage .\\punktfunk-client-windows_x64.msix"
]
},
{
"id": "macos-client",
"name": "macOS",
"installs": "client",
"packageManager": "dmg",
"docs": "/docs/install-client#macos",
"url": "https://git.unom.io/unom/punktfunk/releases"
},
{
"id": "apple-client",
"name": "iPhone, iPad, Apple TV",
"installs": "client",
"packageManager": "testflight",
"docs": "/docs/install-client#ios-ipados-apple-tv",
"url": "https://testflight.apple.com/join/Qr7uSemk"
},
{
"id": "android-client",
"name": "Android / Android TV",
"installs": "client",
"packageManager": "play",
"docs": "/docs/install-client#android",
"url": "https://play.google.com/store/apps/details?id=io.unom.punktfunk"
}
]
}
+4 -5
View File
@@ -43,11 +43,10 @@ the imported key), the repo-add uses `printf` rather than a heredoc so it works
default shell has no `<<EOF`), and Arch is rolling — packages are built against current Arch
sonames, so the box itself must stay updated.
The repo-add **appends** to `/etc/pacman.conf`, so running it twice leaves two `[punktfunk]`
blocks and every later pacman run opens with
`error: could not register 'punktfunk' database (database already registered)`.
It is harmless — pacman ignores the duplicate and carries on — but to silence it, delete the
extra block from `/etc/pacman.conf`.
The repo-add **appends** to `/etc/pacman.conf`; the documented line is guarded with a `grep -q` so a
second run is a no-op, and the symptom of an unguarded double add (`database already registered`)
is on the
[troubleshooting page](https://docs.punktfunk.unom.io/docs/troubleshooting#pacman-error-could-not-register-punktfunk-database-database-already-registered).
Then the same first-run steps as a source build (printed by the install scriptlet): `input`
group, `host.env`, `systemctl --user enable --now punktfunk-host` — see the next section.
+11 -36
View File
@@ -146,44 +146,19 @@ desktop mode, verifying the first stream — lives on the
install paths. Don't restate it here; a fact stated twice is a fact that drifts (see
"Where facts live" in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)).
## 3. Gotchas the docs don't carry (yet)
## 3. Gotchas
<!-- WP2 of the docs overhaul moves these into /docs/troubleshooting; until then this is their
only home, so they stay. -->
All user-facing ones live on the docs site now — the ffmpeg-libs weak dependency (NVENC fails at
runtime without RPM Fusion's build:
[Troubleshooting](https://docs.punktfunk.unom.io/docs/troubleshooting#no-video-on-fedora-nvenc-fails-ffmpeg-libs-is-missing))
and the ds_inhibit SELinux storm with DualSense-type pads
([Troubleshooting](https://docs.punktfunk.unom.io/docs/troubleshooting#stream-lags-then-freezes-with-a-dualsense-pad-bazzite-selinux);
the `dontaudit`-vs-`allow` rationale is the header of `punktfunk-ds-inhibit.cil`).
- **No video / NVENC fails to encode.** RPM Fusion's `ffmpeg-libs` (with NVENC) is a **weak
dependency** (`Recommends:` in the spec) — the package installs without it, and NVENC then
fails at runtime. Re-run the RPM Fusion step in Path C step 1.
(`packaging/rpm/punktfunk.spec`: `Recommends: ffmpeg-libs`.)
- **Dev-layout unit.** `scripts/punktfunk-host.service` (the upstream/dev unit) assumes the binary
at `%h/punktfunk/target/release/punktfunk-host`; the packaged binary is `/usr/bin/punktfunk-host`.
If `systemctl --user cat punktfunk-host` shows `ExecStart` pointing into a home dir, drop an
override (`systemctl --user edit punktfunk-host`) setting
`ExecStart=/usr/bin/punktfunk-host serve --gamestream` (or bare `serve` for native-only).
- **Stream lags, then freezes, with a DualSense-type client pad (SELinux enforcing).** The virtual
DualSense / DualShock 4 binds the kernel's `hid-playstation` driver, and Valve's `ds_inhibit`
(inside `steamos-manager`, shipped on Bazzite) reacts to *any* such hidraw by walking
`/proc/*/fd/` on every open/close. SELinux denies `steamos_manager_t` that walk, spraying
**~324 `avc: denied` per second**, and `setroubleshootd` amplifies the flood into a box-wide
fork storm that starves the stream (gamescope 0 fps, `tx_mbps` collapsing) — measured live on
Bazzite 43, 2026-08-15. Two traps while diagnosing: the AVC lines read `comm="tokio-rt-worker"`
— that is **steamos-manager, not punktfunk** (check `scontext=…steamos_manager_t…`); and once
started the setroubleshootd storm **outlives the denials by 15+ minutes**, so the box stays
starved after the pad is gone. Fixes:
- punktfunk ships a `dontaudit` SELinux drop-in that silences the flood (ds_inhibit then simply
leaves the pad uninhibited — harmless). The sysext installs it automatically on
install/update; on an existing install run `sudo punktfunk-sysext reapply`. On a layered or
bootc host: `sudo semodule -i /usr/share/punktfunk/selinux/punktfunk-ds-inhibit.cil`
(remove with `sudo semodule -r punktfunk-ds-inhibit`).
- **Hardening (recommended on any streaming host):** `sudo systemctl mask --now
setroubleshootd`. It is purely a desktop alert daemon — nothing depends on it
(`systemctl list-dependencies --reverse setroubleshootd` returns only itself) — and masking
it makes the box robust against *any* AVC burst, not just this one. Reversible with `unmask`.
- Workaround with the feature loss: set the **client's** Controller type to Xbox 360 (uinput,
no `hid-playstation`) — costs adaptive triggers, lightbar and touchpad. The host-side
`PUNKTFUNK_GAMEPAD` knob does **not** help: an explicit client choice outranks it.
One packager-only note: `scripts/punktfunk-host.service` (the upstream/dev unit) assumes the binary
at `%h/punktfunk/target/release/punktfunk-host`; the packaged binary is `/usr/bin/punktfunk-host`.
If `systemctl --user cat punktfunk-host` shows `ExecStart` pointing into a home dir, drop an override
(`systemctl --user edit punktfunk-host`) setting `ExecStart=/usr/bin/punktfunk-host serve`.
## Appendix — if the COPR isn't published yet
+9 -1
View File
@@ -20,7 +20,10 @@
# 4. Every command the host-cli.md tables list must still exist as a string literal in
# crates/punktfunk-host (same "docs describing removed things" class as gate 2).
# 5. data/platforms.json (the single source for install/port facts that docs, the website
# download page and the guided installer consume) must parse.
# download page and the guided installer consume) must parse, and docs-site/src/data/
# platforms.json — the snapshot the <Install/> and <Ports/> MDX components render from
# (the docs Docker build context is docs-site/ alone) — must be a byte copy of it, same
# rule as gate 1.
#
# Textual gates, so textual limits: gate 2/3 match token spelling, not env reads — a var name in
# a code comment counts as "exists", and a quoted constant that isn't an env var counts toward
@@ -98,5 +101,10 @@ elif command -v bun >/dev/null 2>&1; then
elif command -v node >/dev/null 2>&1; then
node -e "$json_check" || { echo "::error::data/platforms.json is not valid JSON"; fail=1; }
fi
if [ "$(cksum < data/platforms.json)" != "$(cksum < docs-site/src/data/platforms.json 2>/dev/null)" ]; then
echo "::error::docs-site/src/data/platforms.json is not a copy of data/platforms.json — re-sync it:"
echo " cp data/platforms.json docs-site/src/data/platforms.json"
fail=1
fi
exit "$fail"