known·good

Notes / chrome-remote-desktoppipewireaudiodbusarch

No audio in Chrome Remote Desktop session on PipeWire: "ALSA: Couldn't open audio device: Host is down"

· by shaun, written up with Claude

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.

Tested onArch 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

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:
textFailed 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-<user>/crd_audio#<id>
PULSE_SINK chrome_remote_desktop_session
PIPEWIRE_REMOTE crd_audio#<id>/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:

bashunset 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:

bashunset 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:

bashCRD_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:

bashdbus-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/<uid>/bus, so take its address from a session process:

bashDBUS=$(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:

    bashtr '\0' '\n' < /proc/<pid>/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:

    bashlsof /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.