#5985

Linux: existing virtual sinks without a PulseAudio owner module (native PipeWire nodes) are duplicated instead of reused

Openherunanopened 7d ago

Is there an existing issue for this?

  • I have searched the existing issues

Is your issue present in the latest beta/pre-release?

The logic is unchanged on master as of 2026-10-23 (src/platform/linux/audio.cpp, server_t::sink_info()); tested with 2026.1008.155609.

Describe the Bug

On Linux, sink_info() is meant to reuse existing sink-sunshine-stereo / sink-sunshine-surround51 / sink-sunshine-surround71 sinks ("Ensure Sunshine won't create a sink that already exists"), but it uses the sink's owner_module as the "exists" flag:

if (!std::strcmp(sink_info->name, stereo)) {
  index.stereo = sink_info->owner_module;
  ++nullcount;
}
...
if (index.stereo == PA_INVALID_INDEX) {
  index.stereo = load_null(stereo, ...);
  ...
  ++nullcount;
}
...
if (nullcount == 3) {
  sink.null = ...;
}

A sink that wasn't created by a PulseAudio module has owner_module == PA_INVALID_INDEX. That is the case for every native PipeWire node, e.g. null sinks defined permanently in ~/.config/pipewire/pipewire.conf.d/ with factory.name = support.null-audio-sink (pactl list sinks shows Owner Module: 4294967295). For such sinks Sunshine:

  1. counts the sink as found (++nullcount), then
  2. still calls load_null() because the index is PA_INVALID_INDEX, creating a second sink with the same name, and counts it again;
  3. ends up with nullcount == 6, so sink.null stays empty and the virtual sinks are never used. Audio is captured from the host sink's monitor instead, even with "play audio on host" off.

Why someone would want permanent sinks: today every session loads three module-null-sink modules, moves the user's streams onto one by changing the default sink, moves them back and unloads the modules a moment later. On a PipeWire host that churn coincided with a system-wide audio stall that needed a restart of pipewire/wireplumber (reported to PipeWire separately). Pre-defining the sinks so that Sunshine never loads or unloads anything avoids that, but only works if Sunshine reuses them.

Current workaround: give the native nodes a fake pulse.module.id without pipewire-pulse's module flag (e.g. pulse.module.id = 268435455). Sunshine then sees a valid owner index and reuses the sinks, and its unload_null() calls at the end fail with Access denied (logged as an error three times per session).

Expected Behavior

Sinks that already exist by name are reused, and Sunshine only unloads modules it loaded itself. For example, track "found" and "loaded by us" separately:

// in the sink list callback
if (!std::strcmp(sink_info->name, stereo)) {
  found.stereo = true;   // don't touch index.stereo: we didn't load it
  ++nullcount;
}
...
if (!found.stereo) {
  index.stereo = load_null(stereo, ...);
  if (index.stereo != PA_INVALID_INDEX) ++nullcount;
}

so that ~server_t() only unloads indexes from load_null(). That also stops Sunshine from unloading sinks that a user or another program created with pactl load-module module-null-sink sink_name=sink-sunshine-stereo ..., which the current code does (it unloads whatever owner module it recorded).

Additional Context

Steps to reproduce:

  1. Add ~/.config/pipewire/pipewire.conf.d/sunshine-sinks.conf with three support.null-audio-sink adapter objects named sink-sunshine-stereo (FL FR), sink-sunshine-surround51 (FL FR FC LFE RL RR) and sink-sunshine-surround71 (FL FR FC LFE RL RR SL SR), and restart PipeWire.
  2. Start a stream with "play audio on host" off.
  3. pactl list modules short | grep null-sink shows three new module-null-sink modules, pactl list sinks short shows two sinks of each name, and the log never shows Setting default sink to: [sink-sunshine-stereo].

Host Operating System

Linux

Operating System Version

CachyOS (rolling), kernel 7.2, PipeWire 1.6.9 with pipewire-pulse, WirePlumber 0.5.18

Architecture

amd64/x86_64

Package

CachyOS repository package (sunshine 2026.1008.155609-1)

GPU Type

AMD (not relevant: audio only)

Activity

0

No activity yet.