Skip to content

feat(pipewire,pulseaudio): name the application and its streams - #1409

Open
crmne wants to merge 1 commit into
RustAudio:masterfrom
crmne:stream-metadata
Open

crmne wants to merge 1 commit into
RustAudio:masterfrom
crmne:stream-metadata

Conversation

@crmne

@crmne crmne commented Oct 20, 2026

Copy link
Copy Markdown

Problem

Volume mixers (pavucontrol, GNOME and KDE sound settings) list cpal streams with cpal's own names:

  • PipeWire: the stream's media.name is cpal-playback-<pid> / cpal-capture-<pid>, with no application.name or icon on the node and no media.role.
  • PulseAudio: the client is cpal-pulseaudio-<pid>, and the stream has no name, icon or role.

Besides looking wrong, the pid defeats stream-restore: pipewire-pulse keys a PipeWire-host stream's remembered volume by sink-input-by-media-name:cpal-playback-<pid>, and a PulseAudio-host stream's by sink-input-by-application-name:cpal-pulseaudio-<pid>, so no volume is ever restored across runs. This is the request in #1186 (closed) and the "naming and metadata" row of the proposal in #1334.

Change

Three host-agnostic value types in the crate root, which hosts treat as hints:

pub struct ApplicationInfo { pub name: String, pub icon_name: Option<String> }
pub struct StreamMetadata { pub name: Option<String>, pub role: Option<StreamRole> }
#[non_exhaustive]
pub enum StreamRole { Music, Movie, Game, Communication, Notification, Production, Accessibility }

The PipeWire and PulseAudio hosts take them through host-specific configuration, following set_connect_automatically:

  • Host::with_application(ApplicationInfo) -> Result<Host, Error>. It is a constructor because PulseAudio names the application by its connection, which Host::new opens.
  • Host::set_stream_metadata(StreamMetadata) -> Result<(), Error> applies to streams built from the host's devices afterwards.

Both return ErrorKind::InvalidInput for a value containing a NUL byte. PulseAudioHost is now exported from cpal::platform, as PipeWireHost already is, so apps can construct it.

PipeWire PulseAudio
application name application.name on the stream and on its core/client client name (SetClientName) and the stream's application.name
application icon application.icon-name on the stream and on its client the stream's application.icon_name (pulseaudio-rs's Client::from_env takes only a name)
stream name media.name and the pw_stream name (defaults to cpal's cpal-playback-<pid> as before) media.name in the stream proplist (unset by default, as before)
role media.role: Music, Movie, Game, Communication, Notification, Production, Accessibility media.role: music, video, game, phone, event, production, a11y

With nothing set, behaviour is unchanged.

Relation to #1334

#1329 was parked to wait for the extension API in #1334, and I know this PR runs into the same thing. I've tried to keep the parts that should outlast that API apart from the parts that won't:

  • The value types and the per-backend mapping are what a name_hint-style stream-builder knob and a host-builder knob would both pass down. They hold typed data, not raw key/value pairs, so the role works on more than one backend (WASAPI's AUDIO_STREAM_CATEGORY and AAudio's usage could map from the same enum later).
  • The two host methods are the interim way in, in the shape cpal already uses for host-specific options. Once the builders land they can become thin wrappers, or be deprecated.

If you'd rather wait for #1334, the mapping and tests can move onto it unchanged. I'm also glad to rename or reshape the types to fit that design.

Not in this PR

  • Renaming an open stream. Both servers can do it (pw_stream_update_properties; PulseAudio's SET_PLAYBACK_STREAM_NAME / UPDATE_*_STREAM_PROPLIST, which pulseaudio-rs exposes as PlaybackStream::set_name), and a music player wants it for the track title. It needs a StreamTrait method, which is a core API decision, so I left it out.
  • JACK's client name (can't set the name of jack client #1235): ApplicationInfo::name would fit it, but it belongs with the host builder.

Breaking changes

  • pulseaudio::Device::Sink and Device::Source (reachable through DeviceInner) have a new identity field, so exhaustive patterns on them outside cpal break. This is noted under Changed in the changelog.

Testing

Unit tests cover the property mapping on both hosts and the NUL check (host::pipewire::device::test::identity_properties, host::pulseaudio::tests::identity_props, stream_metadata::tests::rejects_nul_bytes).

I ran a small program that opens one silent output or input on each host, with and without names, against PipeWire 1.6.9 and pipewire-pulse ("PulseAudio (on PipeWire 1.6.9)") on Arch Linux, and read the results back with pw-dump and pactl list sink-inputs / source-outputs / clients.

PipeWire host, output, before:

Node   media.name = "cpal-playback-2309653"   node.name = "cpal-probe"
Client application.name = "cpal-probe"
pactl: module-stream-restore.id = "sink-input-by-media-name:cpal-playback-2309653"

PipeWire host, output, with ApplicationInfo { name: "Spotifast", icon_name: Some("spotifast") } and StreamMetadata { name: Some("Some Song"), role: Some(Music) }:

Node   application.name = "Spotifast"  application.icon-name = "spotifast"
       media.name = "Some Song"  media.role = "Music"  node.name = "Spotifast"
Client application.name = "Spotifast"  application.icon-name = "spotifast"
pactl: media.role = "music"  module-stream-restore.id = "sink-input-by-media-role:music"

PipeWire host, input, with ZapFast / Voice call / Communication:

Node   application.name = "ZapFast"  media.name = "Voice call"  media.role = "Communication"
pactl: media.role = "phone"  module-stream-restore.id = "source-output-by-media-role:phone"

PulseAudio host, output, before:

Client application.name = "cpal-pulseaudio-2309924"
Sink input: application.name = "cpal-pulseaudio-2309924", no media.name, no icon, no role

PulseAudio host, output and input, named as above:

Client application.name = "Spotifast"
Sink input: application.name = "Spotifast"  application.icon_name = "spotifast"
            media.name = "Some Song"  media.role = "music"
Source output: application.name = "ZapFast"  application.icon_name = "zapfast"
               media.name = "Voice call"  media.role = "phone"

Checks run locally: cargo fmt --all -- --check; cargo clippy --all -- -D warnings with default features and with --all-features; cargo clippy --target aarch64-apple-darwin --all-features -- -D warnings; cargo test --workspace with default and all features; cargo check --examples --all-features; cargo +1.85.0 check --features pipewire; DOCS_RS=1 RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc --all-features --no-deps.

Unverified

  • A real PulseAudio daemon. I tested the PulseAudio host only through pipewire-pulse. The client name and stream proplist go through standard protocol commands, but I haven't run them against pulseaudio itself.
  • FreeBSD, NetBSD and DragonFly. These are covered by the same cfg as Linux, but I haven't built or run them.
  • Windows. I could only cross-check it without ASIO. cargo clippy --target x86_64-pc-windows-msvc stops on an unused frames_to_duration, and plain master does the same, so this change doesn't cause it. CI's ASIO build has not been run.
  • Visual check. I haven't looked at the result in a GUI mixer such as pavucontrol. All the evidence above comes from server properties.

Volume mixers list cpal streams as `cpal-playback-<pid>` and PulseAudio
clients as `cpal-pulseaudio-<pid>`, with no icon or role. The pid in the
name also defeats stream-restore, which keys a stream's volume by its
media name.

Add `ApplicationInfo`, `StreamMetadata` and `StreamRole`, host-agnostic
hints, and take them on the PipeWire and PulseAudio hosts:

- `Host::with_application` names the application: PipeWire's
  `application.name` and `application.icon-name` on the stream and its
  client, PulseAudio's client name and the stream's `application.name`
  and `application.icon_name`.
- `Host::set_stream_metadata` sets `media.name` and `media.role` on the
  streams built afterwards, with the role in each server's vocabulary
  (`Communication` on PipeWire, `phone` on PulseAudio).

Export `PulseAudioHost` from `cpal::platform`, as `PipeWireHost` is.
crmne added a commit to crmne/fastframe that referenced this pull request Oct 20, 2026
No cpal release can name streams, so fastframe-audio now gets cpal from
crmne/cpal's stream-metadata-0.18 branch at fcd8b6e: cpal 0.18.2 with
RustAudio/cpal#1409, the one fork dependency the maintainer approved. It
is carried until a cpal release includes #1409 or its successor, then goes
back to crates.io. AGENTS.md records the exception, and MIGRATION.md and
the crate README tell apps that also get cpal 0.18 from crates.io (through
kira, for example) to patch it to the same rev, so they build one cpal.

On PipeWire and PulseAudio every host the process makes now names the
app, and the host a stream opens from names the stream: its media name
(the app's when the options give none) and its role. The connection kept
to PipeWire's PulseAudio server is named too, and the first choice of the
sound server makes the named host directly, so no unnamed
`cpal-pulseaudio-<pid>` client is left in the mixer. Names lose any NUL
byte, which the servers cannot take.
crmne added a commit to crmne/chatwithwork-desktop that referenced this pull request Oct 20, 2026
On PipeWire and PulseAudio, dictation now shows as "Dictation" under
"Chat with Work" with the cww-app icon the Linux packages install,
instead of cpal-pulseaudio-<pid>. It has no stream role: it isn't call
audio. fastframe-audio now gets cpal from crmne/cpal (RustAudio/cpal#1409),
the only cpal in the tree, so cargo deny allows that source.
crmne added a commit to crmne/spotifast that referenced this pull request Oct 20, 2026
On PipeWire and PulseAudio the stream showed as cpal-playback-<pid>,
with the client as cpal-pulseaudio-<pid>. fastframe-audio 0.14 names
the app and its streams: Spotifast now calls set_app_info at startup,
before any output is listed or opened, with its name and the desktop
entry's icon, and its output's media name is "Spotify playback", fixed
while the stream is open. No role is set, so the mixer remembers the
volume for Spotifast rather than one shared by every music player.

Every fastframe crate moves to v0.14.0 together. fastframe-audio gets
cpal from crmne/cpal (cpal 0.18.2 with RustAudio/cpal#1409); rodio
0.21 gets cpal 0.18.2 from crates.io, so [patch.crates-io] points it at
the same fork and the build has one cpal. The Nix vendor hash follows
the lockfile.

Checked on a private PipeWire 1.6 with WirePlumber and pipewire-pulse:
pw-dump shows the client and node with application.name Spotifast,
application.icon-name spotifast and media.name Spotify playback, and
pactl list sink-inputs shows the same.
@Marco-Farruggio

Copy link
Copy Markdown
Contributor

cargo clippy --target x86_64-pc-windows-msvc stops on an unused frames_to_duration

This has been on my todo list for a few days, please expect a fix relatively soon

@Marco-Farruggio Marco-Farruggio mentioned this pull request Oct 23, 2026
7 tasks done

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants