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
+2
-1
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 47998–48000 — 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.)
|
||||
@@ -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).
|
||||
@@ -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 1–600.
|
||||
- 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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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 —
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 1–2 are the
|
||||
pad's speaker, channels 3–4 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 1–2 are the pad's speaker,
|
||||
channels 3–4 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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 47998–48000 — 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).
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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." />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:**
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
47998–48010, 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,
|
||||
|
||||
@@ -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 47998–48000 — 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.)
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user