[GH-ISSUE #413] Streamed apps have no audio on NVIDIA — audio producer pipeline never constructed (no pulsesrc, no record client in WolfPulseAudio) #268

Open
opened 2026-10-15 12:59:03 +00:00 by bsakaguchi · 6 comments
Owner

Originally created by @xavdematos on GitHub (May 20, 2026).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/413

Streamed apps have no audio on NVIDIA — audio producer pipeline never constructed (no pulsesrc, no record client in WolfPulseAudio)

Summary

Streamed apps on NVIDIA play silently. The per-session PulseAudio sink can be present and the streamed app can be bound to it cleanly, yet Wolf never seems to build the audio producer pipeline (pulsesrc device=virtual_sink_<id> ! ... ! interpipesink name=<id>_audio) — only the encoder/consumer pipeline (interpipesrc … ! opusenc ! rtpmoonlightpay_audio ! appsink) is ever logged, and WolfPulseAudio never gains a record client. As a result the Opus encoder has no audio source feeding it and the WebRTC audio track is silent.

The video side works correctly on the same setup (corresponding interpipesink … name=<id>_video IS present in logs).

Reproduced on three Wolf image tags (:stable, :fix-pulseaudio-nvidia-device-nodes, :alpha), identical symptom on each.

Environment

  • Host: Linux x86_64 with an NVIDIA GPU (driver 595.x).
  • Wolf GPU stack: gow nvidia-driver-vol (NVIDIA_DRIVER_VOLUME_NAME), NOT runtime: nvidia (avoids MESA ZINK: failed to choose pdev). Volume populated from gow/nvidia-driver Dockerfile pinned to the host driver version. nvidia-drm modeset=Y verified.
  • Wolf socket mounted as a directory bind (not a file bind) so a Wolf restart that recreates the socket doesn't leave clients with a stale file handle.
  • WolfPulseAudio image: ghcr.io/games-on-whales/pulseaudio:master (no other tag tried).
  • Wolf images tested (all identical failure mode below):
    • ghcr.io/games-on-whales/wolf:stable
    • ghcr.io/games-on-whales/wolf:fix-pulseaudio-nvidia-device-nodes
    • ghcr.io/games-on-whales/wolf:alpha (GStreamer 1.26.7-0)
  • Single-session, single Wolf app; one interpipesrc/interpipesink ID throughout.

What works (so this is squarely an audio-producer issue, not config / pulse-permissions / app-side)

  • WolfPulseAudio (default.pa loads module-native-protocol-unix auth-anonymous=1 + module-always-sink) is up; the streamed container shares its socket via the per-session volume.
  • The streamed app container creates the per-session sink itself via pactl load-module module-null-sink sink_name=$PULSE_SINK (since Wolf's own session-side [STREAM_SESSION] Create virtual audio sink doesn't actually result in a sink in our reproductions — auto_null only, but that's likely a downstream of the same issue and not the focus here).
  • Wolf's [PULSE_ROUTER] observes the sink and logs Sink appeared name='virtual_sink_<id>' idx=1 -> session='<id>'.
  • The streamed app (a PulseAudio client targeting virtual_sink_<id> explicitly) opens PulseAudio cleanly — buffer allocated, audio driver started, no fallback errors.

What does NOT happen

Across many sessions on each of the three Wolf images:

  1. No pulsesrc ever appears in Wolf's logs at WOLF_LOG_LEVEL=DEBUG. docker logs wolf | grep pulsesrc is empty for the entire container lifetime.
  2. No audio interpipesink is ever logged. Wolf's Starting audio pipeline: debug shows only the encoder/consumer pipeline:
    interpipesrc name=interpipesrc_<id>_audio listen-to=<id>_audio is-live=true stream-sync=restart-ts max-bytes=0 max-buffers=3 block=false !
    queue max-size-buffers=3 leaky=downstream ! audiorate ! audioconvert !
    opusenc bitrate=96000 bitrate-type=cbr frame-size=5 bandwidth=fullband audio-type=restricted-lowdelay max-payload-size=1400 !
    queue ! rtpmoonlightpay_audio name=moonlight_pay packet_duration=5 encrypt=true aes_key="…" aes_iv="…" !
    appsink name=wolf_udp_sink
    
    The matching producer pipeline (pulsesrc device=virtual_sink_<id> … ! interpipesink name=<id>_audio) is never logged. Compare the video side, where interpipesink … name=<id>_video IS present.
  3. WolfPulseAudio never gains a record client. From inside the pulse container as uid 1000:
    $ pactl list short clients
    0   protocol-native.c   wolf       # PULSE_ROUTER subscription
    N   protocol-native.c   pactl      # only the diagnostic invocation
    
    There is no Wolf-side recording client (no client recording from virtual_sink_<id>.monitor) at any point during an active stream.
  4. pactl list short sink-inputs is empty even during active gameplay, although the streamed app's [Audio] driver log indicates it has an open PulseAudio stream.

Minimal reproduction

  1. Wolf + WolfPulseAudio on NVIDIA with the nvidia-driver-vol method (or any working video setup).
  2. One streamed app whose container plays to $PULSE_SINK = virtual_sink_<sessionid> (any PulseAudio client targeting that sink will do).
  3. Ensure the sink exists in the shared pulse server (we create it ourselves from the runner since Wolf's session-side creation also fails on our setup, but the audio-producer-pipeline absence is independent of that).
  4. Stream the app from a Moonlight (or moonlight-web) client.
  5. Observe: video works, app's audio driver started OK, sink exists, PULSE_ROUTER reports Sink appeared, but client hears nothing.
  6. Verify: docker logs wolf | grep pulsesrc → empty. pactl list short clients inside WolfPulseAudio → only wolf (router) and your own diagnostic. pactl list short sink-inputs → empty.

Source pointers / model

  • src/moonlight-server/audio/pulse_router.cpp — router model (move sink-inputs onto virtual_sink_<id>); does NOT construct the audio capture pipeline itself.
  • src/moonlight-server/streaming/streaming.cpp::start_streaming_audio() — uses pulsesrc device="{sink_name}" server="{server_name}" against audio_session->gst_pipeline; expected to be where the producer pipeline goes live. In our reproductions this code path either isn't entered or its run_pipeline() is silently no-op on NVIDIA.

What we'd like to know

  • Is the audio producer pipeline meant to be visible in WOLF_LOG_LEVEL=DEBUG? If yes, its absence indicates the construction is skipped; pointers to the precondition that's being missed (any required gstreamer.audio template, audio bitrate, session config flag) would be enough to unblock us.
  • Any known regression that would fit this profile (PULSE_ROUTER refactor era)?

I can attach the raw Wolf log file, pactl list snapshots, and the streamed-app's PulseAudio init log if helpful — happy to share whatever helps.

Thanks for Wolf — the rest of the stack (video, controller, save sync) works beautifully.

Originally created by @xavdematos on GitHub (May 20, 2026). Original GitHub issue: https://github.com/games-on-whales/wolf/issues/413 # Streamed apps have no audio on NVIDIA — audio producer pipeline never constructed (no `pulsesrc`, no record client in WolfPulseAudio) ## Summary Streamed apps on NVIDIA play silently. The per-session PulseAudio sink can be present and the streamed app can be bound to it cleanly, yet Wolf never seems to build the **audio producer** pipeline (`pulsesrc device=virtual_sink_<id> ! ... ! interpipesink name=<id>_audio`) — only the **encoder/consumer** pipeline (`interpipesrc … ! opusenc ! rtpmoonlightpay_audio ! appsink`) is ever logged, and `WolfPulseAudio` never gains a record client. As a result the Opus encoder has no audio source feeding it and the WebRTC audio track is silent. The video side works correctly on the same setup (corresponding `interpipesink … name=<id>_video` IS present in logs). Reproduced on **three Wolf image tags** (`:stable`, `:fix-pulseaudio-nvidia-device-nodes`, `:alpha`), identical symptom on each. ## Environment - Host: Linux x86_64 with an NVIDIA GPU (driver 595.x). - Wolf GPU stack: gow `nvidia-driver-vol` (NVIDIA_DRIVER_VOLUME_NAME), NOT `runtime: nvidia` (avoids `MESA ZINK: failed to choose pdev`). Volume populated from `gow/nvidia-driver` Dockerfile pinned to the host driver version. `nvidia-drm modeset=Y` verified. - Wolf socket mounted as a directory bind (not a file bind) so a Wolf restart that recreates the socket doesn't leave clients with a stale file handle. - WolfPulseAudio image: `ghcr.io/games-on-whales/pulseaudio:master` (no other tag tried). - Wolf images tested (all identical failure mode below): - `ghcr.io/games-on-whales/wolf:stable` - `ghcr.io/games-on-whales/wolf:fix-pulseaudio-nvidia-device-nodes` - `ghcr.io/games-on-whales/wolf:alpha` (GStreamer 1.26.7-0) - Single-session, single Wolf app; one `interpipesrc/interpipesink` ID throughout. ## What works (so this is squarely an audio-producer issue, not config / pulse-permissions / app-side) - `WolfPulseAudio` (default.pa loads `module-native-protocol-unix auth-anonymous=1` + `module-always-sink`) is up; the streamed container shares its socket via the per-session volume. - The streamed app container creates the per-session sink itself via `pactl load-module module-null-sink sink_name=$PULSE_SINK` (since Wolf's own session-side `[STREAM_SESSION] Create virtual audio sink` doesn't actually result in a sink in our reproductions — `auto_null` only, but that's likely a downstream of the same issue and not the focus here). - Wolf's `[PULSE_ROUTER]` observes the sink and logs `Sink appeared name='virtual_sink_<id>' idx=1 -> session='<id>'`. - The streamed app (a PulseAudio client targeting `virtual_sink_<id>` explicitly) opens PulseAudio cleanly — buffer allocated, audio driver started, no fallback errors. ## What does NOT happen Across many sessions on each of the three Wolf images: 1. **No `pulsesrc` ever appears** in Wolf's logs at `WOLF_LOG_LEVEL=DEBUG`. `docker logs wolf | grep pulsesrc` is empty for the entire container lifetime. 2. **No audio `interpipesink`** is ever logged. Wolf's `Starting audio pipeline:` debug shows only the encoder/consumer pipeline: ``` interpipesrc name=interpipesrc_<id>_audio listen-to=<id>_audio is-live=true stream-sync=restart-ts max-bytes=0 max-buffers=3 block=false ! queue max-size-buffers=3 leaky=downstream ! audiorate ! audioconvert ! opusenc bitrate=96000 bitrate-type=cbr frame-size=5 bandwidth=fullband audio-type=restricted-lowdelay max-payload-size=1400 ! queue ! rtpmoonlightpay_audio name=moonlight_pay packet_duration=5 encrypt=true aes_key="…" aes_iv="…" ! appsink name=wolf_udp_sink ``` The matching producer pipeline (`pulsesrc device=virtual_sink_<id> … ! interpipesink name=<id>_audio`) is never logged. Compare the video side, where `interpipesink … name=<id>_video` IS present. 3. **`WolfPulseAudio` never gains a record client.** From inside the pulse container as uid 1000: ``` $ pactl list short clients 0 protocol-native.c wolf # PULSE_ROUTER subscription N protocol-native.c pactl # only the diagnostic invocation ``` There is **no** Wolf-side recording client (no client recording from `virtual_sink_<id>.monitor`) at any point during an active stream. 4. **`pactl list short sink-inputs` is empty** even during active gameplay, although the streamed app's `[Audio]` driver log indicates it has an open PulseAudio stream. ## Minimal reproduction 1. Wolf + WolfPulseAudio on NVIDIA with the `nvidia-driver-vol` method (or any working video setup). 2. One streamed app whose container plays to `$PULSE_SINK = virtual_sink_<sessionid>` (any PulseAudio client targeting that sink will do). 3. Ensure the sink exists in the shared pulse server (we create it ourselves from the runner since Wolf's session-side creation also fails on our setup, but the audio-producer-pipeline absence is independent of that). 4. Stream the app from a Moonlight (or moonlight-web) client. 5. Observe: video works, app's audio driver started OK, sink exists, PULSE_ROUTER reports `Sink appeared`, but client hears nothing. 6. Verify: `docker logs wolf | grep pulsesrc` → empty. `pactl list short clients` inside WolfPulseAudio → only `wolf` (router) and your own diagnostic. `pactl list short sink-inputs` → empty. ## Source pointers / model - `src/moonlight-server/audio/pulse_router.cpp` — router model (move sink-inputs onto `virtual_sink_<id>`); does NOT construct the audio capture pipeline itself. - `src/moonlight-server/streaming/streaming.cpp::start_streaming_audio()` — uses `pulsesrc device="{sink_name}" server="{server_name}"` against `audio_session->gst_pipeline`; expected to be where the producer pipeline goes live. In our reproductions this code path either isn't entered or its `run_pipeline()` is silently no-op on NVIDIA. ## What we'd like to know - Is the audio producer pipeline meant to be visible in `WOLF_LOG_LEVEL=DEBUG`? If yes, its absence indicates the construction is skipped; pointers to the precondition that's being missed (any required `gstreamer.audio` template, audio bitrate, session config flag) would be enough to unblock us. - Any known regression that would fit this profile (PULSE_ROUTER refactor era)? I can attach the raw Wolf log file, `pactl list` snapshots, and the streamed-app's PulseAudio init log if helpful — happy to share whatever helps. Thanks for Wolf — the rest of the stack (video, controller, save sync) works beautifully.
Author
Owner

@ABeltramo commented on GitHub (May 20, 2026):

I've skimmed thru the massive text wall, unfortunately it's missing any useful info that I can use to help you out. 😅

Please post the full Wolf logs with the added env variable WOLF_LOG_LEVEL=DEBUG and which Moonlight client are you using for streaming.

<!-- gh-comment-id:4501697595 --> @ABeltramo commented on GitHub (May 20, 2026): I've skimmed thru the massive text wall, unfortunately it's missing any useful info that I can use to help you out. 😅 Please post the full Wolf logs with the added env variable `WOLF_LOG_LEVEL=DEBUG` and which Moonlight client are you using for streaming.
Author
Owner

@xavdematos commented on GitHub (May 21, 2026):

wolf-413-fulllog-scrubbed.txt

Moonlight client: the browser-based moonlight-web-stream (mrcreativ3001's web client / WebRTC bridge), not a native Moonlight app. Signaling is over a WebSocket; media is WebRTC. (Full Wolf DEBUG log attached as wolf-413-fulllog-scrubbed.txt.)

Environment: Wolf :stable, pulseaudio:master, NVIDIA (RTX 3080, driver 595.71.05), Ubuntu 25.04 host. nvh264enc hardware path, audio server enabled.

The bug, pinned at the PulseAudio layer

Captured live from inside the PulseAudio container during an active stream (an emulator is running and producing sound):

$ pactl list short sinks
1   virtual_sink_5209591930361698699   module-null-sink.c   s16le 2ch 48000Hz   RUNNING

$ pactl list short sink-inputs        # the app's audio IS reaching the sink
4   1   39   protocol-native.c   s16le 2ch 48000Hz
5   1   39   protocol-native.c   s16le 2ch 44100Hz

$ pactl list short source-outputs     # <-- EMPTY: nothing records the .monitor
(none)

$ pactl list short clients
0    protocol-native.c   wolf
38   protocol-native.c   waybar
39   protocol-native.c   xemu
47   protocol-native.c   pactl

So:

  1. The per-session sink virtual_sink_<id> exists and is RUNNING.
  2. The streamed app (client xemu here, same with RetroArch) has live sink-inputs — game audio is flowing into the sink.
  3. There is no source-output on the sink's .monitor and no recording client — i.e. Wolf never opens a pulsesrc to capture the monitor, so nothing feeds the Opus encoder. Result: encoded audio stream is silent.

Matching evidence in the Wolf DEBUG log

The audio encoder pipeline starts fine (consumer side):

Starting audio pipeline:
interpipesrc name=interpipesrc_<id>_audio listen-to=<id>_audio ... !
queue ... ! audiorate ! audioconvert !
opusenc bitrate=96000 ... !
queue ! rtpmoonlightpay_audio ... !
appsink name=wolf_udp_sink

…but the producer/capture pipeline that should feed <id>_audio is never built. In the full attached log:

  • Create virtual audio sink — logged 7× (once per session attempt)
  • pulsesrc — 0 occurrences
  • audio interpipesink (i.e. interpipesink name=<id>_audio) — 0 occurrences

Compare the video side, which logs both halves (interpipesink name=<id>_video producer + the interpipesrc … nvh264enc consumer) and works perfectly. Only the audio producer is missing.

Note on the sink (so the logs make sense)

You'll see the sink gets created by a pactl load-module module-null-sink sink_name=virtual_sink_<id> from inside our app container — that's a workaround we added because in our earlier runs Wolf's own Create virtual audio sink step produced only the fallback auto_null (the named per-session sink never appeared). With the sink force-created, [PULSE_ROUTER] does log Sink appeared name='virtual_sink_<id>' and moves the app's sink-input onto it — but it still never opens a capture pulsesrc on the monitor. So whether or not the sink exists, the capture side isn't instantiated on this NVIDIA setup.

Happy to run any specific pactl/gst probes or test a patched build.

<!-- gh-comment-id:4508394865 --> @xavdematos commented on GitHub (May 21, 2026): [wolf-413-fulllog-scrubbed.txt](https://github.com/user-attachments/files/28101382/wolf-413-fulllog-scrubbed.txt) **Moonlight client:** the browser-based **moonlight-web-stream** (mrcreativ3001's web client / WebRTC bridge), not a native Moonlight app. Signaling is over a WebSocket; media is WebRTC. (Full Wolf DEBUG log attached as `wolf-413-fulllog-scrubbed.txt`.) **Environment:** Wolf `:stable`, `pulseaudio:master`, NVIDIA (RTX 3080, driver 595.71.05), Ubuntu 25.04 host. `nvh264enc` hardware path, audio server enabled. ## The bug, pinned at the PulseAudio layer Captured live from inside the PulseAudio container **during an active stream** (an emulator is running and producing sound): ``` $ pactl list short sinks 1 virtual_sink_5209591930361698699 module-null-sink.c s16le 2ch 48000Hz RUNNING $ pactl list short sink-inputs # the app's audio IS reaching the sink 4 1 39 protocol-native.c s16le 2ch 48000Hz 5 1 39 protocol-native.c s16le 2ch 44100Hz $ pactl list short source-outputs # <-- EMPTY: nothing records the .monitor (none) $ pactl list short clients 0 protocol-native.c wolf 38 protocol-native.c waybar 39 protocol-native.c xemu 47 protocol-native.c pactl ``` So: 1. The per-session sink `virtual_sink_<id>` exists and is **RUNNING**. 2. The streamed app (client `xemu` here, same with RetroArch) has live **sink-inputs** — game audio is flowing into the sink. 3. There is **no source-output on the sink's `.monitor`** and **no recording client** — i.e. Wolf never opens a `pulsesrc` to capture the monitor, so nothing feeds the Opus encoder. Result: encoded audio stream is silent. ## Matching evidence in the Wolf DEBUG log The audio **encoder** pipeline starts fine (consumer side): ``` Starting audio pipeline: interpipesrc name=interpipesrc_<id>_audio listen-to=<id>_audio ... ! queue ... ! audiorate ! audioconvert ! opusenc bitrate=96000 ... ! queue ! rtpmoonlightpay_audio ... ! appsink name=wolf_udp_sink ``` …but the **producer/capture** pipeline that should feed `<id>_audio` is never built. In the full attached log: - `Create virtual audio sink` — logged **7×** (once per session attempt) - `pulsesrc` — **0 occurrences** - audio `interpipesink` (i.e. `interpipesink name=<id>_audio`) — **0 occurrences** Compare the video side, which logs both halves (`interpipesink name=<id>_video` producer + the `interpipesrc … nvh264enc` consumer) and works perfectly. Only the audio producer is missing. ## Note on the sink (so the logs make sense) You'll see the sink gets created by a `pactl load-module module-null-sink sink_name=virtual_sink_<id>` from inside our app container — that's a workaround we added because in our earlier runs Wolf's own `Create virtual audio sink` step produced only the fallback `auto_null` (the named per-session sink never appeared). With the sink force-created, `[PULSE_ROUTER]` does log `Sink appeared name='virtual_sink_<id>'` and moves the app's sink-input onto it — but it still never opens a capture `pulsesrc` on the monitor. So whether or not the sink exists, the capture side isn't instantiated on this NVIDIA setup. Happy to run any specific `pactl`/gst probes or test a patched build.
Author
Owner

@JBailes commented on GitHub (May 21, 2026):

What nvidia GPU are you using? What is your CPU?

<!-- gh-comment-id:4508902267 --> @JBailes commented on GitHub (May 21, 2026): What nvidia GPU are you using? What is your CPU?
Author
Owner

@xavdematos commented on GitHub (May 21, 2026):

GPU: NVIDIA GeForce RTX 3080 (GA102, 12 GB VRAM), driver 595.71.05
CPU: AMD Ryzen AI 9 HX PRO 370 (24 threads)
RAM: 96 GB
OS: Unraid 7.2, kernel 6.12.54

Worth flagging: it's a dual-GPU box; the RTX 3080 (c6:00.0, /dev/dri/renderD128, what Wolf uses) plus the CPU's AMD Radeon 890M iGPU (c7:00.0). Wolf correctly picks the NVIDIA node and the amdgpu/VAAPI probes just warn and fall back, so video/NVENC work fine , noting it only in case the second GPU is relevant to audio-device selection.

<!-- gh-comment-id:4513655384 --> @xavdematos commented on GitHub (May 21, 2026): GPU: NVIDIA GeForce RTX 3080 (GA102, 12 GB VRAM), driver 595.71.05 CPU: AMD Ryzen AI 9 HX PRO 370 (24 threads) RAM: 96 GB OS: Unraid 7.2, kernel 6.12.54 Worth flagging: it's a dual-GPU box; the RTX 3080 (c6:00.0, /dev/dri/renderD128, what Wolf uses) plus the CPU's AMD Radeon 890M iGPU (c7:00.0). Wolf correctly picks the NVIDIA node and the amdgpu/VAAPI probes just warn and fall back, so video/NVENC work fine , noting it only in case the second GPU is relevant to audio-device selection.
Author
Owner

@JBailes commented on GitHub (May 22, 2026):

I have a few ideas. I'll do some fiddling today, I think I may be able to help with this. I need a bit of time to parse through the logs.

<!-- gh-comment-id:4516223980 --> @JBailes commented on GitHub (May 22, 2026): I have a few ideas. I'll do some fiddling today, I think I may be able to help with this. I need a bit of time to parse through the logs.
Author
Owner

@xavdematos commented on GitHub (Jun 27, 2026):

I have a few ideas. I'll do some fiddling today, I think I may be able to help with this. I need a bit of time to parse through the logs.

Any thoughts here?

<!-- gh-comment-id:4823032177 --> @xavdematos commented on GitHub (Jun 27, 2026): > I have a few ideas. I'll do some fiddling today, I think I may be able to help with this. I need a bit of time to parse through the logs. Any thoughts here?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#268
No description provided.