From 5bc257f1ae3b984fdcddd88336d278b78a08ff78 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 13 Jun 2026 23:30:36 +0000 Subject: [PATCH] fix(headless/kde): virtual Punktfunk speaker + restart host with the session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio: a headless host has no speakers, and on a LAN with AirPlay devices PipeWire picks a random HomePod as default — so desktop audio (which the host captures from the default sink's monitor) went to a HomePod over AirPlay instead of to the client, and there was no "Punktfunk" output to select. Ship a `punktfunk-sink.conf` (a `support.null-audio-sink` adapter — NOT the non-existent module-null-sink, which makes pipewire refuse to start) with high priority.session so it's the default; run-headless-kde.sh installs it and restarts pipewire once on first install. The host then captures its monitor and streams it. (Disable AirPlay sinks out of band: `dnf remove pipewire-config-raop`.) Input: the host's libei portal D-Bus connection goes stale when the compositor session restarts the portal under it, and the in-process reopen loop can't recover it (EIS setup keeps timing out) — only a full restart does. Add PartOf=punktfunk-kde-session.service so the host restarts with the session. Both verified live on the Fedora 44 KDE box. Co-Authored-By: Claude Opus 4.8 (1M context) --- packaging/debian/build-deb.sh | 1 + packaging/rpm/punktfunk.spec | 2 ++ scripts/headless/punktfunk-sink.conf | 26 ++++++++++++++++++++++++++ scripts/headless/run-headless-kde.sh | 13 +++++++++++++ scripts/punktfunk-host.service | 6 ++++++ 5 files changed, 48 insertions(+) create mode 100644 scripts/headless/punktfunk-sink.conf diff --git a/packaging/debian/build-deb.sh b/packaging/debian/build-deb.sh index 7ea31f8..64cdcdc 100755 --- a/packaging/debian/build-deb.sh +++ b/packaging/debian/build-deb.sh @@ -48,6 +48,7 @@ sed -i 's#%h/punktfunk/target/release/punktfunk-host#/usr/bin/punktfunk-host#' \ install -Dm0755 scripts/headless/run-headless-kde.sh "$SHAREDIR/headless/run-headless-kde.sh" install -Dm0755 scripts/headless/run-headless-sway.sh "$SHAREDIR/headless/run-headless-sway.sh" install -Dm0644 scripts/headless/kde-authorized "$SHAREDIR/headless/kde-authorized" +install -Dm0644 scripts/headless/punktfunk-sink.conf "$SHAREDIR/headless/punktfunk-sink.conf" install -Dm0644 scripts/host.env.example "$SHAREDIR/host.env.example" install -Dm0644 packaging/bazzite/host.env "$SHAREDIR/host.env.bazzite" install -Dm0644 docs/api/openapi.json "$SHAREDIR/openapi.json" diff --git a/packaging/rpm/punktfunk.spec b/packaging/rpm/punktfunk.spec index 7d136fb..742a857 100644 --- a/packaging/rpm/punktfunk.spec +++ b/packaging/rpm/punktfunk.spec @@ -161,6 +161,8 @@ install -Dm0755 scripts/headless/run-headless-kde.sh %{buildroot}%{_datadir}/% install -Dm0755 scripts/headless/run-headless-sway.sh %{buildroot}%{_datadir}/%{name}/headless/run-headless-sway.sh # RemoteDesktop grant pre-seed for headless libei input (run-headless-kde.sh copies it in). install -Dm0644 scripts/headless/kde-authorized %{buildroot}%{_datadir}/%{name}/headless/kde-authorized +# Virtual "Punktfunk" speaker (null sink the host captures/streams; run-headless-kde.sh installs it). +install -Dm0644 scripts/headless/punktfunk-sink.conf %{buildroot}%{_datadir}/%{name}/headless/punktfunk-sink.conf install -Dm0644 scripts/host.env.example %{buildroot}%{_datadir}/%{name}/host.env.example install -Dm0644 packaging/bazzite/host.env %{buildroot}%{_datadir}/%{name}/host.env.bazzite install -Dm0644 packaging/kde/host.env %{buildroot}%{_datadir}/%{name}/host.env.kde diff --git a/scripts/headless/punktfunk-sink.conf b/scripts/headless/punktfunk-sink.conf new file mode 100644 index 0000000..d516caf --- /dev/null +++ b/scripts/headless/punktfunk-sink.conf @@ -0,0 +1,26 @@ +# PipeWire drop-in for the headless punktfunk host: a virtual "Punktfunk" speaker. +# +# A headless streaming host has no real speakers — desktop audio should go to a sink the host +# captures and streams to the client, not to whatever PipeWire picked (on a LAN with AirPlay +# devices that's often a random HomePod). This null sink is that capture target; its high +# priority.session makes WirePlumber select it as the default output, so apps play to it, the host +# captures its monitor (audio/linux.rs, stream.capture.sink), and the client hears the desktop. +# +# Copied to ~/.config/pipewire/pipewire.conf.d/ by run-headless-kde.sh. Pair with disabling RAOP +# (remove the pipewire-config-raop package / its 50-raop.conf symlink) so AirPlay devices don't +# show up as outputs at all. NB: a null sink is a `support.null-audio-sink` adapter object — NOT +# `libpipewire-module-null-sink`, which doesn't exist on PipeWire 1.x and makes the daemon refuse +# to start (mandatory-module load failure). +context.objects = [ + { factory = adapter + args = { + factory.name = support.null-audio-sink + node.name = "punktfunk" + node.description = "Punktfunk" + media.class = "Audio/Sink" + object.linger = true + audio.position = [ FL FR ] + priority.session = 2000 + } + } +] diff --git a/scripts/headless/run-headless-kde.sh b/scripts/headless/run-headless-kde.sh index a4794f9..b8ef8c7 100755 --- a/scripts/headless/run-headless-kde.sh +++ b/scripts/headless/run-headless-kde.sh @@ -130,6 +130,19 @@ if [[ ! -s "$DB" && -s "$SELF_DIR/kde-authorized" ]]; then echo "seeded RemoteDesktop grant: $DB" fi +# Virtual "Punktfunk" speaker: a null sink (shipped next to this script) that the host captures + +# streams, set default so desktop audio goes there instead of a real/AirPlay device — a headless +# host has no speakers, and on a LAN with AirPlay gear PipeWire otherwise picks a random HomePod. +# pipewire reads its own config at start, so on FIRST install (config not yet present) restart it +# once to load the sink; later boots already have it. (Also disable AirPlay discovery out of band: +# `sudo dnf remove pipewire-config-raop`.) +PWSINK="$HOME/.config/pipewire/pipewire.conf.d/50-punktfunk-sink.conf" +if [[ ! -s "$PWSINK" && -s "$SELF_DIR/punktfunk-sink.conf" ]]; then + mkdir -p "$(dirname "$PWSINK")" && cp "$SELF_DIR/punktfunk-sink.conf" "$PWSINK" + echo "installed Punktfunk virtual speaker → restarting pipewire to load it" + systemctl --user restart pipewire 2>/dev/null || true +fi + # Reach graphical-session.target so xdg-desktop-portal (which is ordered After / fails its start # job without it) can come up — a headless linger session never gets there on its own, and Fedora's # target carries RefuseManualStart=yes, so drop that in once. Without the portal, libei input fails. diff --git a/scripts/punktfunk-host.service b/scripts/punktfunk-host.service index d95f5f6..411fd63 100644 --- a/scripts/punktfunk-host.service +++ b/scripts/punktfunk-host.service @@ -20,6 +20,12 @@ [Unit] Description=punktfunk GameStream + punktfunk/1 host After=pipewire.service punktfunk-kde-session.service +# PartOf the kwin session (when present): the host's libei input uses the RemoteDesktop portal, +# whose D-Bus connection goes stale if the compositor session restarts the portal under it — the +# in-process reopen loop can't recover that, but a full restart re-establishes it. So restart the +# host whenever the kde-session restarts. Ignored when punktfunk-kde-session isn't installed +# (gamescope / other backends). +PartOf=punktfunk-kde-session.service [Service] EnvironmentFile=%h/.config/punktfunk/host.env