--- title: No audio in Chrome Remote Desktop session on PipeWire: "ALSA: Couldn't open audio device: Host is down" date: 2026-09-11 summary: In a Chrome Remote Desktop X session on PipeWire, apps were silent or failed with "Host is down". CRD puts PULSE_RUNTIME_PATH, PULSE_SINK and PIPEWIRE_REMOTE into the session, pointing at its own PipeWire, which had not started. Unset them before dbus-launch, bridge system PipeWire to CRD's FIFO, check sink volume. tags: chrome-remote-desktop, pipewire, audio, dbus, arch environment: Arch Linux; PipeWire 1.6.8 with pipewire-pulse and WirePlumber; Chrome Remote Desktop (Linux host, X session from ~/.chrome-remote-desktop-session); LXQt 2.3 / Openbox author: shaun, written up with Claude status: published --- ## Symptoms Connected to a Linux desktop with Chrome Remote Desktop (CRD), no sound reached the CRD client. In a terminal inside the CRD session: ```text $ mpg123 beep.mp3 Couldn't open SDL audio: ALSA: Couldn't open audio device: Host is down ``` - A GUI media player (Parole) "played" an MP3 in silence, and no sink-input for it ever appeared in `pactl list sink-inputs`. - Command-line PipeWire tools (`pw-cli`, `pw-dump`) run in the session quietly talked to the wrong PipeWire instance. - After a first attempt at a fix, some apps started over D-Bus failed in libpulse with: ```text Failed to create secure directory () ``` ## Root cause Three separate problems were stacked. Each one alone is enough to leave the CRD client silent. ### 1. CRD's audio variables leak into the whole session CRD starts the remote session with three variables that point at a PipeWire instance of its own, which is supposed to write audio into a FIFO that the CRD host reads: | Variable | Value set by CRD | |---|---| | `PULSE_RUNTIME_PATH` | `/tmp/pyxdg-runtime-dir-fallback-/crd_audio#` | | `PULSE_SINK` | `chrome_remote_desktop_session` | | `PIPEWIRE_REMOTE` | `crd_audio#/pipewire` | On this machine CRD's own PipeWire never came up: it failed, CRD retried it five times, then started the host without it. (Our reading of why: the CRD session had no `XDG_RUNTIME_DIR` and no systemd user bus, so that PipeWire found no runtime directory.) The variables were still set, and everything started from the session script inherits them: the desktop session, panel, file manager, terminals, shells, and the `dbus-launch` session bus. ALSA clients going through the PipeWire ALSA plugin (mpg123's default output, GStreamer apps) follow `PIPEWIRE_REMOTE` to the dead instance and fail with `Host is down`. Setting `PULSE_SERVER` in `.bashrc` does not help them: it only affects libpulse clients, and only in interactive shells. ### 2. Empty values pushed into the D-Bus activation environment `dbus-launch` copies its own environment into the activation environment of the session bus, so D-Bus-activated services (for example `xdg-desktop-portal*`) got the bad variables too. `dbus-update-activation-environment` can set variables but cannot unset them, and the first attempt "cleared" them by setting them to empty strings. An empty `PULSE_RUNTIME_PATH` makes libpulse fail with `Failed to create secure directory ()`. We confirmed this by reproducing it. ### 3. The CRD sink came back at 19 % volume The working setup routes the system PipeWire into CRD's FIFO through a pipe-sink (see Fix). WirePlumber restores the last stored volume of a re-created sink; for this sink that was 19 % (-43 dB). So even when routing was correct, the audio was nearly silent. ## Fix ### Strip the variables before `dbus-launch` In `~/.chrome-remote-desktop-session`, put this at the very top, before `dbus-launch` and before the desktop session starts: ```bash unset PULSE_RUNTIME_PATH PULSE_SINK PIPEWIRE_REMOTE export XDG_RUNTIME_DIR=/run/user/$(id -u) export $(dbus-launch) exec startlxqt # or your session ``` Because the session bus is started after the `unset`, its activation environment is clean too. Apps now use the normal per-user PipeWire. For shells that still inherit the variables (for example in a session started before this change), unset them in your shell startup file as well: ```bash unset PULSE_RUNTIME_PATH PULSE_SINK PIPEWIRE_REMOTE ``` ### Bridge the system PipeWire into CRD's FIFO With CRD's own PipeWire not running, nothing writes to the FIFO the host reads. Load a pipe sink into the system PipeWire (through pipewire-pulse) pointing at that FIFO, and make it the default: ```bash CRD_DIR=$(find /tmp/pyxdg-runtime-dir-fallback-$USER "${XDG_RUNTIME_DIR:-/run/user/$UID}" \ -maxdepth 1 -type d -name 'crd_audio*' 2>/dev/null | head -1) CRD_FIFO="$CRD_DIR/fifo_output" [ -p "$CRD_FIFO" ] || mkfifo "$CRD_FIFO" pactl load-module module-pipe-sink sink_name=crd_sink file="$CRD_FIFO" \ rate=48000 channels=2 format=s16le pactl set-default-sink crd_sink pactl set-sink-volume crd_sink 100% # WirePlumber may restore an old low level pactl set-sink-mute crd_sink 0 ``` This has to run after the CRD host is up, because the FIFO directory belongs to the host's session. Running it late is fine: the host picks up data from the FIFO whenever it arrives. We start it from the session script after a fixed `sleep 20`. That delay is a guess at the host's start-up time, not a measured value. ### If something must update the D-Bus activation environment, set real values If you have a switcher script (or anything else) that fixes the activation environment of an already-running session bus, set working defaults instead of empty strings: ```bash dbus-update-activation-environment \ PIPEWIRE_REMOTE=pipewire-0 \ PULSE_RUNTIME_PATH="${XDG_RUNTIME_DIR:-/run/user/$UID}/pulse" ``` (The script on the affected machine also passes `PULSE_SINK=`. Only an empty `PULSE_RUNTIME_PATH` was shown to break libpulse. The repaired session ran with an empty `PULSE_SINK` and played audio, but that was not tested separately.) ### Repairing a session that is already running Processes that are already running keep the variables they started with. Without logging out, restart each *launcher* (panel, desktop/file manager, app runner, hotkey daemon, window manager, notification daemon, `xdg-desktop-portal*`) with a clean environment so its children inherit it. The CRD session bus is the `dbus-launch` one, not `/run/user//bus`, so take its address from a session process: ```bash DBUS=$(tr '\0' '\n' < /proc/$(pgrep -x lxqt-session)/environ \ | grep '^DBUS_SESSION_BUS_ADDRESS=' | cut -d= -f2-) env -u PULSE_RUNTIME_PATH -u PULSE_SINK -u PIPEWIRE_REMOTE -u PULSE_SERVER \ DISPLAY="$DISPLAY" XDG_RUNTIME_DIR=/run/user/$(id -u) \ DBUS_SESSION_BUS_ADDRESS="$DBUS" setsid -f lxqt-panel ``` D-Bus-activated services such as `xdg-desktop-portal*` can simply be killed after the activation environment is fixed; they restart on demand with the new environment. Kill them by PID (`pgrep -f '^/usr/lib/xdg-desktop-portal'`): `pkill -f xdg-desktop-portal-` also matched the command line of the shell running it and killed that shell. ### What was verified - A beep played with `mpg123` in a CRD terminal was heard in the CRD client. - The media player, relaunched with a clean environment, had a sink-input on `crd_sink`, uncorked, at 100 %. - Freshly D-Bus-activated `xdg-desktop-portal*` processes got `PIPEWIRE_REMOTE=pipewire-0` and a valid `PULSE_RUNTIME_PATH`. Not yet verified: a complete new CRD session using the updated session script. The session in this write-up was repaired in place. ## How it was found - Look for leaked variables in any process: ```bash tr '\0' '\n' < /proc//environ | grep -E 'PULSE|PIPEWIRE' ``` - PipeWire tools inside a CRD shell need the variable removed, or they talk to the dead instance: `env -u PIPEWIRE_REMOTE pw-cli ...`. - `busctl --user` and `systemctl --user show-environment` look at the systemd user bus, which is the wrong bus for a CRD session started with `dbus-launch`. - Check that the CRD host is reading the FIFO. `lsof` should show the host with it open for reading and `pipewire-pulse` with it open read/write: ```bash lsof /tmp/pyxdg-runtime-dir-fallback-$USER/crd_audio*/fifo_output timeout 8 dd if=/dev/zero of=/tmp/pyxdg-runtime-dir-fallback-$USER/crd_audio*/fifo_output bs=4096 count=100 ``` The `dd` (400 KiB) finished in about 1.8 s, which matches 48 kHz 16-bit stereo (192 kB/s). If it hangs, the host is not reading. - `pactl get-sink-volume crd_sink` showed the 19 % level.