Customizing WirePlumber's configuration for embedded systems
Recently, I had the opportunity to work with a customer that required customization of WirePlumber’s configuration for use in their embedded Linux system. Oftentimes, people are confused about how to customize WirePlumber for their use case. I thought I’d take this opportunity to discuss how it was configured in this particular case and why.
Configuration structure
Let me first give you some context on how WirePlumber’s configuration is structured.
Starting with WirePlumber 0.5, the loading process is governed by the configuration file’s
wireplumber.components
and wireplumber.profiles
sections. The components section defines all
the available modules and scripts and the profiles section defines which of those components are
going to be loaded.
Components
A typical component description looks like this:
{
name = monitors/bluez.lua, type = script/lua
provides = monitor.bluez
requires = [ support.export-core,
pw.client-device,
pw.client-node,
pw.node-factory.adapter ]
wants = [ monitor.bluez.seat-monitoring ]
}
As you can tell, each component description starts with a name
, which specifies which script or
module to load, followed by a type
that indicates what kind of component it is. This one is a Lua
script, designated as script/lua
. In addition, each component provides
a certain feature (in
this case, monitor.bluez
) and may optionally require or want other features in order to load.
This is a dependency mechanism, allowing components to depend on, or be depended on by, other
components during the loading process. Components listed in the requires
field will need to be
loaded successfully before loading this component and ones listed in the wants
field will be
attempted to be loaded, but they are optional if they fail (or if they are explicitly disabled, see
below).
Profiles
Having an idea of what components look like, now let’s look at profiles. Here’s a typical profile description:
main = {
inherits = [ base ]
metadata.sm-settings = required
metadata.sm-objects = required
policy.standard = required
hardware.audio = required
hardware.bluetooth = required
hardware.video-capture = required
}
A profile always has a name, in this case main
, which can be used to select which profile to load
in the WirePlumber command line (with the -p / --profile
switch). If none is specified,
WirePlumber loads the main
profile by default. Inside the profile description, we can list
component features, setting them to either required
or disabled
, to designate if they must be
loaded or if they must not. If a component feature is not listed here, it is assumed to be
optional
, which means it will be loaded only if it’s pulled by another component via the
dependency mechanism. Finally, a profile may have an inherits
field, which is an array listing
other profiles from which it inherits. In this case, base
is another profile that lists common
features that are required by many profiles and the inheritance avoids copy-pasting them all over
the place:
base = {
check.no-media-session = required
support.settings = required
support.log-settings = required
support.session-services = required
}
Upstream defaults
The default wireplumber.conf
configuration file, which is included with WirePlumber, lists all the upstream provided components
together with a set of profiles which can be used as a starting point for any customized
configuration. For instance, apart from the main
profile, the default configuration also includes
the main-systemwide
, main-embedded
and video-only
profiles, defined as:
# Profile for running on a systemwide level
main-systemwide = {
inherits = [ main, mixin.systemwide-session ]
}
# Typical profile for embedded use cases, systemwide without maintaining state
main-embedded = {
inherits = [ main, mixin.systemwide-session, mixin.stateless ]
}
# Profile for video-only use cases (camera & screen sharing)
video-only = {
inherits = [ main ]
hardware.audio = disabled
hardware.bluetooth = disabled
}
The mixin.*
profiles that you see there are also profiles, but they are not meant to be used
standalone; they are only meant to be inherited. In this case, you can see that they are inherited
together with the main
profile. You can see their full description
in the repository.
Something noteworthy to point out here is that when features are listed more than once, each new
listing shadows the previous one and overrides its value. In the above example, you may have noticed
that the video-only
profile inherits main
, which was listing hardware.audio
and
hardware.bluetooth
as required
. However, the video-only
profile redefines them as disabled
.
In this case, the latter definition overrides the previous one in main
, effectively disabling
those components.
Profile overrides
There’s one more thing to note about profile inheritance. While it is possible to inherit other
profiles when creating our own using the inherits
keyword, it is also possible to override
existing profiles by redefining them in a
configuration fragment file.
Doing that effectively uses the built-in file merging mechanism that the configuration loader uses,
which merges arrays and objects together, overriding previous values. For instance, the video-only
profile shown above could also be defined like this in a fragment file (ex. in
wireplumber.conf.d/video-only.conf
):
main = {
hardware.audio = disabled
hardware.bluetooth = disabled
}
This is effectively the same as the video-only
profile, but it removes the requirement that you
load WirePlumber with the -p
switch. So, if you want your system to permanently have this
configuration, you can add this fragment file instead of modifying the systemd units to load
WirePlumber with -p video-only
.
Settings
Apart from components and profiles, there’s also a way to configure variables that are used by
various components to tune their operation. These are called settings and can be set in the
wireplumber.settings
section. The default configuration file lists all the available settings with
their default values and, like with profiles, it is possible to re-define the wireplumber.settings
section in any configuration fragment and override values in the same way that it works for
profiles.
The customer’s configuration
Without any further delay, here’s the configuration fragment that was used in this customer’s
system to customize WirePlumber, installed as
/etc/wireplumber/wireplumber.conf.d/90-custom-wireplumber.conf
wireplumber.profiles = {
# component overrides for the "main" profile
main = {
# disable v4l2 and libcamera sources, we use pipewire only for audio
hardware.video-capture = disabled
# disable MIDI device monitors
monitor.alsa-midi = disabled
monitor.bluez-midi = disabled
# we don't ship pipewire-media-session, this check is redundant
check.no-media-session = disabled
# disable unneeded desktop features
support.portal-permissionstore = disabled
support.reserve-device = disabled
monitor.bluez.seat-monitoring = disabled
# disable all hooks that save/restore state information from ~/.local/state
hooks.device.profile.state = disabled
hooks.device.routes.state = disabled
hooks.default-nodes.state = disabled
hooks.stream.state = disabled
# disable flatpak & snap access rules - not needed
script.client.access-portal = disabled
script.client.access-snap = disabled
}
}
wireplumber.settings = {
# disable bluetooth profile auto-switching and state storage
bluetooth.use-persistent-storage = false
bluetooth.autoswitch-to-headset-profile = false
device.routes.default-sink-volume = 1.0
}
The first thing you may notice is that this file follows the approach of overriding the main
profile instead of defining a new one. This is to avoid changing the systemd units, since this
is the only profile that will ever be used on this system.
Second, you see that the hardware.video-capture
feature, as well as the two MIDI-related device
monitors are disabled. This reduces the available devices to just audio ones, with the remaining
enabled monitors being the ALSA and Bluetooth (BlueZ) ones, aligning with the use cases of this
system. Note that disabling the video capture and MIDI device monitors does not mean that PipeWire
is no more able to carry video or MIDI data. It is perfectly capable of doing so and in fact, the
screensharing functionality, being a video stream between two applications, is still available on
this system.
Moving forward, there are some more features that are being disabled:
-
The
check.no-media-session
feature is a plugin that double-checks your system is not running the old pipewire-media-session at the same time, as that would clash with WirePlumber. That’s definitely not needed on a controlled system where we know this program is not present and there is no way for users to install it. -
The
support.portal-permissionstore
,support.reserve-device
andmonitor.bluez.seat-monitoring
features are all desktop-related and also unnecessary here. The “portal permission store” plugin allows WirePlumber to communicate with the permission store of the XDG Desktop Portal to figure out permissions for Flatpak applications. The “reserve device” plugin allows device monitors to register devices on D-Bus and arbitrate their exclusive access, which is needed if you want to allow other audio daemons (ex. JACK) to take over ALSA devices from PipeWire. Finally, the “seat monitoring” plugin ensures that Bluetooth devices are released when you switch between users, so that they are always grabbed by and available to the active user in a multi-user system. None of that is necessary to our embedded system. -
The
hooks.*.state
features control Lua scripts that contain event hooks to manage state storage in~/.local/state
. This state includes things such as the user’s preferred default devices, preferred profiles and routes to use for devices, volume levels for various applications that were changed manually, etc… This is all unwanted on an embedded system that is meant to always boot into the same clean state, and if there’s any state at all, it needs to be controlled by upper layers (i.e. the customer’s middleware & UI). -
The
script.client.access-*
features contain access rules for use with Flatpak and Snap. Since this system doesn’t use either Flatpak or Snap, these are also useless.
Last but not least, the wireplumber.settings
section lists changes to three of the settings
variables. The first two are related to the Bluetooth headset autoswitch functionality, which
implements automatic switching of Bluetooth headsets between the A2DP and HFP profiles depending
on the use case. This is not really necessary here, but unfortunately, for historical reasons, the
code that implements this is not easy to disable via a component feature. This is something to be
improved in the future. The last setting instructs WirePlumber to configure audio sinks to 100%
volume when they are initialized, instead of the 40% default that is used on desktops.
Remarks
You may ask at this point: is it harmful to leave all those features enabled if they are not being
used? The answer is, some features are harmless if left enabled, but some others may have
side-effects. For instance, the check.no-media-session
and script.client.access-*
features are
pretty much harmless if they are left enabled; they may just add some processing overhead. However,
the hooks.*.state
features actively manage state and have visible side-effects.
Now, the desktop-related features are somewhat special. On one hand, they are harmless when left
enabled and unused, but on the other hand they introduce significant dependencies. The
support.portal-permissionstore
and support.reserve-device
plugins both depend on support.dbus
,
which is the plugin that enables WirePlumber to talk to D-Bus and requires an active user session
bus. This cannot work on systems where PipeWire and WirePlumber are deployed system-wide and there
is no such bus. Similarly, the monitor.bluez.seat-monitoring
depends on support.logind
, which
enables communication with systemd-logind, allowing WirePlumber to track the active user session.
This may also be problematic on system-wide deployments or on systems that don’t use systemd
(although alternatives exist to make this work if needed).
Finally, you may have noticed that this override of the main
profile is not using any inherits
.
The truth is, it would make a lot of sense to inherit some of the mixin.*
profiles. For instance,
disabling the hooks.*.state
features can be achieved by inheriting mixin.stateless
, and
disabling the support features that can be problematic on system-wide deployments, by inheriting
mixin.systemwide-session
. This would avoid repetition and also make the configuration more
future-proof in case additional upstream components are added in these categories. However, the
version of WirePlumber used on this system was older than the version that added the inherits
mechanism, therefore it was not possible to use it. In fact, this project served as inspiration for
implementing the inheritance mechanism in the first place!
Conclusion
WirePlumber is a highly modular daemon whose behavior fully depends on the components that it loads, based on a profile description and the dependencies between those components. Furthermore, the behavior of the loaded components can be tuned via settings. All of those can be described in configuration fragments, which allow easy customization on embedded systems, as shown in this example. You can read more about WirePlumber’s configuration process in the official documentation