Compare commits

...

533 commits

Author SHA1 Message Date
Julian Bouzas
8b42a5a3ae wpctl: Apply the same volume to all nodes when setting by PID
Fixes #944
2026-05-05 07:43:21 -04:00
bhack
5a2f52dab4 spa-pod: mark borrowed string out params transfer none 2026-05-04 22:05:35 +02:00
Julian Bouzas
7fa44ef8d0 find-preferred-profile: Add new 'bluetooth.profile-preference' setting
This setting will use the best quality or latency profiles  for BT devices if
available. HSP/HFP profiles will always be ignored. The setting is a string that
only accepts 'quality' and 'latency' strings. Any other value will be treated
the same way as the 'quality' value.
2026-04-30 07:35:38 -04:00
qaqland
c579d1d839 wpctl: add bash completion for list subcommand
Refs: 85a7201409
Signed-off-by: qaqland <anguoli@uniontech.com>
2026-04-29 16:13:25 +03:00
Torkel Niklasson
26f5fc11a6 permission-manager: Add core_permissions support
The core object (ID 0) is implicit in the PipeWire connection and never
appears in the permission manager's ObjectManager. Add a
core_permissions field to set explicit permissions on it independently
of default_permissions.
2026-04-29 08:20:55 +02:00
Torkel Niklasson
1f0c590f49 docs: add WpPermissionManager API page and document permission managers in access config 2026-04-29 08:20:55 +02:00
zhouyong
1f8475b15f find-portal-access:Add a cache for camera permission checks to avoid frequent calls 2026-04-23 18:06:05 +08:00
zhouyong
bd4beadb43 portal-permissionstore: Add 3s timeout to D-Bus calls and fix Set create parameter 2026-04-22 19:39:05 +08:00
Sergey Veselkov
85a7201409 wpctl: add list subcommand to show objects in a more script-friendly format 2026-04-14 19:14:40 +03:00
Sergey Veselkov
5c0712322f meson: fix tools build without daemon 2026-04-14 18:38:55 +03:00
Марко М. Костић (Marko M. Kostić)
2fa1414fbe
po: Format the updated Serbian and Serbian Latin translations 2026-04-11 14:06:45 +02:00
Марко М. Костић (Marko M. Kostić)
45a2786c1b
po: Update Serbian and add Serbian Latin translations 2026-04-11 14:03:06 +02:00
George Kiagiadakis
409446046c Revert "object, registry: Increase prio of idle sources"
This is suspected to be the reason why the CI pipelines fail on some
of the linking tests. Reverting the commit for now, until the issue
is better understood.

See #934

This reverts commit 529aaa66cb.
2026-04-09 15:29:43 +03:00
Julian Bouzas
767a83a5f0 state-profile: Fix nil value when logging
Use warning instead of critical as there is no critical level API in Lua.
2026-04-09 11:00:07 +03:00
Julian Bouzas
f4f1a33446 permission-manager: Fix null pointer dereference
This fixes issue reported by coverity scan.
2026-04-09 09:26:51 +03:00
Julian Bouzas
e1874f8b31 wpctl: Connect to the manager socket if possible
This gives the tool unrestricted access.
2026-04-07 09:38:58 -04:00
Julian Bouzas
478c9402fc module: Call parent's destructor before finalizing
This fixes memleaks when unreferencing the module.
2026-04-01 08:36:39 -04:00
Julian Bouzas
210467c5ce scripts/client: Refactor scripts to use the new PermissionManager API
The refactoring uses a new 'select-access' event to select the access for each
client with a fallback mechanism. The fallback priority is: configuration,
flatpak, snap, portal, and default.

The access JSON configuration has also been improved so that users can create
their custom permission managers and attach them to any client. See the access
configuration example for more information describing how to do this.
2026-03-31 12:15:14 +03:00
Julian Bouzas
dcd59bc31d client: Add _attach_permission_manager () API
This attaches a permission manager to a client so that it can handle permissions
automatically when the interested objects have changed.
2026-03-31 12:15:14 +03:00
Julian Bouzas
c03f4fd4d7 m-lua-scripting: Add Lua API for WpPermissionManager
This allows using the new permission manager API in Lua scripts.
2026-03-31 12:15:14 +03:00
Julian Bouzas
484e1f0fb7 lib: Add new WpPermissionManager API
This allows setting object specific permissions on any client easily.
2026-03-31 12:15:14 +03:00
Julian Bouzas
78bd42cad8 bluez: Don't set bluez5.autoswitch-routes on BT devices
This seems to cause some issues with BT profile autoswitch.

See #932
2026-03-30 07:33:58 -04:00
Jonas Holmberg
529aaa66cb object, registry: Increase prio of idle sources
Raise the priority of all idle sources that need to be dispatched in
order to make the linking/linkable-added-immediate hook run.
2026-03-25 16:30:46 +01:00
Jonas Holmberg
374c48b339 event-dispatcher: Dispatch one event at a time
Return after dispatching one event so that other GSources also can be
dispatched in between events when there are many events in queue.
2026-03-25 16:30:46 +01:00
George Kiagiadakis
07e730b279 0.5.14 2026-03-25 12:49:03 +02:00
Barnabás Pőcze
c2b96ebb39 m-lua-scripting: impl_module_new(): fix property list memory leak
The `properties` argument of `wp_impl_module_load()` is marked
"transfer none", thus the caller's reference remains valid and
must be disposed of.

Fixes: ef29018c55 ("m-lua-scripting: Add WpImplModule bindings")
2026-03-20 20:30:44 +01:00
Torkel Niklasson
1cfeab9a86 linking: Make rescan optional on linkable changes
Split rescan-on-linkable triggering into a separate script file
(linking/rescan-on-linkable.lua) that can be disabled via
wireplumber.profiles by setting hooks.linking.rescan-on-linkable =
disabled.
2026-03-19 20:29:32 +02:00
Julian Bouzas
b453464320 autoswitch-bluetooth-profile: Switch/restore the profile using 'autoswitch-*' event hooks
Since 'autoswitch-*' events have lower priority than 'select-*' events,
this guarantees that the autoswitch hooks will always run after the
'device/apply-profile' hook, avoiding possible race conditions.
2026-03-19 20:25:38 +02:00
Julian Bouzas
7023ad0c2c m-standard-event-source: Add 'autoswitch-*' local event priority
These local events have lower priority than the 'create-*' and 'select-*' ones,
and are meant to be used when wireplumber wants to automatically switch profiles
or other things.
2026-03-19 20:25:38 +02:00
Julian Bouzas
20238072e2 autoswitch-bluetooth-profile: Ensure the saved profile is headset/non-headset before switching/restoring
Otherwise find the highest priority one.
2026-03-19 20:25:38 +02:00
Julian Bouzas
f38f1a2af8 autoswitch-bluetooth-profile: Rename device-profile-changed hook name to be more consistent
This makes all the script hooks more consistent.
2026-03-19 20:25:38 +02:00
Julian Bouzas
9fb963d4e5 autoswitch-bluetooth-profile: Don't evaluate if node state changes from 'idle' to 'suspended'
This is not needed and can improve performance a little bit.
2026-03-19 20:25:38 +02:00
Julian Bouzas
8dcf3ce6ed autoswitch-bluetooth-profile: Check profile names to see if a profile is headset
We cannot only rely on the input routes to check whether a profile is a headset
profile or not because some BT devices can have A2DP source nodes, causing the
autoswitch logic to not work properly.

This patch fixes the problem by also checking if the profile name matches the
'headset-head-unit*' or 'bap-duplex' patterns. If the profile name does not
match those patterns, the profile is not considered headset profile.

See #926
2026-03-19 20:25:38 +02:00
Julian Bouzas
2954e0d5e8 autoswitch-bluetooth-profile: Make sure current profile is valid before switching 2026-03-19 20:25:38 +02:00
Pauli Virtanen
76b26deabf monitors/bluez: Don't set api.bluez5.internal=true on HFP HF streams
Setting api.bluez5.internal=true changes media.class to
Audio/(Stream|Sink)/Internal which makes HFP HF output get wrong media
class.

Don't set this for headset-audio-gateway. Fixes HFP HF stream media
class.
2026-03-18 22:14:14 +02:00
Arun Raghavan
eef9baee61 apply-routes.lua: Add a mechanism for per-device default volumes
This allows us to have per-device configuration for device volumes, via
a `device.routes.default-volume` property. This allows a non-global
override for situations where we want to expose a different out-of-box
volume (say for an internal speaker where we know a better comfortable
default setting, or HDMI where we might want to avoid attenuation by
default).
2026-03-18 18:05:28 +02:00
NorwayFun
46638971c3 po: Update Georgian translation 2026-03-18 17:59:58 +02:00
Barnabás Pőcze
215c9efd02 monitors/libcamera: load node locally
At the moment the libcamera monitor and "Device" objects are loaded in the
wireplumber process, but the "Node" objects are loaded in the pipewire daemon.
Due to that, both processes have their own separate `libcamera::CameraManager`
objects. These operate independently.

As a consequence of the above, there is an inherent race condition: when a
new camera appears and the wireplumber process detects it and instructs the
pipewire process to create a "Node" for it, at that point, the camera might
not exist in the `CameraManager` of the "pipewire" process.

This can happen during the initial enumeration as well as hotplug. So load
the nodes locally in the wireplumber process so that all libcamera objects
use the same `CameraManager`, thus eliminating the race condition.
2026-03-18 17:35:33 +02:00
Robert Mader
f535befda4 systemd: allow mincore system call for Mesa/EGL
This is required in order to allow plugins to use GL as mincore
is used in Mesas `_eglPointerIsDereferenceable()`.

One example for a client wanting to do so is the in-development
libcamera GPUISP, see https://patchwork.libcamera.org/cover/24183/

(cherry picked from commit pipewire@4796b3fb9524c20ac0f5006143b6a13ee50c01ec)

See pipewire/pipewire!2530
2026-03-18 17:35:33 +02:00
Frédéric Danis
1762d91e75 wp-uninstalled: Allow to pass WIREPLUMBER_CONFIG_DIR 2026-03-18 17:29:42 +02:00
Baurzhan Muftakhidinov
355bb0fb8f Update Kazakh translation 2026-03-02 12:28:25 +02:00
Anders Jonsson
b262ac43be Update Swedish translation 2026-02-25 09:08:25 +02:00
twlvnn
38c07393e5 Updated Bulgarian translation 2026-02-21 13:07:17 +01:00
Julian Bouzas
83d08dfa43 bluez: Remove sink loopback node
Desktop environments like KDE and GNOME seem to have issues with sink loopback
nodes. Let's remove them for now.
2026-02-16 21:54:37 +02:00
Julian Bouzas
48ed27d11b state-stream: fix Lua 5.4 compatibility
The 'elseif' and 'else if' keywords are treated differently in Lua 5.4
2026-02-13 10:17:02 -05:00
George Kiagiadakis
de0bca5902 state-stream: fix crash in case the Format has a Choice for the number of channels
Fixes: #903
2026-02-06 11:28:34 +02:00
Achill Gilgenast
27337ed268
systemd: Allow installation of systemd services without libsystemd
Allows installation of systemd services without libsystemd installed.
Useful for Alpine Linux where systemd services are allowed to be subpackaged
(e.g. for postmarketOS) but hasn't systemd in it's repos.

It has no change in existing behavior, but installs services if the unit
directories are explicitly set.
2026-02-05 17:42:14 +01:00
Julian Bouzas
72b680fc4c bluez: Use 'target.object' instead of smart filters for BT loopback nodes
This allows treating BT loopback nodes as regular nodes.

See #898
2026-02-03 12:49:57 -05:00
Barnabás Pőcze
11af177902 event-hook: fix interest hook event type memory leak
In `wp_interest_event_hook_get_matching_event_types()`, the variable
`res` owns the allocated `GPtrArray`, but there is an early return
in the function. Hitting that will leak the allocation, so use
`g_autoptr` to avoid that.

Fixes: b80a0975c7 ("event-dispatcher: Register hooks for defined events in a hash table")
2026-01-29 12:47:27 +01:00
Barnabás Pőcze
2b78d9c20d meson: update lua wrap to 5.5.0
Update the lua wrap file to the latest lua version, which is 5.5.0.
2026-01-29 11:06:00 +01:00
Julian Bouzas
4cebb63d76 monitors/bluez: Always create loopbacks if Device support A2DP and HSP/HFP profiles
This simplifies a lot the logic as we don't need to destroy and re-create the
internal BT nodes right away if the loopback nodes were create after them.

We also now listen for changes in the BT profile autoswitch setting. If the
setting is disabled, the source loopback is destroyed. If it is enabled, the
source loopack is created. This makes the setting to take effect immediately,
without needing to disconnect and re-connect the BT device for the setting to
take effect.
2026-01-27 09:28:34 +02:00
Pauli Virtanen
d81b170bbf monitors/bluez: fix BAP device set channel prop
As properties are no longer Lua tables, we'll need to make a table copy
for the Json constructor.

Fixes assigning Json.Array to a non-table, and ending up with
"audio.position":"0x7b771267d690" instead of ["FL","FR"] in json output,
which caused BAP device set node to always have mono channels.
2026-01-26 19:56:28 +02:00
George Kiagiadakis
024f88322c wpctl: Allow virtual nodes in set-default command
Use prefix matching instead of exact matching for media.class when
validating nodes in the set-default command. This allows virtual
nodes (e.g. Audio/Source/Virtual) to be set as default devices,
while still excluding internal nodes.

Fixes #896

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:39:17 +02:00
Julian Bouzas
80842cbb96 default-nodes: Never consider Audio/Sink nodes as best for default audio.source node type
Audio/Sink nodes should only be used as default audio source node type if the
user has explicitly selected it. If the user has not explicitly selected it,
we should always ignore it and instead select the highest priority Audio/Source
node.

Fixes #886
2026-01-21 10:49:13 -05:00
Barnabás Pőcze
a5a079ec1d meson: accept lua 5.5 as well
Lua 5.5 was released on 2025-12-22[0], and wireplumber appears
to work fine with it.

[0]: https://lua.org/versions.html#5.5
2026-01-12 14:20:16 +01:00
Barnabás Pőcze
9040ec1e51 meson: simplify lua dependency lookup
Use an array to store all the possible dependency name suffixes and
look them up in order instead of manually doing each dependency lookup.
2026-01-12 14:20:16 +01:00
Julian Bouzas
f088a6f63d autoswitch-bluetooth-profile: Fix attempt to index a number value error
The getLinkedBluetoothLoopbackSourceNodeForStream() function expects stream to be
and object and not a stream ID.
2026-01-08 13:47:41 -05:00
Pauli Virtanen
b60b2f4ece monitors/bluez: request device ports take loopback nodes into account
Take the loopback nodes into account also in device Routes.

Some Pulseaudio applications (eg GNOME) determine what to do based on
device ports, so make sure they are consistent with what nodes will be
emitted.
2026-01-05 00:16:01 +02:00
George Kiagiadakis
84429b4794 0.5.13 2025-12-23 20:48:36 +02:00
Zander Brown
58b48c0a8a m-mpris: Check variant type directly 2025-12-23 20:05:09 +02:00
Zander Brown
af7a951bd9 m-mpris: We must chain up on finalize 2025-12-23 20:05:09 +02:00
Zander Brown
ded213093d m-mpris: Only initialise the builder once 2025-12-23 20:05:09 +02:00
Zander Brown
3a6f2c1e90 m-mpris: ‘Item’s are allocated with GLib
As are the string copies they point to, don't leak them and free them
the right way.
2025-12-23 20:05:09 +02:00
Zander Brown
1846d75717 m-mpris: ‘items’ is a GHashTable, not a GObject
That did fun things to my session that did.
2025-12-23 20:00:18 +02:00
George Kiagiadakis
444bfc04d8 Revert "state-routes.lua: Add new 'bluetooth.keep-volume-on-profile-changed' setting"
This reverts commit 00c272670c.

https://gitlab.freedesktop.org/pipewire/wireplumber/-/merge_requests/739#note_3163620
2025-12-23 19:57:12 +02:00
Julian Bouzas
3887e1ca82 monitor/bluez.lua: Don't set priority.driver in loopback nodes
Loopback nodes should never be the driver as it can cause audio issues.
2025-12-23 19:54:07 +02:00
Julian Bouzas
00c272670c state-routes.lua: Add new 'bluetooth.keep-volume-on-profile-changed' setting
If enabled, this setting will use the same volume levels as the previous
profile. This is useful on some bluetooth devices if the bluetooth profile
audioswitch is enabled.
2025-12-23 19:54:07 +02:00
Julian Bouzas
da831fdc65 monitors/bluez.lua: Create sink loopback for SCO-A2DP sink nodes
If the BT profile autoswitch setting is enabled, we also want to create a sink
loopback for SCO-A2DP sink nodes. Since BT nodes are removed and created again
when the profile changes, this avoids confusing some apps making them think
that the BT profile has not changed at all, because the loopback nodes are
always present, even when switching profiles.
2025-12-23 19:54:07 +02:00
Julian Bouzas
6a9e977d26 autoswitch-bluetooth-profile.lua: Refactor and fix issues with saved profiles
This patch improves the BT profile autoswitch logic so that it is simpler and
more robust. Instead of just relying on capture clients that are linked to (or
unlinked from) the BT loopback source node to evaluate whether we have to switch
to a headset profile or not, we now also evaluate the autoswitch every time the
BT loopback source node state changes. This avoids problems with some capture
clients that pause the input stream without closing them.

Apart from this, the patch also fixes some issues with saved profiles if the
user manually switched to a headset profile when no capture streams are present.
The autoswitch logic should restore back the non-headset profile (A2DP) every
time a capture client is disconnected or the BT loopback source node stops
running.

Finally, the 'bluetooth.autoswitch-to-headset-profile' setting will now register
or remove the necessary hooks depending on whether the setting is enabled or
disabled, improving WirePlumber performance if autoswitching is disabled.
2025-12-23 19:54:07 +02:00
filmsi
80478e7548 Update Slovenian translation (sl.po) 2025-12-16 16:03:57 +02:00
Barnabás Pőcze
3fb5b775ee m-modem-manager: Unref WpCore
When getting the "core" property from a `WpObject`, a strong reference is
returned to the `WpCore` object. This has to be unref-d when not needed
anymore. `wp_modem_manager_enable()` fails to do so, so fix it.

Fixes: 2794764d5a ("m-modem-manager: add module for tracking status of voice calls")
2025-12-16 15:59:07 +02:00
lumingzh
a5538f4167 update Chinese translation 2025-12-16 10:14:56 +08:00
Julian Bouzas
bec20fc054 create-item: Only configure audio device sink nodes in MONO
This allows multi channel mixing in the graph, even if MONO is enabled.
2025-12-08 09:43:56 -05:00
George Kiagiadakis
beded0214d meson: define SPA_AUDIO_MAX_CHANNELS only on newer spa headers
Older spa headers define this without an #ifndef check,
which could lead to compilation issues.
2025-12-02 12:59:58 +02:00
George Kiagiadakis
2286152c07 ci: adapt pipewire build options based on the pw version we are building 2025-12-02 12:32:01 +02:00
George Kiagiadakis
94fe1cbfbd ci: add builds with older versions of libpipewire 2025-12-02 11:59:45 +02:00
Torkel Niklasson
6ebf81453c rescan: Optimize linking for simple stream nodes
Add immediate linking for common cases to reduce latency when new
audio/video streams are added. The full rescan mechanism is preserved
and still triggers for all session-item-added events via the
rescan-trigger hook.
2025-11-28 11:05:34 +00:00
Wim Taymans
ceed5dca7c meson: bump max channels to 128
Recompiling with the SPA_AUDIO_MAX_CHANNELS=128u defined, will create
larger audio_info structures that can hold more channels. Because
PipeWire is now using 128 channels, do the same in wireplumber so that
the generated PortConfig param can hold all channel positions.

See pipewire#4995
2025-11-26 10:35:00 +01:00
Evangelos Ribeiro Tzaras
84e4752f1a m-modem-manager: Prefer automatic cleanup
Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
9e390f1121 m-modem-manager: Avoid memory allocations unpacking string types
Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
133b82e61a m-modem-manager: Don't leak path
Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
9045d2439a m-modem-manager: Don't leak error
And include the error message while we're at it.

Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
278541f637 m-modem-manager: Set GDBusConnection before trying to use it
Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
27b6027649 m-modem-manager: Use correct error clearing function
We segfault otherwise on error:

  Thread 1 "wireplumber" received signal SIGSEGV, Segmentation fault.
  g_type_check_instance_is_fundamentally_a (type_instance=type_instance@entry=0x7fffec008120,
      fundamental_type=fundamental_type@entry=0x50 [GObject]) at ../../../gobject/gtype.c:3917
  warning: 3917	../../../gobject/gtype.c: No such file or directory
  (gdb) bt
  #0  g_type_check_instance_is_fundamentally_a
      (type_instance=type_instance@entry=0x7fffec008120, fundamental_type=fundamental_type@entry=0x50 [GObject])
      at ../../../gobject/gtype.c:3917
  #1  0x00007ffff7eafe3d in g_object_unref (_object=0x7fffec008120) at ../../../gobject/gobject.c:4743
  #2  0x00007ffff4784ec1 in list_calls_done (obj=<optimized out>, res=<optimized out>, data=0x5555556ab1b0)
      at ../modules/module-modem-manager.c:209

Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Guido Günther
6398bf1bce docs: Switch to build directory for run invocation
There's no `Makefile` in the top level build directory so
switch to the build dir.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-11-24 23:23:51 +01:00
Julian Bouzas
f196d10e87 linking/rescan.lua: Clean a bit the code using the new properties API 2025-11-24 19:42:07 +02:00
Julian Bouzas
5071a85997 scripts: Fix compatibility issues with new Lua Properties API
We need to explicitly use Properties() in those cases as {} construct tables.
2025-11-24 19:42:07 +02:00
Julian Bouzas
15d98f59e5 state-routes: use get_count() instead of next()
We cannot use next() as the properties are not a Lua table anymore.
2025-11-24 19:42:07 +02:00
Julian Bouzas
be6f2b2926 m-lua-scripting: Handle both Properties and Lua tables in all Lua APIs 2025-11-24 19:42:07 +02:00
Julian Bouzas
01eb206460 m-lua-scripting: Add get_property() API for pipewire objects
This can be faster if we only want to get one property.
2025-11-24 19:42:07 +02:00
Julian Bouzas
5a4ecceee6 m-lua-scripting: Add get_property() API for session items
This can be faster if we only want to get one property.
2025-11-24 19:42:07 +02:00
Julian Bouzas
a35e40c1d2 m-lua-scripting: Add WpProperties API
Similar to Pod and Json, this API allows handling WpProperties references.
2025-11-24 19:42:07 +02:00
Julian Bouzas
2712cbb5a9 pipewire-object-mixin: Copy the props instead of wrapping them
This allows users to have full control of the properties when they get them.
2025-11-24 19:42:07 +02:00
Julian Bouzas
c68eb59017 device: Copy the props instead of wrapping them before emitting create-device signal
This allows the signal handlers to have full control of the properties.
2025-11-24 19:42:07 +02:00
Julian Bouzas
c0e047c241 apply-default-node: Make sure the metadata is valid
This fixes some warnings in the Lua tests scripts from tests/scripts/scripts/*
2025-11-24 19:42:07 +02:00
Julian Bouzas
238fd3c067 event-dispatcher: Sort hooks when registering them
This avoids sorting them constantly when collecting them. If a hook has a
circular dependency, a warning will be logged and the hook won't be registered.

See #824
2025-11-24 08:01:06 -05:00
Julian Bouzas
b80a0975c7 event-dispatcher: Register hooks for defined events in a hash table
Since all the current hooks are defined specifically for a particular event
type, we can register the hooks in a hash table using the event type as key
for faster event hook collection.

Also, hooks that are not specific to a particular event type, like constraints
such as 'event.type=*', will be registered in both the undefined hook list,
and also in all the hash table defined hook lists so they are always evaluated.

Even though 'wp_event_dispatcher_new_hooks_iterator()' can still be used, it is
now marked as deprecated because it is slower. The event hook collection uses
'wp_event_dispatcher_new_hooks_for_event_type_iterator()' now because it is
much faster.

Previously, the more hooks we were registering, the slower WirePlumber would
process events as all hooks needed to be evaluated for all events constantly.
This is not the case anymore with this patch. We can register thousands of
hooks, and if only 1 of those runs for a particular event, only 1 will be
evaluated instead of all of them.

See #824
2025-11-24 08:01:00 -05:00
qaqland
7ca21699a9 wpctl: add bash completions 2025-11-17 18:00:48 +08:00
Julian Bouzas
551353482a monitor/alsa: Also include alsa.* device properties for rule matching
UCM alsa nodes don't seem to have the 'alsa.*' properties from the device
included, which make it harder to match those nodes with alsa rules. This
patch adds all the 'alsa.*' properties in the UCM node to solve this.
2025-11-13 11:09:41 -05:00
George Kiagiadakis
f0b224b210 po: update Turkish translation
Closes: #871
2025-11-10 12:45:20 +02:00
George Kiagiadakis
0dad52f774 event-hook: simplify interest matching
We never use the feature of matching the subject type, so we can make
this simpler by not specifying CHECK_ALL, which also allows the
match_full() function to return early if some constraint doesn't match
instead of checking them all and wasting time.
2025-10-31 18:52:03 +02:00
Charles
27f97f6c45 monitors/alsa: Increase headroom for VMware and VirtualBox
Ubuntu received reports of very bad audio stuttering when running
25.10 in VMware & Virtualbox on Windows 11 hosts.

ac0d8ee4a8 dropped the headroom from
8192 samples to 2048. This seems fine for QEMU+KVM, but for unknown
reasons, the system emulation on Windows with these VMware/Virtualbox
is much slower, and xruns become frequent.

See:
https://bugs.launchpad.net/ubuntu/+source/wireplumber/+bug/2127250
https://discourse.ubuntu.com/t/vm-ubuntu2025-10-on-vmware-workstation-and-sound-problem/71066/3
2025-10-28 04:49:03 +00:00
filmsi
285230af67 Update Slovenian translation 2025-10-26 08:28:03 +00:00
Guido Günther
c095ae5254 role-based-policy: Allow to set target sink for media role loopbacks
There are streams that should go to a speaker rather than a headphone
or earpiece by default. Examples are alarms and emergency alerts on
phones. Allow to set a preference via
`policy.role-based.preferred-target` which then looks up the target
via `node.name` and `node.nick`.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-25 16:12:01 +03:00
Guido Günther
d2a49e8bc5 docs/linking: Fix typo
Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-25 16:12:01 +03:00
Guido Günther
b2c4993ab5 doc: Fix role based policy name
The role-priority-system doesn't exist anymore

Fixes: 0d995c56 ("wireplumber.conf: improve standard policy definition")
Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-25 16:12:01 +03:00
Julian Bouzas
ae30b4f022 state-profile: Handle new 'session.dont-restore-off-profile' property
This avoids restoring the Off profile if it was saved before. The property can
be set using the 'monitor.alsa.rules' section of the configuration.
2025-10-25 15:36:00 +03:00
Guido Günther
962be34a2b docs: Update rescan-for-linking priority
`rescan-for-linking` is the lowest priority linking event but the
`rescan-for-media-role-volume` event is lower overall priority.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:24:25 +02:00
Guido Günther
6f5ca5a79d wireplumber.conf: Enable default volume control tracking
Track a suitable default volume for media role based policies.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:20:33 +02:00
Guido Günther
5ecfe9f555 scripts: Add script to find a suitable volume control
When using role based priorities a volume slider presented to the
user should adjust the volume of the currently playing role.

E.g. when a phone has an incoming call and is ringing the default volume
slider should adjust the ringing volume and once the call has been
picked up it should adjust the call volume and once the call ended it
should adjust the media volume again.

It's currently hard for e.g. desktop shells to find out what a suitable
sink for volume control is so add a script to find a suitable target for
volume control (based on media role priority) by storing it's name in
the metadata.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:20:33 +02:00
Guido Günther
93377a8b4f m-std-event-source: Add rescan-for-media-role-volume
This allows us to do reduce the number of default volume updates.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:18:56 +02:00
Wim Taymans
ee72196500 m-si-audio-adapter: don't overread the position array
Limit the amount of channels we read from and write to the position
array with SPA_N_ELEMENTS(). The number of channels might be larger than
what we have positions for.
2025-10-21 16:31:00 +02:00
Julian Bouzas
e30c2a7cd9 state-routes.lua: Use the returned iterator from enum_params() to update routes info
This does not really fix anything but it saves some CPU cycles as we don't need
to get the route params from the cache anymore.
2025-10-17 09:05:27 -04:00
Julian Bouzas
4239055454 m-lua-scripting: Pass returned itrator to the closure when finishing enum_param()
We should not ignore the returned iterator as it allows users to get the exact
returned params after enumerating them, which might be useful in some cases.
2025-10-17 09:05:27 -04:00
Julian Bouzas
f188ddfb34 m-lua-scripting: Add WpIterator API
We skip the fold() API as the same thing can be done just using Lua closures.
2025-10-17 09:05:22 -04:00
Julian Bouzas
fb1738932b audio-group.lua: Demote creation of audio group log to info
This was never meant to be a warning message.
2025-10-17 15:49:02 +03:00
Julian Bouzas
f8be5a76e6 proc-utils: Make sure '/proc/<pid>/*' files exist before opening them
This avoids warnings if the specific file does not exist, especially when the
process has being removed quickly.

Fixes #816
2025-10-17 15:49:02 +03:00
Julian Bouzas
5c6a72e3cf m-si-standard-link: log error message when link activation fails
This makes debugging easier if a link fails to activate.
2025-10-17 08:41:48 -04:00
lumingzh
7b78078ed2 update Chinese translation 2025-10-14 10:01:43 +08:00
Julian Bouzas
6cfaf3f70d automute-alsa-routes.lua: Don't register/remove hooks if never registered/removed before
This avoids avent dispatcher errors in the log.
2025-10-13 16:15:34 +03:00
Julian Bouzas
2942903d0e scripts: Add node/filter-graph.lua 2025-10-13 15:48:41 +03:00
Arun Raghavan
38a21ea191 monitor/alsa: Add a setting to use HDMI channel detection
This allows us to set up the device to use HDMI ELD information for
channels. Not yet documented while we experiment with different ways to
make this work.
2025-10-10 15:44:48 -07:00
George Kiagiadakis
41b310c2d5 Add AGENTS.md
This helps steer LLMs when operating on this codebase - https://agents.md/

Mostly to help me with making releases for now
2025-10-10 18:03:07 +03:00
George Kiagiadakis
499916b996 0.5.12 2025-10-10 17:43:57 +03:00
Charles
627b003a05 m-permissions-portal: Avoid race condition during shutdown
Attempts to workaround a race condition between daemon thread and
GDBus worker thread during shutdown.

Ubuntu bug: https://bugs.launchpad.net/bugs/2127049

I've not been able to get a symbolic backtrace yet or reproduce it
myself, but the behaviour points to a threading bug. Hypothesis,

Main thread (1, daemon thread) shuts down, unregistering its plugins.
One of the plugins, module-permissions-portal, is triggered to
shutdown.
It tries to clear its GDBus connection handle without disconnecting
its signal handlers.
GDBus thread (2) is in the middle of writing a message on the same
connection handle.
Once finished, it also tries to clear its handle.
The main thread has already taken the signal lock and the signal
handler table ends up in an invalid state, triggering the assert.

I believe this could happen since
wp_portal_permissionstore_plugin_disable is not disconnecting its
signal handlers before trying to clear its DBus object.

See https://bugzilla.gnome.org/show_bug.cgi?id=730296 for more
discussion about this assert in the Glib signal handling code.
2025-10-10 13:04:01 +03:00
lumingzh
385fc83f46 update Chinese translation 2025-10-02 07:58:47 +08:00
Julian Bouzas
f82247c42c config: Add new 'node.features.audio.mono' setting
This setting allows users to toggle between MONO audio or not at runtime.
2025-10-01 18:59:13 +03:00
Julian Bouzas
084b3aab89 m-si-audio-adapter: Add new 'item.features.mono' configuration property
This allows configuring the audio adapter in MONO. The property is set to FALSE
by default.
2025-10-01 18:59:13 +03:00
Julian Bouzas
71f98c40f0 create-item: Reconfigure audio adapters if 'node.features.audio.*' settings changed
Up until now, all the 'node.features.audio.*' settings did not have any effect
if changed at runtime. This patch fixes this by reconfiguring the audio adapters
every time those settings have changed.
2025-10-01 18:59:13 +03:00
Julian Bouzas
2a4aa9281c m-si-audio-adapter: Configure the node ports if the item has been re-configured
If we re-configure the adapter with different settings than the ones from the
first configuration, we also need to configure the node ports to make sure they
are updated with the new settings.

For example, this is needed for stream audio adapters that have been configured
with monitor ports, and later re-configured without monitor ports. Without this
change, since stream audio adapters never configure the node ports on activation,
the internal node will still have the monitor ports present even after disabling
them in the 2nd re-configuration.
2025-10-01 18:59:13 +03:00
Julian Bouzas
35d63a7847 scripts: Add automute-alsa-routes.lua to auto-mute ALSA routes
This script mutes available output ALSA routes if an audio node that was
previously running was removed. This is useful for cases where users might
unplug their headset accidentaly, causing undesired loud audio to play on the
Speakers.

Two new settings are added to chose whether the user wants to do this for
ALSA devices, Bluetooth devices or both. The settings are set to false by
default.

Finally, a notification is also sent to notify the user that the devices were
muted.
2025-10-01 18:04:58 +03:00
Julian Bouzas
d21ff24ea1 modules: Add notifications-api module
This allows sending Desktop notifications using D-Bus.
2025-10-01 18:04:58 +03:00
lumingzh
68bd93e1ed Update Chinese translation 2025-09-29 12:49:33 +03:00
Carlos Rafael Giani
a461d9e738 object-interest: set pw_props variable if not set and global props exist
Otherwise, object manager lookups and iterations with type "pw" may
not work properly.
2025-09-10 15:04:36 +02:00
Julian Bouzas
ebd9d2a7d5 state-routes.lua: Make sure the device is still valid after doing enum_params() 2025-09-10 15:43:57 +03:00
Pablo Correa Gómez
7eacea9da9
apply-profile: add find-calling-profile to after array 2025-09-07 10:23:26 +02:00
Pablo Correa Gómez
dcf083ef3b
find-voice-call-profile: improve logging
To make it more specific to the hook and avoid confusion with the
best-profile one.
2025-09-07 10:23:26 +02:00
Pablo Correa Gómez
a6c0bb202d
scripts: document device/find-calling-profile hook 2025-09-07 10:23:26 +02:00
Pablo Correa Gómez
97d8761914
scripts: document device/find-preferred-profile hook 2025-09-07 10:23:26 +02:00
George Kiagiadakis
df0136ce0b docs: improve copyright & author statements 2025-09-05 18:19:25 +03:00
George Kiagiadakis
a5e58536dd docs: add wpctl man page and Tools documentation section
Add comprehensive wpctl documentation that generates both HTML docs and an installable man page from a single RST source.

Closes: #825

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 18:15:08 +03:00
Yukari Chiba
1bde4f2cdf device-info-cache: use Log.warning instead of Log.critical
Log.critical does not actually exist.
When logging error, it will in fact throw an exception:

[string "device-info-cache.lua"]:36: attempt to call a nil value
(field 'critical')

Change Log.critical to Log.warning to fix it.
2025-09-05 01:40:05 +08:00
George Kiagiadakis
3a785e5026 0.5.11 2025-09-02 16:26:40 +03:00
George Kiagiadakis
865cdcb89c conf: remove std-event-source dependency from the find-voice-call hook
This dependency must not be added here because it creates a circle.
m-standard-event-source is loaded after the hooks, as specified in the
wireplumber.components.rules section.
2025-08-29 19:55:18 +03:00
George Kiagiadakis
5ded5cbf1e lib: fix GObject introspection closure annotation warnings
Move closure annotations from data parameters to function parameters and
also add destroy annotations where necessary. This fixes the "invalid
closure annotation" warnings during the build.
2025-08-29 19:38:11 +03:00
Mark Nauwelaerts
e1807231ce global-proxy: also clear OWNED_BY_PROXY flag when proxy destroyed
... to avoid potential subsequent dangling pointer to proxy
2025-08-29 18:59:46 +03:00
Pauli Virtanen
8fdf726a66 linking: remove/add mpris-pause hooks on setting enable/disable
Remove mpris-pause hooks when the controlling setting is disabled,
and re-add when enabled, to avoid unnecessary processing.

Fix link tracking initialization, which previously never run. It worked
earlier since the event hook was registered early, but now it's needed.
2025-08-29 18:51:44 +03:00
Pauli Virtanen
2287c49139 po: restore deleted translations in sl.po 2025-08-29 18:22:13 +03:00
filmsi
9a89596ad2 Update Slovenian (sl) 2025-08-29 18:22:13 +03:00
Richard Acayan
0fd1b05b01 device-profile-hooks: add hook to select Voice Call profile
A "Voice Call" profile is supposed to be active during a call, even if
there is a higher priority device profile. Add a hook to select the
Voice Call profile when a call is active, and select a profile when
transitioning in and out of an active call.
2025-08-29 18:13:02 +03:00
Richard Acayan
2794764d5a m-modem-manager: add module for tracking status of voice calls
Voice calls can require special audio routing to work, such as by
switching the profile or opening an audio stream. Add a module to
monitor for the starting and stopping of a voice call.

Signed-off-by: Richard Acayan <mailingradian@gmail.com>
2025-08-29 18:02:59 +03:00
Julian Bouzas
bc026593d6 m-dbus-connection: Add 'plugin.name' and 'bus.system' args
These allow creating a shared system D-Bus connection.
2025-08-29 18:02:59 +03:00
Julian Bouzas
ed58f65184 state-routes.lua: Don't save again the route when restoring it
There is no need to save the route again after it has been restored because its
value has not changed.
2025-08-22 12:15:13 -04:00
Julian Bouzas
5060b27a9e apply-routes.lua: Always set save=false when applying routes
When applying new routes, we always want the save property to be false because
it is done by WirePlumber (not the user). WirePlumber applies routes when
restoring them from the state file, or when finding the best routes after the
profile has changed. In both cases, it does not make sense to set save=true.
2025-08-22 12:15:13 -04:00
Julian Bouzas
ea6f24e861 state-routes: Don't save routes that are not available
If the user changed the volume on a ACP 'Headphones' route to a value different
than 100%, and then unplugs the jack headset, the ACP 'Headphones' route becomes
unavailable with volume set to 100% and the save flag not cleared.

Since the save flag is not cleared when the ACP 'Headphones' route becomes
unavailable, WirePlumber will save it with 100% volume, overriding the previous
volume value set by the user. This is not ideal because the volume will be
restored to 100% by WirePlumber when plugging back the headset.

This change fixes this by never saving routes that are not available.
2025-08-22 12:15:13 -04:00
Julian Bouzas
ebd6d49a81 state-routes.lua: Make sure routes cache is always updated when evaluating them
The Route params changed event can be emitted before the EnumRoute params with
some devices, causing wrong evaluation of the routes because their cached info
is not updated. This change always enumerates the EnumRoute params before
evaluating them to make sure the cache info is always valid.

Fixes: #762
2025-08-22 12:15:13 -04:00
Julian Bouzas
8ab6ae5897 m-lua-scripting: Add enum_params Lua API for WpPipewireObject 2025-08-22 12:15:08 -04:00
George Kiagiadakis
30e8440b25 Update Slovenian (sl) po file
Closes: #832
2025-08-15 18:53:26 +03:00
Demi Marie Obenour
15f5f96693 Fix Lua type confusion bug
The only secure and robust way to check that a userdata is of the
expected type is to check its metatable.  Userdata metatables are not
changeable by Lua code without the debug library, so if the metatable is
a certain table, only C code could have made it so.  GLib type checking
functions are _not_ a robust or secure way to check that a block of
memory is a specific type of GObject, because Lua code could cause a
type confusion bug and potentially use it to forge pointers.
2025-07-29 17:24:35 -04:00
Barnabás Pőcze
05c3f31362 script-tester: wp_script_tester_create_stream(): fix property list leak
The `WpProperties` object containing the properties of the stream
was not released after it was no longer needed. Fix that.
2025-07-29 09:18:46 +03:00
Barnabás Pőcze
83e992b238 lib: settings: iterator: release parent object reference
`wp_settings_new_iterator()` takes a reference to the underlying
`WpSettings` object with `g_object_ref()`, however, it fails to
release it during finalization. Fix that.
2025-07-28 21:23:59 +02:00
George Kiagiadakis
96848883fe lib: spa-type: convert constant to a #define to make it work in switch statements 2025-07-25 22:38:19 +03:00
Demi Marie Obenour
f3625bee61 lua: fix SPA POD array and choice builders
These builders had many bugs:

1. They would longjmp() across the destructor of a g_autoptr() if a Lua
   error was thrown.  This will leak the memory in the g_autoptr()
   unless Lua is compiled with C++ exceptions.
2. They depended on the iteration order of numerical keys in Lua tables.
   Lua explicitly does not specify this order.
3. They would produce nonsensical SPA POD array or choice types with
   strings or bytes as values.  These would cause undefined behavior if
   manipulated by naive C code, or assertion failures if
   spa_pod_is_array() and spa_pod_is_choice() are modified to check that
   the contents of arrays and choices have sensible types.
4. They silently accepted extra arguments, potentially causing confusion
   and making it harder to extend the functions in a
   backwards-compatible way.

Solve the first problem by calling functions that can raise a Lua error
in a protected environment (with lua_pcall).  If there is a Lua error,
rethrow it after the g_autoptr() destructor has run.

Solve the second problem by first obtaining the number of keys in the
table and then iterating over the keys that are expected to be present.
If any of the keys are not contiguious integers starting at 1, the range
[1..number of keys] will include a number that is not a table key.  This
will result in lua_rawgeti pushing a nil onto the Lua stack.  An
explicit check throws a useful error in this case.

Solve the third problem by explicitly checking that the type is
reasonable before building an array or choice.  If it is wrong,
a Lua error is thrown.

Solve the fourth problem by using luaL_checktype (L, 2, LUA_TNONE) to
check that no unwanted values were passed.  The C function called with
lua_pcall is passed every argument passed by Lua, followed by a light
userdata that stores a context pointer.  After the light userdata is
popped from the Lua stack, the Lua stack is identical to what Lua
created when it called the outer C function, so the type-checking
functions in the auxillary library can be used to enforce that only the
correct number and type of arguments were passed.
2025-07-24 12:36:29 +03:00
Demi Marie Obenour
0cba7b9525 lua: Push "_new", not '_' and then "new".
No functional change intended.
2025-07-24 12:36:29 +03:00
Demi Marie Obenour
2ea068de1b _wplua_pcall: avoid Lua stack overflow
C code must ensure that the Lua stack does not overflow.  Ensure there
are enough slots for both the error handler and for the return values.
2025-07-24 12:36:29 +03:00
Demi Marie Obenour
db755a6a19 lua: use Lua extradata to store the reference count
Lua provides "extra data", which is some memory in each Lua state that
the application can use for its own purposes.  Use this to store the
reference count.
2025-07-24 12:36:29 +03:00
Pauli Virtanen
36f809fb50 lib: settings: make settings name optional
The "name" field needs to be optional, to be backward compatible with
old settings spec format.  If it's omitted, make it NULL.
2025-07-24 12:14:33 +03:00
Pauli Virtanen
9e47393643 po: update Finnish translation 2025-07-24 11:44:04 +03:00
Pauli Virtanen
a32e31ffa1 wpctl: localize settings descriptions + show names 2025-07-24 11:44:04 +03:00
Pauli Virtanen
8d26e9f73c wireplumber.conf: provide human-readable names for settings 2025-07-24 11:44:04 +03:00
Pauli Virtanen
3b1acc5474 lib: settings: add wp_settings_spec_get_name() for human-readable name
Extend settings spec with a human-readable name, and add function to get
it.
2025-07-24 11:44:04 +03:00
Pauli Virtanen
a8283001d9 po: extract translatable strings from wireplumber.conf
Add rules to extract translatable strings from wireplumber.conf.

Meson i18n.gettext does not support extracting strings from
autogenerated files. Hence, we must commit conf.pot to repository.

These setting descriptions are meant to be user-facing. Translating them
allows also 3rd party apps to get the translations from 'wireplumber'
domain.
2025-07-24 11:29:44 +03:00
Pauli Virtanen
e070260c5c tools: add utility to extract strings from SPA-JSON for translation
Add utility that extracts strings from SPA-JSON files in POT format.
2025-07-24 11:29:20 +03:00
Pauli Virtanen
f6912ec23c wireplumber.conf: improve settings descriptions
The setting descriptions are in principle user-facing, so try to make
their descriptions more clear.
2025-07-24 11:29:16 +03:00
Pauli Virtanen
fb218fe016 scripts: add mpris-pause.lua to pause media streams when target removed
When current output target of a media player application is removed, it
can be useful if playback is paused (to avoid e.g. music playback to
going to speakers when headset is accidentally unplugged).  Android etc.
implement a policy like this.

Add a policy script that monitors stream target removals. When it
detects a media player application that is linked to a no longer present
output target, it checks whether the stream is associated with a media
player seen in MPRIS. If yes, it sends MPRIS Pause() command to the
media player.

Enable this policy by default.
2025-07-23 10:19:44 +03:00
Pauli Virtanen
bc713acafd m-mpris: add MPRIS plugin
Add a plugin module that can list active MPRIS media players, and send
Pause commands to them.
2025-07-23 10:19:44 +03:00
George Kiagiadakis
6430e747f9 gitlab-ci: s/systemd/libsystemd/ in pipewire's meson command line
See pipewire@f2c878a2
2025-07-23 10:15:06 +03:00
George Kiagiadakis
faf042a82b wpctl: set-profile: set the "save" flag on the selected profile
Fixes: #808
2025-07-01 13:37:02 +03:00
George Kiagiadakis
e9928b4beb default-nodes/README: update documentation regarding the select-default-node event 2025-07-01 13:20:22 +03:00
George Kiagiadakis
754c805061 default-nodes/find-best: skip the hook if a very high priority node is selected
Just to optimize a bit more for performance
2025-07-01 13:13:21 +03:00
George Kiagiadakis
f1c96843ee default-nodes/rescan: remove unused table and access node.properties only once 2025-07-01 12:52:36 +03:00
Julian Bouzas
a433a49e28 default-nodes: Use session and route priorities when finding the default node
Some PCM devices can expose multiple nodes with same session priorities but
different route priorities. This improves the default nodes logic to also check
the route priorities when the session priorities are the same.
2025-07-01 12:52:36 +03:00
Carlos Rafael Giani
e97551818a m-si-audio-adapter: Suspend node before setting Format and avoid redundancy
When a new Format param is set, the node's state is not checked, so this
attempt can even take place when the node is not suspended. Setting that
param will not work if the node isn't suspended though. Add a check for
the state and suspend the node if needed.

Also, do not set the Format param if the new param POD is the same as that
of the existing format to avoid redundant calls.

(This mirrors already existing checks for the PortConfig param.)
2025-06-27 13:08:30 +03:00
Julian Bouzas
84e2fcc050 monitor/alsa: Increase priority for USB devices
We always want the plugged USB ALSA device to have higher priority than the
internal ALSA device.
2025-06-26 14:05:38 -04:00
Richard Acayan
73f52cfb94 monitors/alsa: manage node when bound
If the managed node needs to emit events before it is bound, Wireplumber
treats it as destroyed and ignores the events. Add the node as pending
before it is bound so the node can run set_param on events that happen
before it gets bound.

Signed-off-by: Richard Acayan <mailingradian@gmail.com>
2025-06-23 08:02:46 +03:00
George Kiagiadakis
f198f39f37 lib: module: clear the impl_module pointer when it is destroyed by itself
Some modules (like module-loopback) may destroy themselves with
pw_impl_module_schedule_destroy() when the pipewire connection closes.
In that case, we need to clear our pointer so that we don't try to
destroy them again (and crash)

Fixes: #812
2025-06-12 07:40:21 +03:00
George Kiagiadakis
7a4d317755 0.5.10 2025-05-21 07:32:15 +03:00
Tiago de Paula
ee42211bc6
fix: nil value in haveAvailableRoutes
Cause from partially renamed variables in af3c520d.

Closes #797
2025-05-21 01:00:59 -03:00
George Kiagiadakis
76b9e509d1 0.5.9 2025-05-19 12:32:21 +02:00
Philipp Jungkamp
404a634b92 docs: fix wrong description of software_dsp example 2025-05-18 15:49:59 +03:00
Julian Bouzas
0251d644a8 default-nodes/rescan: Check available routes using linking-utils
The linking-utils module already implements a way to check for available routes,
this patch uses it in default-nodes/rescan.lua to remove redundant code.
2025-05-18 14:11:53 +02:00
George Kiagiadakis
b8506d0d56 linking-utils: fix missing 'end' 2025-05-18 14:11:53 +02:00
George Kiagiadakis
af3c520d3e linking-utils: improve haveAvailableRoutes
Based on nodeHasAvailableRoutes() from default-nodes/rescan.lua
2025-05-18 13:56:14 +02:00
Julian Bouzas
050cd772be settings: cache setting values locally to avoid syncing issues
This patch caches the settings info to make sure their values are always
updated, even if using the settings API multiple times before pipewire
finishes synchronizing the metadata objects.

Fixes #749
2025-05-17 13:39:12 +03:00
Andrew Sayers
2a5606e437
Add TID and SYSLOG_{IDENTIFIER,FACILITY,PID} to log messages
Systemd journal entries have several common entries:
https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html

Add "SYSLOG_IDENTIFIER" to make it easier to find wireplumber messages.
Add "SYSLOG_FACILITY" to avoid confusing programs that expect both or neither.
Add "TID" and "SYSLOG_PID" to make debugging a little easier.
2025-04-22 11:24:09 +01:00
David Mandelberg
08d7e51efb test-utils: make it possible to specify a device's props
The test I wrote for
https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/778 uses
this, since it needs a target-loudness to make a loudness filter:

```
tu.createDeviceNode (
  "default-device-node",
  "Audio/Sink",
  { ["device.target-loudness"] = -18 }
)
```
2025-04-11 13:55:24 +00:00
Pauli Virtanen
2d48caa74b monitors/alsa: fix nil table indexing
It's possible that managed_node.properties["node.name"] == nil if the
node is gone.

The removeDevice call above has already cleared the node names, so no
need to do it again.
2025-04-11 13:07:43 +00:00
George Kiagiadakis
9f440d0b50 monitors/libcamera: fix deduplicating devices with the same name 2025-04-11 15:53:44 +03:00
Lukas Riezler
e51e1b6080 v4l2/monitor: scripts: fix for deduplicate devices with the same name 2025-04-08 10:06:45 +02:00
David Mandelberg
a2605a2cdf Change node.dont-remix to stream.dont-remix
I'm guessing this was a typo? I ran `git grep dont-remix` in both
wireplumber's and pipewire's repos, and all the other references were to
stream.dont-remix, including the definition of PW_KEY_STREAM_DONT_REMIX.
2025-04-07 17:28:15 +00:00
Robert Mader
0d356f90ed monitor-utils: Support devices without any device ids
Such as libcameras virtual devices.
2025-04-07 17:04:22 +00:00
Andrew Sayers
1eed9669f1
Avoid spurious warnings when dbus.service stops
wireplumber.service generates the following when dbus.service
stops before it (e.g. when the user logs out):

    m-dbus-connection: <WpDBusConnection:0x556b3c561680> DBus connection closed: Underlying GIOStream returned 0 bytes on an async read
    m-dbus-connection: <WpDBusConnection:0x556b3c561680> Trying to reconnect after core sync

Stop the service before dbus.service exits, to avoid these messages.
2025-04-03 15:38:08 +01:00
Julian Bouzas
ce4f9b08a9 proc-utils: Make sure cgroup length is valid before removing EOF char
This fixes a Coverity Scan defect.
2025-03-05 13:36:04 -05:00
George Kiagiadakis
0f3e005a92 gitlab-ci: update fedora and alpine images 2025-03-05 19:05:12 +02:00
George Kiagiadakis
ff692952c4 docs: software_dsp: fix example config snippet
The wireplumber.profiles section is an object, not an array
2025-03-05 17:50:43 +02:00
Julian Bouzas
0b716118c7 scripts: Add audio-group-utils.lua to group audio streams
This allows grouping audio streams that have a pw-audio-namespace ancestor
process name. The grouping is done by creating a loopback filter for each group
or namespace. Those loopback filters are then linked in between the actual
stream and device nodes. A '--target-object' flag is also supported in the
ancestor process name to define a target for the loopback stream node.
2025-03-05 16:28:34 +02:00
Julian Bouzas
86cdfaccc4 lib: Add new proc-utils API for process utilities 2025-03-05 16:28:34 +02:00
Pauli Virtanen
78f1e34029 autoswitch-bluetooth-profile: use s-device log topic 2025-03-02 15:09:53 +02:00
Hugo
d222b957af Improve documentation for lua scripts
I had a hard time figuring out all the steps relevant for this to work.
Hopefully this brief summary and couple of links will help the next
person writing their own script.

See: #601
2025-02-24 06:48:40 +00:00
Andrew Sayers
eec702e4a1
Use wp_info() on normal termination
The service is normally stopped by SIGTERM.  Using wp_info() here
means users will be more likely to notice abnormal exits.
2025-02-21 13:03:11 +00:00
Andrew Sayers
07e8248928
Use wp_info() for "Loading profile" message
This is an ordinary progress message - nothing for the user to do about it.
2025-02-21 13:03:09 +00:00
Andrew Sayers
d91c366c2f
Change the new "skipping device" warning to debug
The other "skipping device" (a few lines later) uses log:debug,
so it makes sense for these to be the same.
2025-02-21 13:03:03 +00:00
Barnabás Pőcze
5846d12ea1 wpctl: fix types in variadic arguments
`wp_object_manager_add_interest()` passes the format string
and the arguments after that to `g_variant_new()`, which
requires a 32-bit integer for "u". Passing a 64-bit integer
will cause problems on certain ABIs.

Furthermore, remove the metadata related interest declaration
from `set_default_prepare()` since the "set-default" command
does not access metadata directly, it uses the "default-nodes-api"
plugin.

Fixes: 7784cfad92 ("wpctl: support @DEFAULT_{AUDIO,VIDEO}_{SINK,SOURCE}@ as ID ")
Fixes #773
2025-02-19 18:49:07 +01:00
Barnabás Pőcze
f3bc7168ed wpctl: fix default device name leak
The `get-default-configured-node-name` handler returns a copy
of the name of the node, hence it must be freed.
2025-02-19 18:35:42 +01:00
George Kiagiadakis
32d2abdf34 internal-comp-loader: generate a "provides" for components that don't have one
It is valid for components not to have a "provides" field, but it
prevents them from being able to have "before" and "after" dependencies.
With this patch, we generate a hidden "provides" field so that the
dependencies sorting algorithm can work without issues.

Fixes: #771
2025-02-13 16:06:29 +02:00
George Kiagiadakis
ac69acb3c2 0.5.8 2025-02-07 17:42:58 +02:00
George Kiagiadakis
b031d3fcd1 scripts: populate session.services via a script
See pipewire!1409 / wireplumber!441
2025-02-07 11:05:44 +00:00
George Kiagiadakis
b697546476 lua: bind wp_core_update_properties() 2025-02-07 11:05:44 +00:00
George Kiagiadakis
b8f0cf3644 monitors/bluez: make the source loopback node appear as non-virtual
Fixes: #729
2025-02-07 12:54:43 +02:00
George Kiagiadakis
14cbddd007 tests/scripts: fix tests to respect "object.serial" vs "node.id" differences
Fixes: #761
2025-02-07 08:54:05 +02:00
Twlvnn Kraftwerk
48a415bc8f Update Bulgarian translation
Based of on https://l10n.gnome.org/vertimus/WirePlumber/master/po/bg/

Signed-off-by: Alexander Shopov <ash@kambanaria.org>
2025-02-05 12:09:53 +01:00
luzpaz
cb770c1d7e docs: fix various codebase typos Found via codespell -q 3 -S "*.po,./po/*,NEWS.rst" -L bootup,gir,inout 2025-01-28 15:45:54 +01:00
Pauli Virtanen
899943bfcf monitors: disable stream-restore for device loopback nodes
stream-restore should not be touching node properties of the Bluetooth
mic / device set / offload / ALSA UCM split loopback nodes. Those are
controlled by Route settings on the device.

Set device.routes and state.restore-props=false as appropriate to avoid
that.
2025-01-26 21:33:22 +02:00
George Kiagiadakis
a1bc3d9285 lua: fix wp_lua_log_topic_copy() to copy the topic name correctly
Fixes: #757
2025-01-08 11:35:57 +02:00
Pauli Virtanen
4d02d0275f monitors/alsa: provide splitting of UCM SplitPCM nodes
Instruct ACP to provide information about UCM SplitPCM channel
splitting instead of doing it with alsa-lib plugins.

Use the provided information to load loopbacks that create virtual sinks
that do the channel remapping from UCM.
2024-12-23 11:42:23 +02:00
Pauli Virtanen
83e93876d5 lib: spa-device: fix POD props iteration + key lifetimes
Fix memory leaks in bad GValue handling.  Unset iterator GValue after
use and strdup keys.  The keys aren't necessarily static strings (for
id-XXXX properties), so have to be dup'd.
2024-12-20 19:44:36 +02:00
Pauli Virtanen
027aba7f3b monitors/bluez: rename api.bluez5.id -> spa.object.id
Use same name as in alsa monitor.
2024-12-19 18:45:16 +02:00
Pauli Virtanen
ada687a4cc monitors/alsa: decouple name deduplication from node objects
Node name deduplication relying on presence of node objects creates race
conditions, as the name cannot be marked unused if the node object was
not created or was destroyed.

Use separate (device_id, node_id) -> name table to track name ownership
separately from the existence of node objects.

Also clear up the reserved names when device is destroyed, by the
monitor or device reservation.  In these cases "object-removed" for
nodes is not called, so this fixes names leaking when e.g. pw-reserve is
used.
2024-12-19 18:41:01 +02:00
Pauli Virtanen
65989e7e38 monitors: use WpSpaDevice:set_managed_pending()
In cases where a Node is created asynchronously and associated with the
Device later, set the id pending so that we don't miss ObjectConfig
events.

The "set Route again" workaround is also not needed, moreover it was not
reliable before either since the Device might issue ObjectConfig only
for changed properties.
2024-12-15 20:51:55 +02:00
Pauli Virtanen
2df5d94697 m-lua-scripting: add WpSpaDevice:set_managed_pending 2024-12-15 20:51:55 +02:00
Pauli Virtanen
22ab3c938f lib: spa-device: add wp_spa_device_set_managed_pending()
Allow marking WpSpaDevice object ids "pending", which means Props from
any ObjectConfig events received for the ids are saved, if there is no
associated object set yet.

When wp_spa_device_store_managed_object() is called, any pending Props
are set on the managed object.

This is useful when nodes cannot be immediately created in the
"create-object" signal handler. For example, in cases where the nodes
are created asynchronously, e.g.  by "module-loopback".  In this case,
although the nodes can be later associated with the WpSpaDevice, any
ObjectConfig events received in the meantime are lost, so for example
restoring saved Routes will race against async node creation.  Using
wp_spa_device_set_managed_pending() solves this race condition.
2024-12-15 20:51:55 +02:00
Quentin
2bfc464444 Update Occitan locale 2024-12-10 17:18:41 +00:00
Pauli Virtanen
4c1a6f5840 monitors/bluez: recreate SCO source when loopback is emitted
If loopback is emitted after the SCO node, e.g. when A2DP profile
connects late, recreate the SCO node.

This ensures the underlying SCO node is hidden when the loopback is
present.
2024-12-09 20:28:53 +02:00
George Kiagiadakis
3e7c87a84c 0.5.7 2024-12-02 16:10:17 +02:00
George Kiagiadakis
4c07fd1da1 device: find-preferred-profile: define device_name variable
Fixes: #751
2024-12-02 12:18:42 +02:00
George Kiagiadakis
e76ebde6d8 conf.d.examples: improve alsa.conf
Completely remove references to auto-profile/auto-port.
It's better if users are not tempted to enable them.
2024-11-28 16:04:55 +02:00
George Kiagiadakis
f79d4b1b3b monitors: alsa: disable api.acp.auto-profile by default
This seems to be an omission from when we transferred the default device
properties from the config file on to the Lua script. Both auto-profile
and auto-port are meant to be disabled, so that we can apply our own
management logic.

Fixes: #734
2024-11-28 16:02:57 +02:00
George Kiagiadakis
77d2dcd97f autoswitch-bluetooth-profile.lua: drop local function declarations
There is no point in local functions in the global script scope.
We use a sandbox anyway to isolate from other scripts.
2024-11-28 11:55:38 +02:00
Pauli Virtanen
1ddb473deb monitors/alsa: handle node activation failure
When node activation fails, it won't be created and its object-removed
signal can be called with some properties missing.  This breaks node
name deduplication, causing error

wplua: [string "alsa.lua"]:182: attempt to concatenate a nil value (local 'node_name')

It occurs e.g. when switching ALSA device profile, while some
application has opened the device with ALSA and is keeping it busy.

Fix by handling the activation failure, and tolerating missing property
in object-removed.
2024-11-23 17:11:37 +02:00
Wim Taymans
f4f495ee21 node: cast proxy to pw_node* when calling pw_node functions
This currently works fine because the functions accept void* but will
fail when they accept struct pw_node* in the future.
2024-11-20 10:10:09 +01:00
Andika Triwidada
19526e128f Update Indonesian translation 2024-11-11 10:37:30 +00:00
Pauli Virtanen
b65b53b200 m-reserve-device: cancel get proxy callback properly
Cancel the async calls that get the name of the application owning the
service, when WpReserveDevice is finalized or we are going to make
another call.

Fixes UAF accessing self when the async callback runs.
2024-11-01 20:46:54 +02:00
Pauli Virtanen
71f8682337 po: update Finnish translation 2024-10-12 13:46:30 +03:00
Julian Bouzas
76985fff5b autoswitch-bluetooth-profile: Switch to HSP/HFP on timeout
This patch adds a 500ms timeout callback to switch to HSP/HFP when a stream
starts capturing BT audio. This avoids quickly switching from A2DP to HSP/HFP
back and forth if an application just wants to probe the BT source for a short
period of time.

See #634
2024-10-10 15:54:59 +00:00
George Kiagiadakis
881c7ce2d7 ci: improve the rules of the previous commit 2024-10-09 17:15:47 -04:00
George Kiagiadakis
e3fbc91abf ci: optimize the CI to run less when not needed
Some jobs do not make sense to be run on certain cases and we can save
CI cycles that way.

Most build rules can be run only on merge requests and custom branches.
On master, it only makes sense to build fedora_with_docs for the
purpose of deploying updated documentation on gitlab pages.
The analysis steps only make sense to run when the relevant files have
changed.
2024-10-09 17:05:19 -04:00
lumingzh
51b68a7c20 update Chinese translation 2024-10-09 18:03:54 +00:00
Arun Raghavan
f5ed10d857 ci: Add workflow rules to avoid duplicate branch/MR pipelines
Copied from pipewire@15b5185e6fa5d4437b6acd9cbdf7a698e01019ab
2024-10-09 14:01:16 -04:00
Jhonata Fernandes
3e101f6941 Update Brazilian Portuguese translation 2024-10-09 17:33:31 +00:00
Robert Mader
b2d2f656fd monitor-utils: Check all libcamera v4l2 devices
The actual deduplication only checked the first device. Extend it to the
full list, as intended.

Fixes: 848b6326 (monitor-utils: Rework camera deduplication code)
2024-10-03 10:46:49 +02:00
Robert Mader
848b6326a9 monitor-utils: Rework camera deduplication code
The previous code made some invalid assumptions and was rather
complicated. Rework and simplify it.

The approach work as follows:
1. Once a new device gets registered, store its data in a list of pending
   devices.
2. Start a timer. On timeout, we check all pending devices and depulicate
   according to our heuristic. The timer gets reset whenever a device is
   added in order to avoid race conditions.
3. On timeout, add the pending devices to categories. For now: UVC cameras
   from the V4L2 plugin, libcamera cameras and other cameras from the V4L2
   plugin.
4. Then process the different categories in order of preference and store
   their V4L2 device IDs in a list for each plugin.
5. Before creating a camera, check that the V4L2 device(s) it uses are not
   yet used by a already existing camera from the other plugin.

While on it, drop support for Pipewire versions that don't report V4L2
device IDs at all.

Closes https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/689
Closes https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/708
2024-09-30 01:06:40 +02:00
Barnabás Pőcze
ed80938b8c module-dbus-connection: fix GCancellable leak
`wp_dbus_connection_disable()` creates a new GCancellable
object at the end, which is never freed if the GObject
is then destroyed. To fix this, override `finalize()` and
clear everything there as well.

Direct leak of 64 byte(s) in 1 object(s) allocated from:
    0 0x70e688efd1aa in calloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:77
    1 0x70e6874b3e62 in g_malloc0 (/usr/lib/libglib-2.0.so.0+0x63e62) (BuildId: 7b781c8d1a6e2161838c5d8f3bd797797c132753)
    2 0x70e6875dea75 in g_type_create_instance (/usr/lib/libgobject-2.0.so.0+0x3ea75) (BuildId: 5af5e0f7d0a900ecb6083fbd71e22e5522d872e2)
    3 0x70e6875c3804  (/usr/lib/libgobject-2.0.so.0+0x23804) (BuildId: 5af5e0f7d0a900ecb6083fbd71e22e5522d872e2)
    4 0x70e6875c4e7e in g_object_new_with_properties (/usr/lib/libgobject-2.0.so.0+0x24e7e) (BuildId: 5af5e0f7d0a900ecb6083fbd71e22e5522d872e2)
    5 0x70e6875c5ed1 in g_object_new (/usr/lib/libgobject-2.0.so.0+0x25ed1) (BuildId: 5af5e0f7d0a900ecb6083fbd71e22e5522d872e2)
    6 0x70e684d2a8a6 in wp_dbus_connection_disable ../subprojects/wireplumber/modules/module-dbus-connection.c:173
    7 0x70e688a833cc in wp_plugin_deactivate ../subprojects/wireplumber/lib/wp/plugin.c:144
    8 0x70e688a7126c in wp_object_deactivate ../subprojects/wireplumber/lib/wp/object.c:542
    9 0x70e688a6e74e in wp_object_dispose ../subprojects/wireplumber/lib/wp/object.c:191
    10 0x70e6875c0f6c in g_object_unref (/usr/lib/libgobject-2.0.so.0+0x20f6c) (BuildId: 5af5e0f7d0a900ecb6083fbd71e22e5522d872e2)
    11 0x70e6841f7d6d in wp_portal_permissionstore_plugin_disable ../subprojects/wireplumber/modules/module-portal-permissionstore.c:207
    12 0x70e688a833cc in wp_plugin_deactivate ../subprojects/wireplumber/lib/wp/plugin.c:144
    [...]
2024-09-26 15:08:39 +02:00
Torkel Niklasson
255b65d182 m-mixer-api: Fix memory in leak wp_mixer_api_set_volume
Declare result from wp_object_manager_lookup as g_autoptr, to prevent
leaking memory.
2024-09-26 14:12:18 +02:00
Pauli Virtanen
b68a6794cd autoswitch-bluetooth-profile: switch only Bluetooth devices
Handle only devices associated with Bluetooth loopback nodes.

Make sure the node.link-group iteration cannot get stuck if there is a
loop in the link graph.
2024-09-10 07:51:03 +00:00
George Kiagiadakis
141b2d5d3f 0.5.6 2024-09-05 20:59:11 +03:00
George Kiagiadakis
f2013d8cd0 lib: wp_core_connect_fd: add \since marker 2024-09-05 20:19:33 +03:00
George Kiagiadakis
e7dc79859d docs: document multi-instance configuration profiles 2024-09-05 20:12:03 +03:00
Robert Rosengren
ec8975ac6a docs: clarified how to setup debug logs in lua 2024-09-05 10:52:28 +00:00
George Kiagiadakis
40e5dbff3d systemd: load the system instance with the 'main-systemwide' profile by default
This can still be overriden in the configuration file, if needed.

See #608
2024-09-03 11:40:05 +03:00
George Kiagiadakis
43ea3db02c wireplumber.conf: add systemwide, embedded and split-instance profiles
Revamp the profiles section, making use of the inherits feature
and add commonly used profiles for systemwide & embedded use cases,
as well as profiles for a split-instance configuration where the
policy hooks run in a separate instance from the alsa, bluetooth
and camera monitors (which run in 3 separate instances respectively)

Also add an example on how to configure the loaded profile using
a config file instead of the CLI switch.

Fixes: #608
2024-09-03 11:39:10 +03:00
George Kiagiadakis
a061018150 internal-comp-loader: implement profiles inheriting other profiles
This allows to inherit all the profile definitions of another profile
before the current profile's definitions are parsed, allowing for
more complex structures to be present in the default wireplumber.conf
without too much copy-paste
2024-09-02 17:00:53 +03:00
George Kiagiadakis
fae966558c wireplumber.conf: add before/after dependencies where needed
All hooks except the monitor ones should be loaded before the
standard-event-source and all the monitors should be loaded after.
2024-09-02 15:45:29 +03:00
George Kiagiadakis
32be79ee56 wireplumber.conf: improve the v4l2 and libcamera monitors components definitions
This was needlessly complicated and some of the requires did not make sense
2024-09-02 13:55:41 +03:00
George Kiagiadakis
89ab5616c0 tests: component-loader: fix GError memory leak 2024-08-31 20:47:45 +03:00
George Kiagiadakis
a245d5fa46 internal-comp-loader: implement before/after dependencies for components
In some cases, requires/wants dependencies are not enough. As we saw in
!617, the m-standard-event-source module needs to be loaded after all
the hooks, otherwise there may be missed events that the hook was
supposed to "catch", but they were delivered before the hook was actually
loaded. In a similar fashion, we have in purpose put all the "monitor"
components at the every end of the array because if we load them earlier,
they will create devices and nodes before all the hooks are in place to
react.

While in standard configuration we can work this around, in extended
user configurations with custom components, it is impossible to do this
without overriding the entire components array.

To fix this properly, introduce before/after dependencies. They work in
a similar fashion as they work with event hooks. They do not implicitly
"pull" any components to be loaded, but they affect the ordering if the
mentioned components are indeed present.

Note that for backwards compatibility reasons and unlike systemd units,
the "requires"/"wants" targets imply an "after" dependency on them.

Fixes: #600
2024-08-31 20:33:42 +03:00
George Kiagiadakis
479523abf9 lib: settings: find the first loaded instance of WpSettings when metadata_name is NULL
This allows changing the metadata name in the configuration file
and get all the Lua scripts looking at this new settings metadata
without requiring any other code changes. It can be useful in
multi-instance setups where we'd like the instances to be isolated
and load their own settings instead of relying on each other for that.
2024-08-29 17:15:50 +03:00
George Kiagiadakis
219711129e main: show the profile name on the app name
Useful in multi-instance configurations
2024-08-29 17:15:50 +03:00
tytan652
895c1c7286 lib: core: merge wp_core_connect implementations 2024-08-26 14:56:03 +00:00
tytan652
41523b68ba lib: core: allow to connect a core with a given socket 2024-08-26 14:56:03 +00:00
George Kiagiadakis
8c25ee2e19 po: add Slovenian (sl) language
Closes: #705
2024-08-26 17:33:36 +03:00
Julian Bouzas
0a86653085 autoswitch-bluetooth-profile: Use event source object managers
This patch removes the manually created object managers in the BT profile
autoswitch logic, and instead uses the object managers from the event source,
simplifying the logic a bit.
2024-07-12 10:18:12 -04:00
George Kiagiadakis
773fee315a docs: add info on how to set the log level via configuration 2024-07-09 08:59:18 +03:00
Julian Bouzas
afcb91f59b rescan: Stop rescan for 2s if BT node is removed
The intention here is to fix a Bluetooth bug where changing between different
profiles momentarily causes streams to be linked to an internal speaker.
Unfortunately, this case is not handled by the event dispatcher because some
signals are emitted using idle callbacks, and therefore we have to rely on
extra logic in rescan.lua to solve the problem.
2024-07-01 08:12:33 -04:00
Julian Bouzas
ff33f38bea rescan: Merge filters metadata changed hook with rescan-trigger 2024-06-28 11:42:11 -04:00
George Kiagiadakis
43e939c0e3 0.5.5 2024-06-28 18:18:02 +03:00
Julian Bouzas
c60475b293 wpctl: Make sure default node Id is updated when printing filters
This will properly show '*' for default sink filters in the status output.
2024-06-28 15:13:34 +00:00
George Kiagiadakis
11f10b4f45 Revert "lib: core: set the export core to be shared to pipewire modules"
This reverts commit 8012fbd5cf.

There seems to be some underlying bug that sometimes causes a crash
when the Bluetooth monitor uses a loopback for autoswitching between
profiles. Reverting temporarily until the cause is fully determined.

See #682
2024-06-28 18:01:56 +03:00
George Kiagiadakis
01a7339625 linking: explicitly mark targets that should be managed by the role-based policy
The previous assumption that any target with "device.intended-roles"
should be managed by the role-based policy is wrong, as for example
Bluetooth SCO nodes always have "device.intended-roles = Communication"
and some ALSA devices managed by ACP also do. This is meant to be used
as a hint for the desktop policy (it's been there in PulseAudio as well)
and does not necessarily mean that a role-priority-based policy should
be applied on all links to those devices.

Instead, use a new property to explicitly mark all the targets that
are meant to be managed by the role-based policy and respect that in
all places where we check for a potential role-based policy link.

Fixes: #682
2024-06-28 10:21:02 +03:00
George Kiagiadakis
abc299c1d3 linking: redefine script dependencies
This way of definining dependencies ensures that if we remove one
of the find-* hooks from the config, the rest of them will continue
to work in the expected order. Previously, removing one of them
would break the entire chain.
2024-06-28 10:21:02 +03:00
Julian Bouzas
96dc045382 l/find-best-target: Allow regular filters to be best targets
Similar to 4868b3c3 and fa671216, we always want to treat regular filters as
normal stream/device nodes.
2024-06-27 15:36:42 +00:00
George Kiagiadakis
fe42d931da linking-utils: fallback to role priority 0 if none is defined
See #682
2024-06-27 17:10:04 +03:00
George Kiagiadakis
dc6694fb84 0.5.4 2024-06-26 17:39:04 +03:00
George Kiagiadakis
d31615c58f smart-equalizer.conf: fix filter.smart.target usage example 2024-06-26 17:32:33 +03:00
George Kiagiadakis
317b5e8013 config: rename the duck-level option to have "role-based" in its name
Also, move the example from linking.conf to media-role-nodes.conf,
as it's more relevant there, and make sure the setting is constantly
read back in the Lua script, so that runtime changes can work.
2024-06-26 17:23:09 +03:00
George Kiagiadakis
52590ac850 docs: linking: update existing hooks documentation 2024-06-26 16:34:39 +03:00
George Kiagiadakis
d8fbf887b8 scripts: remove deprecated and outdated intended-roles.lua
This is now provided by find-media-role-target.lua
2024-06-26 16:30:45 +03:00
George Kiagiadakis
54d0f6fc7b l/rescan: use parseBool() to interpret boolean property
The previous check was also working, but this is safer
2024-06-26 16:19:32 +03:00
George Kiagiadakis
9436dc7afe s/link-target: mark links as role-based only if the role links rescan hook is enabled
Otherwise we would be left with broken links that are never activated
2024-06-26 16:11:00 +03:00
George Kiagiadakis
248b489b1b script-tester: load find-media-role-target.lua to fix test failure 2024-06-26 15:50:20 +03:00
George Kiagiadakis
39a1ce4eee config: add example that shows how to setup a smart equalizer filter 2024-06-26 15:50:20 +03:00
George Kiagiadakis
ebe158d8cb s/link-target: do not treat links to monitoring streams as role links
There are 2 cases captured here:

1. The stream has "stream.monitor = true", which is used
by pavucontrol for the vu meters and should probably not be managed
by the role policy, no matter if the target is a Source or a Sink

2. The stream is an Input (capture) stream and is targeting a Sink,
which may be used to target the sink's monitor ports. Even if we
want, we can't manage these links because they have opposite direction
than the one expected. Let's leave them alone...
2024-06-26 15:50:20 +03:00
George Kiagiadakis
fd4607b4f5 s/link-target: avoid holding a reference to the si in the link-error callback 2024-06-26 15:50:20 +03:00
George Kiagiadakis
fea6b11ad1 s/create-item: add setting to enforce a media.role on streams that don't have one 2024-06-26 15:50:20 +03:00
George Kiagiadakis
42727fcbc6 linking: move find-media-role-target hook to run later in the chain
A defined target should have priority over a role-based target
2024-06-26 15:50:20 +03:00
George Kiagiadakis
8012fbd5cf lib: core: set the export core to be shared to pipewire modules
pw_context allows sharing a core between modules and this is useful
to avoid having all the role loopbacks connect to pipewire on their own
2024-06-26 15:50:20 +03:00
George Kiagiadakis
b029e5a5b5 config: add example media-role-nodes.conf 2024-06-26 15:50:20 +03:00
George Kiagiadakis
0d995c56a8 wireplumber.conf: improve standard policy definition
First, add the find-media-role-target to the standard policy.
It does not affect anything and it doubles as the replacement
for intended-roles.lua, as it merely prioritizes linking nodes
that have "media.role" set to targets that have "device.intended-roles"
set to the corresponding role.

Second, make the rescan-media-role-links.lua also load as part
of the standard policy. The idea is to embrace this functionality
by default and make it available on desktops. It does not break
anything as well, but it will apply its own rules to links that
are created between nodes that have a
"media.role" <-> "device.indended-roles" match.
2024-06-26 15:48:07 +03:00
Ashok Sidipotu
5948539551 remove "virtual items" scripts, m-si-audio-virtual and related tests 2024-06-26 15:48:07 +03:00
Ashok Sidipotu
f6b77c7456 linking: role based priority system: hook to apply policy 2024-06-26 15:48:07 +03:00
Ashok Sidipotu
fcaece85e9 linking-utils: add routines to keep track of the priority media role link 2024-06-26 15:48:07 +03:00
Ashok Sidipotu
375094151d linking: get-filter-from-target.lua: bypass for media role targets 2024-06-26 10:16:10 +03:00
Ashok Sidipotu
4dd831424a linking: add a new hook to find media role targets 2024-06-26 10:16:10 +03:00
Julian Bouzas
4868b3c336 get-filter-from-target: Don't bypass the hook if the session item is a regular filter
Regular filters should be treated as regular stream / device nodes. Note that
the filter utils API is meant to be used using the direction of the main smart
filter nodes. This is why we use the target direction instead of the actual
stream direction to check if the stream session item is smart or not.
2024-06-25 14:15:46 -04:00
Julian Bouzas
fa67121665 filter-utils: Allow smart filters to have as target filters that are not smart
Filters that are not smart should be treated as regular client/device nodes,
therfore, smart filters should be able to use them as a target.
2024-06-25 14:05:43 -04:00
Wim Taymans
78dd8b1d8f tests: skip some tests when audiotestsrc is unavailable 2024-06-18 13:23:44 +02:00
George Kiagiadakis
47ec81408e scripts/device: avoid crashing if the device.name is not set
This should never happen, but there are odd cases with broken
configuration where such a device may appear and the least we can do
is not crash, at least not when the device.name is only used for
debug messages. In state-profile and other places where the name
really matters, we actually have checks in place.

Fixes: #674
2024-06-17 09:13:16 +03:00
George Kiagiadakis
31806862b0 tests/examples: add example on how to set node "params" under Props 2024-06-15 14:05:27 +03:00
Julian Bouzas
2353a0991b autoswitch-bluetooth-profile: Always destroy the restore timeout source before switching
We always have to destroy the restore timeout source when we want to switch to
HSP/HFP profile, even if the device is already set to HSP/HFP or has already an
input route. Apart from this, we also want to make sure there is no pending
timeout source when we are creating a new one. This should avoid an infinite
loop about switching BT profiles when capture nodes are created and destroyed
quickly.
2024-06-12 16:27:19 -04:00
James Calligeros
f57a46308d node/software-dsp: ensure that filter chains are properly unloaded
Indexing into the subject from a node-removed event is slightly quirky.
As a result, we were not properly freeing filter chains tied to
disconnected nodes.

Change how we store the list of loaded filters and ensure they
are properly freed when their parent node is removed from the graph.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-06-10 11:31:50 +00:00
Julian Bouzas
eb707096f7 scripts: Fix autoswitch BT profile when using filters
If a source filter is used, the BT profile autoswitch does not work because
WirePlumber thinks there is always a client capturing audio from it. This patch
fixes the issue by recursively walking through the source filters that are
linked to the BT loopback source until a stream is found. If a stream is found
at the end of the chain, then it switches to HSP/HFP profile, if the stream is
not found, the profile stays to A2DP.
2024-06-06 17:05:18 -04:00
Julian Bouzas
789b526c61 bluez: Don't create loopback source if autoswitch setting is disabled 2024-06-06 17:00:46 -04:00
George Kiagiadakis
be9259d952 conf: further improve how top-level objects are handled
With this change, it is possible to have a top-level object that does
not start at exactly the beginning of the file, allowing comments and
whitespace to exist.

Also add an empty conf file in the tests to verify that
it does not crash.
2024-06-02 15:47:09 +03:00
George Kiagiadakis
5235c025fe conf: skip empty configuration files to avoid crashing
g_mapped_file_get_contents() returns NULL if the file is empty

Fixes: #671
2024-06-02 15:17:06 +03:00
George Kiagiadakis
65e4ae83b9 0.5.3 2024-06-01 14:53:36 +03:00
Barnabás Pőcze
53e3cc7c7e metadata: remove incorrect transfer annotation 2024-05-31 23:03:12 +02:00
Barnabás Pőcze
b6595fb586 core: add missing \ingroup 2024-05-31 23:00:36 +02:00
Barnabás Pőcze
7e21e27ca9 docs: gen-api-gtkdoc.py: fix SyntaxWarning with raw strings
Use raw strings for regex patterns to avoid the invalid escape
sequence `SyntaxWarning`.
2024-05-31 22:54:13 +02:00
George Kiagiadakis
0b51b6b570 gi: hide WP_PRIVATE_API functions from gir
Document them and annotate them with (skip) so that they are
marked as introspectable=0 in the gir file.

Fixes: #599
2024-05-31 21:00:07 +03:00
George Kiagiadakis
ba0de72a9d tests: conf: test a few more edge cases
- split merge.conf into two files, one being standard JSON,
  to ensure we can parse this
- ensure that key-value pairs are correctly overriden when
  merging without the override. prefix
- remove context.modules, which is no longer needed there
- fix a typo with a stray ; character
2024-05-31 19:55:07 +03:00
George Kiagiadakis
5ec1d2c2e1 tests: conf: add test for as-section parsing 2024-05-31 19:54:04 +03:00
George Kiagiadakis
a3d5c8088b lib: conf: make it possible to parse files wrapped in {}, to allow standard JSON
A bit of refactoring was needed in order to avoid breaking as-section
loading, which expects the entire file to be an object or array and doesn't
parse it.

Fixes: #633
2024-05-31 18:35:32 +03:00
George Kiagiadakis
9847ca129a lib: spa-device: emit object-removed & create-object in sequence when an object is updated
In some cases the monitors emit the object_info callback with the same id
and non-NULL info multiple times. For example, when an ACP profile changes from
output:analog-stereo+input:analog-stereo to just output:analog-stereo, it emits
object_info() with NULL info for the input node and object_info() with updated
properties and the same id as before for the output node.

This causes the spa-device here to emit create-object multiple times for the
same object and this breaks the name deduplication logic. To solve this, make
sure we emit object-removed before calling create-object a second time.

Fixes #500
2024-05-31 17:04:18 +03:00
George Kiagiadakis
e6eed2dfb0 monitors/alsa: add some logging around node name deduplication 2024-05-31 17:03:33 +03:00
George Kiagiadakis
e3e8c9cdcb device/state-profile: do not restore unavailable profiles from the state file
Fixes: #613
2024-05-31 12:14:19 +03:00
George Kiagiadakis
9b4628455c node/create-item: simplify si properties configuration
Copy all the node properties into the session item and customize
afterwards, instead of starting with an empty set and cherry-picking
properties... We need to cherry-pick more and more properties
overtime and it's easy to make mistakes
2024-05-24 20:41:16 +03:00
George Kiagiadakis
df8bc12464 state-stream: fix using the default volume
When there was no previous state stored, the default volume was
also not applied because the code would return from the hook early

Fixes: #655
2024-05-24 20:36:28 +03:00
Julian Bouzas
b40ba825c2 filter-utils.lua: Check media type when finding the default filter
This avoids wireplumber trying to link a video stream node with the default
audio smart filter if smart audio filters are configured.
2024-05-24 07:33:21 -04:00
George Kiagiadakis
226be2e2b2 monitor-utils: fix variable check
See !636
2024-05-23 12:59:46 +03:00
George Kiagiadakis
2811d46a38 monitor-utils: make cam_api_data a local variable
Otherwise it is global and it retains a reference to the `parent`
WpSpaDevice object until this function is called again, which prevents
some camera nodes - in some cases - from being destroyed

Fixes #640
2024-05-23 12:35:23 +03:00
Barnabás Pőcze
4ed51791e0 linking: return after aborting transition
Avoid calling return_error() on the same transition multiple times.

Fixes: 4b153ec553 ("link-target.lua: change into a async hook")
See #628
2024-05-18 01:36:39 +02:00
Barnabás Pőcze
89b6766cd6 transition: ensure single completion and finish
At the moment, the same transition can be completed multiple times
(assuming sufficiently high reference count):

  return_error()
  finish()
  return_error()

The consequence of that is that the transition closure will be
invoked multiple times. This is unexpected.

Add some guards against completing a transition or calling finish()
multiple times.

Fixes #628
2024-05-18 01:36:39 +02:00
Barnabás Pőcze
1ddfbc532c transition: fix memory leak when error is already set
Fixes: 18377fbf82 ("transition: don't allow _return_error() to be called recursively")
2024-05-18 00:46:58 +02:00
Ashok Sidipotu
88d1dd86e5 wireplumber.conf: increase the cam discovery timer value 2024-05-08 14:20:16 +00:00
Ashok Sidipotu
a6328cf2c1 m-lua-scripting: correct typo 2024-05-08 14:20:16 +00:00
Ashok Sidipotu
06e11dc4be monitors/camera: fix camera device deduplication not working for certain devices.
Enhance the parsing logic to consider more than one device number. libcamera
devices some times use up multiple V4L2 devices, in this case libcamera should
be given a chance to enumerate the device.

Fixes #623
2024-05-08 14:20:16 +00:00
Tom Hughes
ae983e6fd7 Improve monitoring of seat state
If the user is reported as active then check that they have at
least one active seat and downgrade the status to online if not.

This ensures that a remote login session won't be interpreted as
the user being active on a local seat.
2024-05-08 13:58:16 +00:00
George Kiagiadakis
3e643aad85 log: use G_GNUC_DEPRECATED_FOR to deprecate wp_log_checked 2024-05-03 13:46:52 +00:00
Pauli Virtanen
2fb055f43d log: add wp_logt_checked taking WpLogTopic, to decide on debug messages
Make decision on whether to show code location in journal messages based
on whether the WpLogTopic would enable DEBUG level messages.

Add wp_logt_checked API to take WpLogTopic as input to make this
possible, and deprecate wp_log_checked.
2024-05-03 13:46:52 +00:00
Pauli Virtanen
8016ad1cec log: prepend topic to journald logs & add locations on debug levels
Logging messages are usually developed based on the stderr logging,
which shows code location and log topics as context, and especially
higher log level messages can be cryptic or useless without that
context.

This context is not shown in journalctl by default, and so usually is
missing from logs submitted by users. In principle it is available in
journalctl but requires extra work to show it.

Fix by prepending the log topic to the messages, like Pipewire logging
does. Also show code locations if log level is high enough.
2024-05-03 13:46:52 +00:00
George Kiagiadakis
4eb9454ceb wireplumber.conf: add a video-only profile
For systems where only camera & screensharing are to be used.

Fixes: #652
2024-05-03 16:13:10 +03:00
George Kiagiadakis
e6a70db254 json-utils: fix overriding of non-container values when merging
Non-container values should always be overriden

Fixes: #653
2024-05-03 15:54:37 +03:00
Julian Bouzas
0bd64f17af filter-utils: Check main filter nodes in a more robust way
This patch makes the check more robust when detecting main filter nodes. This is
because some filters might append '/Internal' to their main node media class.
The direction check has also been improved to work with Video filters.
2024-05-02 12:26:55 -04:00
Stefan Ursella
709eecb21f lua: json: fix error ouput 2024-04-30 05:32:33 +00:00
Stefan Ursella
fd7a1af7ff lua: json: add method to merge json containers 2024-04-30 05:32:33 +00:00
Pauli Virtanen
42370f0547 scripts: fix event:get_data() usage in apply-profile
event:set_data() coerces Lua tables to properties, so that keys and
values become strings.  Take into account that profile.index is a string
due to this.

Fixes WP setting the profile, even though it is already the active one.
2024-04-27 17:48:16 +03:00
George Kiagiadakis
b302ebd6ab 0.5.2 2024-04-22 17:19:47 +03:00
Pauli Virtanen
7f30adeb42 core: fix WpLoopSource lifecycle
Currently nothing removes the WpLoopSource from the main context, and we
moreover are creating and attaching unused WpLoopSources when sharing
same pw_context between multiple WpCore.

Fix this by keeping track of the WpLoopSource in the pw_context user
data, only creating them when needed, and finalizing it after the
context is destroyed.
2024-04-22 08:49:28 +00:00
Pauli Virtanen
7997fd490b core: keep pw loop entered exactly when it is running
It is intended that the Pipewire loops are "entered" the whole time they
are running, i.e. are going to process further events. Currently we
enter/leave for single iterations, which is not right.

Fix by entering the loop on initial GSource idle callback, and leaving
at finalize.

That the loop has to be left from the same thread it was entered from is
a bit hard to do with GSources, as the finalize callback appears not
guaranteed to be called from any particular thread.  I didn't find a
fully general way to do this, so this puts additional constraints on how
WpLoopSource (and WpCore) are to be cleaned up.
2024-04-22 08:49:28 +00:00
George Kiagiadakis
e4f9fb824e default-nodes: fallback to priority.driver if priority.session is not set
See #642
2024-04-22 11:39:45 +03:00
Stefan Ursella
473e463c56 meson: create the lib version like pipewire
If we use the project version as library version,
it is not possible to append something to the
project version to indicate a modified wireplumber version.
2024-04-18 09:35:23 +00:00
James Calligeros
1844fd6d61
Revert "node/software-dsp: do not hide target node when hide-parent is false"
Using parseBool is actually more broken if the property is already parsed as a
boolean, and causes the node to remain unhidden even when hide-parent = true.

Revert 2a45842169 to fix this behaviour.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-17 18:31:09 +10:00
James Calligeros
4c57647203
docs: document filter-path property of software DSP policy
Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-17 18:31:09 +10:00
James Calligeros
d89293b606
node/software-dsp: implement loading filter graphs from disk
Using the new Conf() constructor, we can load and parse a filter graph
from a file on disk. This is useful when, for example, maintaining a
large database of filter graphs. It also keeps wireplumber.conf.d free
from clutter.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-17 18:31:09 +10:00
James Calligeros
d77edf70e9
lua: allow Conf methods to be indexed or called
Given that we are allowing Conf() to instantiate a new WpConf,
the methods on the class need to be callable in reference to Self while
maintaining API compatibility with being indexable from the table as a
static method.

Allow this by checking if the first argument passed in is userdata.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-17 18:31:09 +10:00
James Calligeros
f769ea8f30
lua: add constructor and file ops for WpConf
This exposes the ability to load a SPA-JSON representation of a WpConf
object from an arbitrary file on disk to the Lua API

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-17 18:31:04 +10:00
James Calligeros
59d29f37ac conf: allow a SPA-JSON container to be loaded as a named WpConf section
A "plain" JSON object may be passed to WpConf constructors. Previously, this
would cause an error as open_and_load_sections expects the first parsed token
to be a string. Use WpProperties to denote that the file being parsed is an
object/container, and specify a section name for it.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-09 13:15:50 +00:00
James Calligeros
34040d8e44 conf: allow a WpConf to be loaded without fragments
Use WpProperties to indicate that searching for and loading
conf.d fragments can/should be skipped.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-09 13:15:50 +00:00
James Calligeros
42666e2054 conf: Add WpProperties as a member of WpConf
WpProperties may be used to control the behaviour of a WpConf
object. This allows those properties to be referenced at any time
during the lifetime of a WpConf.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-09 13:15:50 +00:00
James Calligeros
105d53025e conf: explain behaviour of wp_base_dirs_find_file
The signature of the function is quite misleading. If an absolute
path is passed to it, it will ignore the directory constraint flags
and search outside of the specified directories anyway. Make a note
of this in its caller.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-04-09 13:15:50 +00:00
George Kiagiadakis
8892204f24 wplua/sandbox: support mixing static methods and constructors in class identifiers
Global class identifiers, such as "Node", "SessionItem", "Conf", etc
are so far defined either as methods, which are constructors for
the GObject class, or as tables, which contain "static" methods, i.e.
methods that can be called without an instance.

In some cases, we may want to mix a class being both instantiatable
with a constructor and also have static methods. To support this,
allow the class identifier be declared as a table with the constructor
being defined as the "__new" method. This change allows calling the
table as a method and execute "__new" underneath.

For instance:
```
json = Conf.get_section_as_json("foobar") -- static method
conf = Conf("/foo/bar") -- constructor, equivalent to Conf.__new("/foo/bar")
```

See also !629
2024-04-09 13:15:50 +00:00
Pauli Virtanen
9d7c6b85d0 monitors/bluez: fix BAP device set node naming
It is intended that its name is the actual device node name.
Fix this, as it was broken in some recent changes.
2024-04-06 21:59:16 +03:00
George Kiagiadakis
8ee351838d monitor-utils: clear cam data after creating nodes
The cam_data structure stores a reference to the "parent" WpSpaDevice
and doesn't allow it to be destroyed when the monitor detects that
the device is no longer present. Clear it right after pushing the event
to make sure there's no dangling reference left around

Fixes: #627
2024-04-03 12:31:42 +00:00
George Kiagiadakis
7856124df0 core: set context.modules.allow-empty to silence warning in pw_context
See cddea858d9

Closes: #620
2024-04-03 11:52:18 +03:00
George Kiagiadakis
961450b2ac 0.5.1 2024-03-30 16:50:38 +02:00
George Kiagiadakis
1fddefa072 docs: move the software_dsp document down in the TOC
This is one of the least likely things that a user would want to
discover and use, making it wierd to be the very first "policy"
to present.
2024-03-30 15:52:14 +02:00
George Kiagiadakis
ab18cb1848 docs: improve the wording and formatting of the software_dsp doc 2024-03-30 15:44:38 +02:00
George Kiagiadakis
3b0c0fcd7e state-stream: fix storing/restoring notification volume
We apparently store things in the <media-class>:<key>:<value>
format, while pipewire-pulse expects <media-class>.<key>:<value>
and this detail was missed in the latest refactoring.

It also seems that I forgot to ensure that restoring this metadata
key worked. The hook was there, but not registered.
Remove the hook and directly populate the metadata object when
it is activated, as it seems simpler.

Fixes: #604
2024-03-30 11:53:45 +02:00
James Calligeros
2a45842169
node/software-dsp: do not hide target node when hide-parent is false
The existence of props["hide-parent"] is truthy. Parse the boolean explicitly
to get the correct logic.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-30 00:00:57 +10:00
James Calligeros
b45eafa53c
docs: Document automatic software DSP handling
Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-30 00:00:52 +10:00
James Calligeros
692e6e4b5b
wireplumber.conf: add node.software-dsp to wireplumber.components
This component provides the automatic filter graph interface to consumers.
We provide the component by default, however it is not activated unless
explicitly requested in wireplumber.profiles by a consumer.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-29 23:49:21 +10:00
James Calligeros
82b0ec8c30
node/software-dsp: Port Node ObjectManager to SimpleEventHook
Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-29 23:48:09 +10:00
James Calligeros
9ab7c024f8
node/software-dsp: get match rules from conf.d
This allows DSP rules to be specified in a wireplumber.conf.d file under
the node.software-dsp.rules section. Properties are specified in the
software-dsp action.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-29 23:46:54 +10:00
James Calligeros
d5217fd4b6
node/software-dsp: move and rename scripts/policy-dsp.lua
As this DSP "policy" is intended to run on the creation/deletion
of a node, move it into the node subdir.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-29 22:44:53 +10:00
James Calligeros
62dd6effa8
policy-dsp: allow matching on all node properties
Prevously, we were only looking at node["global-properties"] for
properties to match on. Change this to instead run against
node.properties, so that we can use type-specific properties such as alsa.id.

Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
2024-03-26 19:14:05 +10:00
Robert Mader
3ffd0956d4 linking: Use sendClientError in link-error handler
The current code was not updated to use `clients_om`, thus never
found a client, ending up never sending client errors.
Just use the shared helper to avoid such issues in the future.
2024-03-25 14:56:30 +01:00
Robert Mader
94031f8ef9 scripts: add error code argument to sendClientError helper
And update all users accordingly. This will allow the following commit
to use the helper with a different error code.
2024-03-25 14:56:04 +01:00
George Kiagiadakis
f69d25631d m-portal-permissionstore: improve the warnings printed due to remote errors
Demote the NotFound error returned by Lookup(), as it seems common to be
printed if the permission has not been configured.
2024-03-25 13:51:46 +02:00
George Kiagiadakis
053d2ae69c m-lua-scripting: downgrade notice to debug when printing operation errors
These errors are propagated to the caller, so it's the caller's
responsibility to print them appropriately, if necessary. Printing
a notice also here is only confusing.

A debug mesage is still be useful for developers to understand the flow.
2024-03-25 09:55:59 +02:00
George Kiagiadakis
c89316e52c linking: improve link failure & debug messages 2024-03-25 09:55:13 +02:00
George Kiagiadakis
7612068675 main: print warning about old style config files
Closes: #611
2024-03-25 09:07:22 +02:00
George Kiagiadakis
3043d258b3 docs: remove main.rst
This is out-of-date and wrong. I wanted to salvage the virtual-items
configuration docs from in there, but we are going to change that soon
- see https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/610 -
so the entire file can go away.
2024-03-23 17:14:01 +02:00
George Kiagiadakis
04a248f4d1 docs: conf_file: raise more the visibility of the config break 2024-03-23 17:09:09 +02:00
George Kiagiadakis
e6dc547624 docs/daemon: re-arrange some sections in the toctree to make more sense 2024-03-23 17:05:49 +02:00
George Kiagiadakis
ccfc501e82 docs: move well-known settings docs from src/scripts/lib to where they belong
These were documented in src/scripts/lib initially because the settings-manager
scripts used to be also there and it made sense to keep them close together.
2024-03-23 16:49:13 +02:00
George Kiagiadakis
c4d4cd48f6 docs: add a basic "config migration from 0.4" guide 2024-03-23 16:45:32 +02:00
Pauli Virtanen
61d1398f5b autoswitch-bluetooth-profile: never use headset profile without input
Autoswitch should make sure the saved headset profile always has an
input route.  Check this on loading/saving it, otherwise reselect it.

Headset mode profile without mic used to make some sense when it wasn't
using the loopback node (ie.  easy way to disable it), but now with the
loopback node this doesn't make sense any more and it's confusing if it
occurs, so we shouldn't allow it.
2024-03-23 14:02:21 +02:00
Pauli Virtanen
a73c931723 monitors/bluez: fix autoswitch A2DP input profiles
Profile autoswitch should support also A2DP profiles with input route,
so the loopback should not assume the source node is SCO one.

Also fix handling of AG SCO output stream nodes, which should not be
hidden as internal.
2024-03-23 14:02:21 +02:00
George Kiagiadakis
a562b22f60 docs: fix typo in {device,node}.disabled documentation
Fixes: #609
2024-03-23 12:00:55 +02:00
Julian Bouzas
291d3cd9a2 m-settings: remove all persistent settings if key is null
The wp_settings_delete_all() API internally uses wp_metadata_clear() to clear
all the keys in the persistent-sm-settings metadata object. This call emits
the metadata changed signal with both 'key' and 'value' set to NULL. When that
happens, we need to clear all settings from the state file.
2024-03-23 09:39:45 +00:00
Julian Bouzas
b16763f8d4 m-settings: clear schema settings metadata when plugin is disabled 2024-03-23 09:39:45 +00:00
George Kiagiadakis
50497cea03 m-std-event-source: cancel events when the node associated with the si dies
It is possible that a node is destroyed while the select-target
event is ongoing. In that case, the next hook to run will likely crash.
Fix this by cancelling events automatically when their subject
is a session-item associated with a node, as soon as the node is destroyed.

See !619
2024-03-22 15:35:22 +02:00
Pauli Virtanen
428462ddf3 filter-utils: fix handling of targetless smart filters
A smart filter should be considered "targetless" (i.e. interpose on
streams going to default target) only if filter.smart.target is not set.

Currently any smart filter with specified target not found is considered
such, which is wrong.  This causes misbehavior, such as all recording
streams going to the bluetooth dummy source.

Fix this by doing it correctly.
2024-03-21 20:19:26 +02:00
George Kiagiadakis
59d190a2bd 0.5.0 2024-03-18 17:51:32 +02:00
George Kiagiadakis
0508561686 main: set application.version on the core
This allows seeing wireplumber's version in pw-dump and such
2024-03-18 17:13:50 +02:00
George Kiagiadakis
a79d002402 alsa: rename vm.type to cpu.vm.name to be consistent with pipewire master
See 7e9e261fa6
2024-03-18 17:03:44 +02:00
George Kiagiadakis
d0b7dde4e9 log: make the log topic flags an enumeration and publicly documented
Fixes: #591
2024-03-18 16:39:45 +02:00
George Kiagiadakis
8a893bdaf0 docs: add document on how to modify the configuration
Including information about the rules syntax.
Fixes: #595
2024-03-18 13:57:26 +02:00
George Kiagiadakis
59183e938a docs: update access configuration doc 2024-03-18 13:57:26 +02:00
George Kiagiadakis
0649ba9aa6 docs: remove "policy" configuration doc page
This is all part of the well-known settings now
2024-03-18 13:57:26 +02:00
George Kiagiadakis
7b918060c4 docs: improve bluetooth documentation and example config file 2024-03-18 13:57:26 +02:00
Ashok Sidipotu
4683f1fa44 wireplumber.conf: run stream-state.lua before m-standard-event-source
Restore-stream hook is missing some of the node-added events if it is run after
they are reported by the m-standard-event-source

Fixes: #577
2024-03-15 16:58:41 +00:00
Anders Jonsson
6e4dff6960 Update Swedish translation 2024-03-15 14:44:10 +01:00
George Kiagiadakis
e0192789e9 docs: configuration: update docs on "settings" after the latest changes 2024-03-13 18:11:49 +02:00
George Kiagiadakis
95cfa9e453 wpctl: fix settings --help listing
GOptionEntry arrays need to be NULL terminated, otherwise garbage
will be printed after the last entry...
2024-03-12 16:50:25 +02:00
George Kiagiadakis
857cee10cf docs: conf_file: small updates 2024-03-12 16:39:44 +02:00
George Kiagiadakis
1ebed7804c docs: installing: update dependency versions 2024-03-12 16:35:14 +02:00
George Kiagiadakis
7d217e37ce si-linkables: do not fully reset when the underlying proxy is destroyed
Reset back to the session item's "configured" state instead of clearing
the si properties completely. This allows the "session-item-removed"
event to be dispatched with all the original properties of the node
intact, for constraint matching purposes.

Fixes #588
2024-03-12 12:44:32 +02:00
George Kiagiadakis
ad743a2143 registry: move to a separate file and decouple it from the object manager
So that it can have its own log topic...
It also makes the code a bit easier to navigate.
2024-03-12 11:55:45 +02:00
George Kiagiadakis
8caf6a6271 log: docs: document the log topic definition macros 2024-03-12 11:55:45 +02:00
Julian Bouzas
f6fede9ee4 monitors/bluez: add 'internal' prefix to internal bluez node names.
And name the loopback source node the same as bluez source without 'internal'
prefix. This keeps consistency with input/output node names when switching
bluetooth profiles.
2024-03-11 13:49:01 -04:00
Julian Bouzas
5f9b3a9659 monitor/bluez: set node.name property when creating combine stream 2024-03-11 13:41:23 -04:00
Julian Bouzas
9caa44cfab meson: bump min pipewire version to 1.0.2
This is because of using 'api.bluez5.internal' property in bluez.lua.

See !589
2024-03-11 12:07:18 -04:00
Julian Bouzas
5e19722491 scripts: fix regression in state-routes.lua when marking routes as 'active'
Like WirePlumber 0.4.17, we need to mark the current routes as 'active' if they
were previously not active as soon as we detect it. This avoids a possible
infinite loop that restores the routes and saves them constantly, which happens
when the device's Route param has changed more than once before the event
'select-routes' is triggered.
2024-03-11 09:47:31 +00:00
Julian Bouzas
bed3b62e0d scripts: improve linking logs 2024-03-11 09:47:31 +00:00
Ashok Sidipotu
88f893e2ce monitors: use parseBool for boolean properties in rules
The boolean values of properties in rules are strings in JSON config files and
they will retain the same type when they are translated to Lua.
Use cutils.parseBool() function when they have to be interpreted as bools.

Fixes: #586
2024-03-11 07:19:27 +00:00
Ashok Sidipotu
08ae195cdb config: add {device|node}.disable 2024-03-11 07:19:27 +00:00
George Kiagiadakis
dee7403f69 object-interest: make WP_INTEREST_MATCH_ALL part of the enum
This is to avoid potential issues with g-i parsing.

See #540
2024-03-09 17:26:52 +02:00
George Kiagiadakis
6ed05b3f00 proxy: make the FEATURES_MINIMAL and FEATURES_ALL constants part of the enum
This fixes their parsing by g-i, correcting their values in the bindings,
which were previously wrong.

Fixes #540
2024-03-09 17:25:03 +02:00
George Kiagiadakis
b106b774f8 log: fix WP_LOG_LEVEL_TRACE value in the g-i bindings
See #540
2024-03-09 17:23:32 +02:00
George Kiagiadakis
f4d8fa94d7 base-dirs: wrap flag groups in parenthesis
The absence of parenthesis confuses the gobject-introspection parser
for some reason, making it emit WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER
multiple times in the .gir file
2024-03-09 16:30:15 +02:00
George Kiagiadakis
e9d8eeedef log.h: define G_LOG_DOMAIN only if WP_USE_LOCAL_LOG_TOPIC_IN_G_LOG is defined
Define WP_USE_LOCAL_LOG_TOPIC_IN_G_LOG in project scope, so that we always
use this feature in our codebase without causing problems for other projects.

Fixes #571
2024-03-09 15:58:01 +02:00
George Kiagiadakis
d8b1efcba7 meson: make sure the boolean options have boolean values
meson 1.1.0 has deprecated the use of strings for boolean options
2024-03-09 15:58:01 +02:00
George Kiagiadakis
3fa5228d22 meson: move the common CFLAGS to project-wide scope 2024-03-09 15:58:01 +02:00
Julian Bouzas
d0f16e4757 scripts: make sure target is not nil when iterating filters with matching targets 2024-03-08 10:56:34 -05:00
George Kiagiadakis
f24edf67fa docs: update the documentation around file search locations 2024-03-07 19:58:52 +02:00
Julian Bouzas
22de7513c1 scripts: rescan linkables when device EnumRoute param changes
This is needed for some devices that expose both Headset and Speaker nodes, so
that the applications are automatically linked to the Headset node or Speakers
node automatically when plugging and unplugging a headset.
2024-03-06 10:46:50 -05:00
Julian Bouzas
e496222a03 scripts: fix available routes check when selecting the default node
Also reevaluates default nodes when the device params changed.
2024-03-06 10:46:50 -05:00
George Kiagiadakis
a141ec0c68 scripts: fix typo in rescan-virtual-links.lua 2024-03-05 16:34:11 +02:00
George Kiagiadakis
2249d8d9df 0.4.90 (0.5.0~rc1) 2024-03-04 19:29:09 +02:00
Julian Bouzas
9da732d88c m-mixer-api: use gboolean instead of bool
The WpSpaPodParser always expects gboolean for boolean values. We should never
use bool in WirePlumber unless strictly necessary.

Fixes #584
2024-03-04 17:22:03 +00:00
George Kiagiadakis
9c3aa5409e core: close the configuration file after loading all components
All components are supposed to read the configuration file during
initialization. After that, the file is not needed anymore.
2024-03-04 16:33:14 +00:00
George Kiagiadakis
6321ff9f62 scripts: access: cache the access.rules in a global config variable 2024-03-04 16:33:14 +00:00
George Kiagiadakis
655a24acf0 scripts: remove cutils.evaluateRulesApplyProperties()
Cache the rules in a global variable in each script, as JSON,
and use JsonUtils directly to evaluate them. This will allow us to
close the WpConf in the future after loading the scripts.

Also change the order of the return values of the match_rules_apply_properties
function to be able to easily ignore the number of changed values,
which is useless in most cases.
2024-03-04 16:33:14 +00:00
George Kiagiadakis
3fbf1286e6 lua: change the Conf API to have methods for getting sections as specific types
In some cases we need to get a section as JSON, so that we can pass it
down to the rules parser, while in other cases we neeed to get it as a
table to use it natively, and in that case we even need to differentiate
between it being an object, an array or an object with WpProperties.

Make it also possible to optionally pass tables with default values to
the functions so that we can get rid of cutils.get_config_section()
as well.
2024-03-04 16:33:14 +00:00
Julian Bouzas
91b5ba5e92 meson: bump min pipewire version to 0.3.82 2024-03-04 16:13:47 +00:00
George Kiagiadakis
acb446d26e meson: fix typo in lib/wp/meson.build: @0 -> @0@ 2024-03-04 17:34:21 +02:00
Julian Bouzas
00e5c0d7f8 settings: log warnings if setting does not exist in the schema 2024-03-04 09:03:51 -05:00
Julian Bouzas
ee366446b6 conf: fix settings schema typos
Fixes #583
2024-03-04 09:03:51 -05:00
George Kiagiadakis
4b0024ed27 base-dirs: add missing (nullable) annotation 2024-03-04 07:07:56 +00:00
George Kiagiadakis
8040967e47 base-dirs: ensure we skip non-absolute paths in the XDG env variables
The XDG basedir spec explicitly says non-absolute paths must be ignored
and the glib wraper functions don't check that.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
a873f47d2e base-dirs: tidy up the build-time base dirs and honor the SUBDIR_WIREPLUMBER flag
Add a new private header file, wpbuildbasedirs.h, that contains the
build-time base directories passed directly from meson, without
the "wireplumber" suffix.

Use this to set the WP_BASE_DIRS_BUILD_* and adjust the code to honor
the SUBDIR_WIREPLUMBER flag.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
ad4c6999e8 base-dirs: add a SUBDIR_WIREPLUMBER flag to append "/wireplumber" to the base dirs
This removes the previous hardcoding of this suffix and allows the
functions to be useful to other projects that use libwireplumber
and want to use this code to locate their data & config.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
e7484c16a3 base-dirs: rename ETC & PREFIX* flags to BUILD* and improve documentation
The directories specified by these flags are build-time constants,
so the BUILD* prefix is more appropriate.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
14daab576b docs: add base-dirs in the generated C API documentation 2024-03-04 07:07:56 +00:00
George Kiagiadakis
c841ec97a8 conf: drop all the _get_value() functions and remove the fallback from _get_section()
We do not use these APIs, so there's no point in keeping them.

Realistically, every component that needs a section just does its
own parsing on it, so the _get_value() functions are not needed.

The fallback in _get_section() is also not needed, as we always
pass NULL and then test for it. In Lua, however, it seems we are
using the fallback to return an empty object, so that getting
a section does not expand to multiple lines of code. For that reason,
I have kept the syntax there and implemented it in the bindings layer.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
4596b7162e conf: add a simple check for old format wireplumber.conf files 2024-03-04 07:07:56 +00:00
George Kiagiadakis
08df33d7a3 wireplumber.conf: document the main difference between context.modules and wp.components 2024-03-04 07:07:56 +00:00
George Kiagiadakis
3d5cee55d8 meson: install configuration files back in $wireplumber_data_dir 2024-03-04 07:07:56 +00:00
George Kiagiadakis
60382df63f conf: refactor configuration loading
Changes:

- Configuration files are no longer located by libpipewire,
  which allows us to control the paths that are being looked up.
  This is a requirement for installations where pipewire and
  wireplumber are built using different prefixes, in which case
  the configuration files of wireplumber end up being installed in
  a place that libpipewire doesn't look into...

- The location of conf files is now again $prefix/share/wireplumber,
  /etc/wireplumber and $XDG_CONFIG_HOME/wireplumber, instead of using
  the pipewire directories. Also, since the previous commits, we now
  also support $XDG_CONFIG_DIRS/wireplumber (typically /etc/xdg/wireplumber)
  and $XDG_DATA_DIRS/wireplumber for system-wide configuration.

- Since libpipewire doesn't expose the parser, we now also do the
  parsing of sections ourselves. This has the advantage that we can
  optimize it a bit for our use case.

- The WpConf API has changed to not be a singleton and it is a
  property of WpCore instead. The configuration is now expected
  to be opened before the core is created, which allows the caller
  to identify configuration errors in advance. By not being a singleton,
  we can also reuse the WpConf API to open other SPA-JSON files.

- WpConf also now has a lazy loading mechanism. The configuration
  files are mmap'ed and the various sections are located in advance,
  but not parsed until they are actually requested. Also, the sections
  are not copied in memory, unlike what happens in libpipewire. They
  are only copied when merging is needed.

- WpCore now disables loading of a configuration file in pw_context,
  if a WpConf is provided. This is to have complete control here.
  The 'context.spa-libs' and 'context.modules' sections are still
  loaded, but we load them in WpConf and pass them down to pw_context
  for parsing. If a WpConf is not provided, pw_context is left to load
  the default configuration file (client.conf normally).
2024-03-04 07:07:56 +00:00
George Kiagiadakis
64c233f3f4 spa-json: wrap the data instead of the spa_json* in _parser_get_json()
This allows the returned WpSpaJson object to be kept around
after the parser has advanced to the next token. The behaviour
of the _new_wrap() function is to wrap the underlying spa_json*
and it breaks as soon as the parser advances.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
ccdca1ffb4 tests/wp/spa-json: add "undefined" parser unit test 2024-03-04 07:07:56 +00:00
George Kiagiadakis
d07b6188e5 spa-json: add new "undefined" parser constructor
This allows reading non-standard JSON data that is not a valid JSON
object or array, but can be treated as such.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
9e77240b64 base-dirs: return NULL from lookup_dirs() when the searched path is absolute
This allows the files iterator to lookup in a specific directory
when the given path is absolute, similar to how pipewire behaves
for configuration files (e.g. /foo/bar/wireplumber.conf must
include /foo/bar/wireplumber.conf.d/*.conf).

This also allows improving the wp_base_dirs_find_file() structure to
avoid duplicated code and add a debug message easily.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
f76f45124e base-dirs: add support for finding modules and remove wp_get_module_dir()
This makes things more consistent and allows us also to add more
search paths in the future if necessary.

Also, stop using g_module_build_path() because it's deprecated and
build the path manually. This obviously is not portable to Windows/Mac
or other exotic platforms, but portability is not important at this
point. PipeWire is also not portable beyond Linux & BSD.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
c8feaad7a9 base-dirs: add XDG_CONFIG/DATA_DIRS and CONFIGURATION & DATA groups
This adds support for the system-wide locations for configuration and
data files, as defined by the XDG Base Directory Specification.

In addition, it adds two flag groups, CONFIGURATION and DATA, to the
base-dirs system, so that we don't have to hard-code the combinations
of flags everywhere.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
a95cbda846 base-dirs: change the function signatures to prefix with wp_base_dirs_*
... and fix the arguments order and the documentation
2024-03-04 07:07:56 +00:00
George Kiagiadakis
6ae8c254a0 base-dirs: move file lookup utils out of wp.{c,h} and into base-dirs.{c,h} 2024-03-04 07:07:56 +00:00
George Kiagiadakis
3dc837c370 WpLookupDirs: remove the flag for looking into G_TEST_SRCDIR
Instead, make it so that WIREPLUMBER_*_DIR environment variables can
contain a list of directories to look into. This is safer and,
as a bonus, allows for more control over the lookup directories.
Using the G_TEST_SRCDIR variable can cause problems for tests of other
projects that use libwireplumber and may also lead to unexpected
behavior by not being obvious that this causes wireplumber to skip
looking in its standard directories...

This also brings back WIREPLUMBER_CONFIG_DIR, which is going to be
needed again for the upcoming WpConf changes.
2024-03-04 07:07:56 +00:00
George Kiagiadakis
770028aad5 wp_new_files_iterator: refactor to behave like pipewire
Previously files would be sorted across all configuration dirs
so their filename was all that mattered, not the priority of the directory.

Ex. you could get:

- /etc/wireplumber/10-foo.conf
- /usr/share/wireplumber/20-bar.conf
- $XDG_CONFIG_HOME/wireplumber/30-baz.conf
- /usr/share/wireplumber/40-zzz.conf

This commit changes that so that it follows the hirerachy of the directories
first and then the order of the filenames, starting from the lowest priority
directory. So now for the same files you get:

- /usr/share/wireplumber/20-bar.conf
- /usr/share/wireplumber/40-zzz.conf
- /etc/wireplumber/10-foo.conf
- $XDG_CONFIG_HOME/wireplumber/30-baz.conf

In addition, the hash table is avoided, making things a bit more efficient
and the files are checked for G_FILE_TEST_IS_REGULAR

Shadowing of files still works the same, so in the above example
if /etc/wireplumber/30-baz.conf also exists, it is not returned
in the list, because it's shadowed by $XDG_CONFIG_HOME/wireplumber/30-baz.conf
2024-03-04 07:07:56 +00:00
George Kiagiadakis
28f9716eff wp_find_file: s/char/gchar/ in suffix argument 2024-03-04 07:07:56 +00:00
Julian Bouzas
db21eb5dec docs: fix documentation for WpMetadataItem 2024-03-04 06:33:04 +00:00
Piotr Drąg
c0f65f6dc3 Update Polish translation 2024-03-03 14:46:29 +01:00
Julian Bouzas
578b85584c settings: fix all coverity scan defects 2024-02-29 07:23:58 -05:00
Julian Bouzas
8935837cda scripts: remove settings-manager and use the Settings API
The settings manager is not needed anymore because the WpSettings Lua API
returns now the default value from the schema if the setting is not found.
2024-02-28 10:20:31 -05:00
Julian Bouzas
f18a8c5a35 wpctl: use WpSettings API instead of metadata in 'settings' sub-command
Also adds -r flag to reset settings to their default value.
2024-02-28 10:20:31 -05:00
Julian Bouzas
f2e7a41175 m-lua-scripting: complete Lua Settings API
This patch adds the new WpSettings API in Lua.
2024-02-28 10:20:26 -05:00
Julian Bouzas
138591c449 m-settings: load all settings in sm-metadata
If a setting is not in the configuration file, use its default value.
2024-02-28 10:20:26 -05:00
Julian Bouzas
424a8e5263 settings: add API to set, reset, save, delete and iterate settings
Also improves and refactor the settings unit test to test all the new API added.
2024-02-28 10:20:20 -05:00
Julian Bouzas
a492d23019 m-settings: add settings schema to metadata
This patch improves module-settings to load the settings schema into a
'schema-sm-settings' metadata for clients to know what values types and range
are accepted for each particular setting. This settings schema is defined in
the wireplumber.conf, under a new section called 'wireplumber.settings.schema'.
2024-02-28 08:15:21 -05:00
Julian Bouzas
a23248847a metadata: remove wp_metadata_iterator_item_extract() API
Similar to WpPropertiesItem, this implements a new WpMetadataItem type that is
returned when iterating metadata
2024-02-28 08:15:17 -05:00
Julian Bouzas
bebfc07d84 wpctl: add settings subcomand to show, delete or change settings 2024-02-14 12:04:41 -05:00
George Kiagiadakis
5826a21456 0.4.82 (0.5.0 pre-release 2) 2024-02-14 18:40:30 +02:00
George Kiagiadakis
4dc7317010 docs: update ALSA documentation 2024-02-14 15:58:19 +02:00
Ashok Sidipotu
89b9218031 device-profile-hooks: add a hook to prioritize the profiles 2024-02-12 13:15:02 +05:30
Ashok Sidipotu
112a45a230 device-profile-hooks: move the selected profile check
move the check to the beginning of the hook.
2024-02-12 13:15:02 +05:30
Ashok Sidipotu
ad8d7aaf75 json-utils: correct typo 2024-02-12 13:15:02 +05:30
George Kiagiadakis
475ec4944d lib/settings: make the WpSettings object a non-singleton
This doesn't need to be a singleton, since we have the core registration
API available publicly nowadays. Makes things more clean for the API,
following the pattern of WpPlugin and WpSiFactory and simplifies the
built-in settings component in the internal component loader :)
2024-02-10 17:48:23 +02:00
George Kiagiadakis
d61bf89969 lib/settings: reorder functions to follow the pattern of other files 2024-02-10 16:42:15 +02:00
George Kiagiadakis
4d33aff107 monitors: improve notice messages about missing SPA plugins 2024-02-10 11:44:19 +02:00
George Kiagiadakis
99ee41c490 README: remove broken badge 2024-02-10 11:44:19 +02:00
Julian Bouzas
b703c01d4c wpctl: show persistent settings and add sub-command to clear them 2024-02-07 11:53:01 -05:00
Julian Bouzas
22f51336aa module-settings: don't remove setting from sm-settings if it was removed from persistent-sm-settings
Similar to 'default.audio.sink', a setting from sm-settings should never be
removed when the associated persistent setting is removed. Only settings from
persistent-sm-settings can be removed, like 'default.configured.audio.sink'.
2024-02-07 11:43:42 -05:00
Julian Bouzas
9bf646aed0 wpctl: show filters in the status output 2024-02-07 11:43:37 -05:00
Stefan Ursella
ac508aef57 linking: handle 'node.linger' property when target node not known
Do not send an error to the client when the target is not defined
and the 'node.linger' property is set.

It is not absolutely necessary that every node has a defined target.

We can have a 'Stream/Output/*' node which can be linked to
multiple 'Stream/Input/*' nodes and only the 'Stream/Input/*'
nodes have a defined target.
2024-02-04 17:33:46 +01:00
Julian Bouzas
1d2fe9b62d module-settings: remove 'settings.persistent' option
This patch removes the 'settings.persistent' option from the configuration as it
only allowed making settings persistent globally instead of individually. This
issue has been addressed in a simpler way by creating a 'persistent-sm-settings'
metadata. If a user wants to make a setting persistent, he can change the
'persistent-sm-settings' metadata object, if the user does not want to make a
persistent change, he can use the 'sm-settings' metadata object. Any changes in
the 'persistent-sm-settings' metadata will be also reflected in the 'sm-settings'
metadata object.
2024-02-02 14:02:49 -05:00
Julian Bouzas
b3a71e3f1c linking: rename props to 'node.dont-fallback', 'node.dont-move' and 'node.linger'
This makes those properties more consistent with 'node.dont-reconnect'.
2024-02-02 11:15:31 +00:00
Julian Bouzas
1c46115433 docs: add linking documentation 2024-02-02 11:15:31 +00:00
George Kiagiadakis
7a13189ce4 tests/wp/events.c: replace g_assert() with g_assert_true()
Fixes: #565
2024-02-01 17:45:44 +02:00
Julian Bouzas
d01441ca0a scripts: move filter-forward-format.lua from 'linking' to 'node' subdirectory
This is because 'filter-forward-format.lua' only configures nodes, and therefore
does not have anything to do with the linking logic. The setting has also been
renamed to 'node.filter.forward-format'.
2024-02-01 10:00:22 -05:00
George Kiagiadakis
32f86e38ad docs: improve smart filters documentation 2024-01-31 16:12:17 +02:00
Pauli Virtanen
6d9205cfe0 filter-utils: fix indexing nil value
Check target is not nil, which sometimes occurs, before trying to access
its .id property.

Fixes lua errors in logs.
2024-01-31 09:04:54 +00:00
George Kiagiadakis
c6e3dbf887 scripts/linking/rescan.lua: fix log access
The log object should be used with : in order to make sure that the
log handler has the correct topic in context.
2024-01-31 11:00:37 +02:00
Julian Bouzas
5581a9c2b7 scripts: only log session item Id when unhandling it
This is because the associated node does not exist anymore. This change also
logs the session item Id when handling it, so that it is easy to know if the
unhandled session item was handled or not.
2024-01-30 11:51:17 -05:00
George Kiagiadakis
2fcd24b2d3 docs: move smart filters documentation to the policies section 2024-01-30 12:13:16 +02:00
George Kiagiadakis
052ca9b4a7 docs: add policies section 2024-01-30 12:07:25 +02:00
Julian Bouzas
9cdb8f3110 bluez.lua: always hide sco-source nodes from applications
Since the loopback bluetooth source node is meant to be always used by
applications, this change hides the actual bluez sco-source node by marking
them as internal. This avoids showing 2 input devices in pavucontrol per BT
device if HSP/HFP profile is enabled.
2024-01-29 12:11:45 -05:00
George Kiagiadakis
cc4549134a script-tester: load the new settings-instance component 2024-01-29 12:57:35 +02:00
George Kiagiadakis
74bfe4baa3 script-tester: simplify the load_component() function 2024-01-29 12:56:46 +02:00
George Kiagiadakis
58b58b4b48 docs: reorder menu items under "Configuration" section
Also remove "main.rst", which is to be removed entirely as a file
(but only after its contents are all moved to other files).
2024-01-29 11:55:18 +02:00
George Kiagiadakis
ef24f35ff8 docs: rename settings section and add link back to the config option types 2024-01-29 11:55:18 +02:00
George Kiagiadakis
e19839dfce docs: add link from the well-known features section back to components & profiles 2024-01-29 11:55:18 +02:00
George Kiagiadakis
d5926b08e0 docs: add info about the different configuration option types 2024-01-29 11:55:18 +02:00
George Kiagiadakis
8b4885d21d conf: split out unneeded example sections to log.conf and settings.conf fragments 2024-01-29 11:55:18 +02:00
George Kiagiadakis
bdc7839ff2 m-settings: rename persistent.settings to settings.persistent
This makes it more consistent with the nomenclature of other settings
2024-01-29 11:55:18 +02:00
George Kiagiadakis
4c94cee54c docs: update multi-instance documentation 2024-01-29 11:55:18 +02:00
George Kiagiadakis
a511c54c5c m-settings: split out the WpSettings instance loading to a new built-in component
When running multi-instance setups or when clients like wpctl want to
access the WpSettings instance, it makes no sense to load the entire
module-settings, which will also create sm-settings metadata instances.
2024-01-29 11:55:18 +02:00
Julian Bouzas
95ae88d3e7 bluez.lua: set the loopback input stream media class to internal
This hides the loopback stream node from applications, making desktop
environments to not show the 'recording from microphone' icon (Eg gnome-panel)
when the bluetooth device is not recording yet.
2024-01-23 11:59:57 -05:00
Robert Rosengren
2e9e3a7564 link/node: Fix docs on state_changed_callback
Docs for state_changed_callback indicates state being emited as pointer,
but implementation emits it as enum value.
2024-01-22 13:45:15 +01:00
Julian Bouzas
874a432c69 autoswitch-bluetooth-profile: remove applications array and use loopback filter
This patch improves the bluetooth profile autoswitch so that it works with any
application that wants to capture from a bluetooth device. To do so, a loopback
source filter is created per connected bluetooth device. If an application wants
to capture audio from such loopback source filter, the profile in the associated
bluetooth device is changed to HSP/HFP. If there isn't any application connected
to the loopback source filter, the profile switches back to A2DP.
2024-01-22 10:15:16 +00:00
Julian Bouzas
68c6fc2a38 filter-utils: always convert to string when checking if target rules match
This fixes the target not being found when setting non-string values in the
JSON matching rules of the 'filter.smart.target' property.
2024-01-22 10:15:16 +00:00
Julian Bouzas
caded6070d linking: remove redundant 'dont_move' parameter
The 'target.dont_move' property is only meant to be used with 'target.object'
metadata property, not smart filters metadata properties. This was probably
left accidentaly unused when designing a solution for #524 involving smart
filters.

Fixes #558
2024-01-19 13:29:02 -05:00
Pauli Virtanen
6f3eb32937 config: update example bluetooth.conf
bluez5.auto-connect is by default disabled. device.profile does not
exist. MIDI nodes also have node.latency-offset-msec
2024-01-14 21:26:06 +02:00
Pauli Virtanen
b932e22849 docs: update Bluetooth docs 2024-01-14 21:23:17 +02:00
George Kiagiadakis
d17c99f63f docs: update locations.rst 2024-01-13 18:49:04 +02:00
Pauli Virtanen
d8a345a30c log: rename back to wp_log_set_level 2024-01-13 16:33:05 +00:00
Pauli Virtanen
82df32b0b0 m-lua-scripting: register/unregister log topics 2024-01-13 16:33:05 +00:00
Pauli Virtanen
35c10181d7 log: forward log level patterns also to libpipewire
Use pw_log_set_level_string to set log topic levels also for libpipewire
topics.
2024-01-13 16:33:05 +00:00
Pauli Virtanen
c746e18350 log: support topic patterns also in config file log.level
Handle log topic patterns also in wp_log_set_global_level, so that they
can be specified everywhere.
2024-01-13 16:33:05 +00:00
Pauli Virtanen
7c28c226b8 log: allow dynamic log levels for WpLogTopic
Change design of WpLogTopic so that it allows for changing their log
levels at runtime.

Add wp_log_topic_register/unregister functions to track lifetime of the
topics.

Auto-register statically defined log topics.

Make the topic registration threadsafe, in case it is done from
constructors that run in a different thread than main thread.
2024-01-13 16:33:05 +00:00
Sergio Costas
2ec202dfa1 client access: add support for snap permissions
This patch adds to wireplumber code to manage the Snap audio
permissions.

SNAP containers have two main "audio" rules:

 * audio-playback: the applications inside the container can
   send audio samples into a sink
 * audio-record: the applications inside the container can
   get audio samples from a source

Also, old SNAP containers had the "pulseaudio" rule, which just
exposed the pulseaudio socket directly, without limits. This
is similar to the current Flatpak audio permissions.

In the pulseaudio days, an specific pulseaudio module was used
that checked the permissions given to the application and
allowed or forbide access to the pulseaudio operations.
With the change to pipewire, this functionality must be
implemented in pipewire-pulse and wireplumber to guarantee
the sandbox security.

The current code checks for the presence of the pipewire.snap.id
property in a client, in which case it will read the
pipewire.snap.audio.playback and pipewire.snap.audio.record
properties, and allow or deny access to that client to
the nodes with Audio/Sink or Audio/Source media.class
property.

See !567 and pipewire!1779
2024-01-13 16:18:13 +00:00
Julian Bouzas
598b3c83ce filter-utils: handle new 'filter.smart.targetable' property
This property indicates whether the filter can be directly linked with clients
that have a defined target (Eg: pw-play --target <filter-name>) or not. This can
be useful when a client wants to be linked with a filter that is in the middle
of the chain in order to bypass the filters that are placed before the selected
one. If the property is not set, wireplumber will consider the filter not
targetable by default, meaning filters will never by bypassed by clients, and
clients will always be linked with the first filter in the chain.

Fixes #554
2024-01-11 10:54:41 -05:00
Julian Bouzas
cdeac07814 linking: handle defined target properly with smart filters
This patch fixes the policy to not link the client to the default filter if the
client's defined target is found, is not a filter, does not have any other
filters linked with it. In this case, the client is therefore linked to the
actual defined target. On the other hand, if the client's defined target is a
filter, the client is linked to the first filter in the chain that has the same
target as the defined filter's target.
2024-01-11 10:48:27 -05:00
Julian Bouzas
c37f95169d filter-utils: improve get_filter_from_target API to also work with filters 2024-01-11 10:34:13 -05:00
Piotr Drąg
42f4fa92b3 Update POTFILES.in 2024-01-08 10:36:34 +00:00
George Kiagiadakis
c5c5317599 state-routes: use the correct device id when restoring route properties
Fixes: #551
2024-01-08 12:28:21 +02:00
George Kiagiadakis
7bd4032a28 lua: hooks: record the entire 'after' array
Previously we were ignoring the last element in the 'after' arrays
because of a wrong loop limit
2024-01-08 12:28:21 +02:00
George Kiagiadakis
4a3557d8e4 event: add some trace logs to debug sorting hook dependencies 2024-01-08 12:28:21 +02:00
George Kiagiadakis
03098c88bb bt-pinephone.lua: remove useless variable assignments
This is a copy/paste from the code that now lives in apply-routes.lua,
but there is no point since the route here is a real route param and
not a route_info structure.
2024-01-08 12:28:21 +02:00
George Kiagiadakis
0b0595a156 apply-routes.lua: rename variable to make the code easier to understand 2024-01-08 12:28:21 +02:00
George Kiagiadakis
a7d0dacd12 state-routes: fix restoring volumes when a Route is changed manually
When a manual change is applied, the store-or-restore-routes is the only
hook that gets executed for this event. In order to be able to test if a
route was changed, we need to reset all of them to 'active = false',
record the previous value of 'active' and test their difference.

Testing only the 'active' variable (and discarding 'prev_active') sounds
tempting here, but if a route is changed back and forth (from A to B
and back to A), there is nothing to reset the 'active' variable of A
when it gets deactivated and it will appear as previously active on
the second switch.

Related to: #551
2024-01-08 12:21:27 +02:00
Julian Bouzas
d60747f40f docs: remove obsolete tags 2024-01-05 09:34:35 -05:00
Julian Bouzas
4ac7dc831e event-dispatcher: fix ingroup documentation 2024-01-05 09:34:35 -05:00
Willow Barraco
cdd472713d
docs-policy: add filter.smart = true 2024-01-05 10:11:00 +01:00
315 changed files with 25749 additions and 8400 deletions

View file

@ -1,6 +1,15 @@
# Create merge request pipelines for open merge requests, branch pipelines
# otherwise. This allows MRs for new users to run CI, and prevents duplicate
# pipelines for branches with open MRs.
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
stages:
- container
- container_coverity
- build
- analysis
- pages
@ -10,8 +19,8 @@ variables:
# change to build against a different tag or branch of pipewire
PIPEWIRE_HEAD: 'master'
# ci-templates as of Mar 24th 2023
.templates_sha: &templates_sha dd90ac0d7a03b574eb4f18d7358083f0c97825f3
# ci-templates as of Feb 14th 2025
.templates_sha: &templates_sha ef5e4669b7500834a17ffe9277e15fbb6d977fff
include:
- project: 'freedesktop/ci-templates'
@ -27,8 +36,8 @@ include:
.fedora:
variables:
# Update this tag when you want to trigger a rebuild
FDO_DISTRIBUTION_TAG: '2023-03-24.1'
FDO_DISTRIBUTION_VERSION: '37'
FDO_DISTRIBUTION_TAG: '2025-03-05.1'
FDO_DISTRIBUTION_VERSION: '41'
# findutils: used by the .build script below
# dbus-devel: required by pipewire
# dbus-daemon: required by GDBus unit tests
@ -80,8 +89,8 @@ include:
.alpine:
variables:
# Update this tag when you want to trigger a rebuild
FDO_DISTRIBUTION_TAG: '2023-03-24.1'
FDO_DISTRIBUTION_VERSION: '3.15'
FDO_DISTRIBUTION_TAG: '2025-03-05.1'
FDO_DISTRIBUTION_VERSION: '3.21'
FDO_DISTRIBUTION_PACKAGES: >-
dbus
dbus-dev
@ -112,14 +121,23 @@ include:
tar xf /tmp/cov-analysis-linux64.tgz ;
mv cov-analysis-linux64-* coverity ;
rm /tmp/cov-analysis-linux64.tgz
only:
variables:
- $COVERITY
.not_coverity:
except:
variables:
- $COVERITY
.rules_on_success_except_coverity:
rules:
- if: $COVERITY
when: never
- when: on_success
.rules_only_on_coverity:
rules:
- if: $COVERITY
.rules_only_on_mr_and_branch:
rules:
- if: $COVERITY
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH != "master"
.build:
before_script:
@ -136,13 +154,22 @@ include:
# Fedora also ships that, but without the test plugins that we need...
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
https://gitlab.freedesktop.org/pipewire/pipewire.git
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
-Dvulkan=disabled -Dgstreamer=disabled -Dsystemd=disabled
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
-Decho-cancel-webrtc=disabled -Dsession-managers=[]
# Set build options based on PipeWire version
- |
case "$PIPEWIRE_HEAD" in
1.0|1.2|1.4)
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
;;
*)
export PIPEWIRE_BUILD_OPTIONS="-Dlibsystemd=disabled"
;;
esac
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" $PIPEWIRE_BUILD_OPTIONS
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled
-Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled
-Dgstreamer=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled
-Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled
-Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[]
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
# misc environment only for wireplumber
@ -164,6 +191,7 @@ include:
container_fedora:
extends:
- .rules_on_success_except_coverity
- .fedora
- .fdo.container-build@fedora
stage: container
@ -172,6 +200,7 @@ container_fedora:
container_ubuntu:
extends:
- .rules_only_on_mr_and_branch
- .ubuntu
- .fdo.container-build@ubuntu
stage: container
@ -180,6 +209,7 @@ container_ubuntu:
container_alpine:
extends:
- .rules_only_on_mr_and_branch
- .alpine
- .fdo.container-build@alpine
stage: container
@ -188,17 +218,18 @@ container_alpine:
container_coverity:
extends:
- .rules_only_on_coverity
- .fedora
- .coverity
- .fdo.container-build@fedora
stage: container_coverity
stage: container
variables:
GIT_STRATEGY: none
build_on_fedora_with_docs:
extends:
- .rules_on_success_except_coverity
- .fedora
- .not_coverity
- .fdo.distribution-image@fedora
- .build
stage: build
@ -207,18 +238,21 @@ build_on_fedora_with_docs:
build_on_fedora_no_docs:
extends:
- .rules_only_on_mr_and_branch
- .fedora
- .not_coverity
- .fdo.distribution-image@fedora
- .build
stage: build
variables:
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
parallel:
matrix:
- PIPEWIRE_HEAD: ['master', '1.4', '1.2', '1.0']
build_on_ubuntu_with_gir:
extends:
- .rules_only_on_mr_and_branch
- .ubuntu
- .not_coverity
- .fdo.distribution-image@ubuntu
- .build
stage: build
@ -227,8 +261,8 @@ build_on_ubuntu_with_gir:
build_on_ubuntu_no_gir:
extends:
- .rules_only_on_mr_and_branch
- .ubuntu
- .not_coverity
- .fdo.distribution-image@ubuntu
- .build
stage: build
@ -237,8 +271,8 @@ build_on_ubuntu_no_gir:
build_on_alpine:
extends:
- .rules_only_on_mr_and_branch
- .alpine
- .not_coverity
- .fdo.distribution-image@alpine
- .build
stage: build
@ -247,6 +281,7 @@ build_on_alpine:
build_with_coverity:
extends:
- .rules_only_on_coverity
- .fedora
- .coverity
- .fdo.suffixed-image@fedora
@ -276,16 +311,23 @@ build_with_coverity:
shellcheck:
extends:
- .fedora
- .not_coverity
- .fdo.distribution-image@fedora
stage: analysis
script:
- shellcheck $(git grep -l "#\!/.*bin/.*sh")
rules:
- if: $COVERITY
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- "**/*.sh"
- if: $CI_COMMIT_BRANCH != "master"
changes:
- "**/*.sh"
linguas_check:
extends:
- .fedora
- .not_coverity
- .fdo.distribution-image@fedora
stage: analysis
script:
@ -294,10 +336,17 @@ linguas_check:
- ls *.po | sed s/.po//g | sort > LINGUAS.new
- diff -u LINGUAS.sorted LINGUAS.new
- rm -f LINGUAS.*
rules:
- if: $COVERITY
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- po/*
- if: $CI_COMMIT_BRANCH != "master"
changes:
- po/*
pages:
extends:
- .not_coverity
stage: pages
dependencies:
- build_on_fedora_with_docs
@ -307,5 +356,7 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $COVERITY
when: never
- if: $CI_COMMIT_BRANCH == "master"

37
AGENTS.md Normal file
View file

@ -0,0 +1,37 @@
## Building and Testing
- To compile the project: `meson compile -C build` (compiles everything, no target needed)
- To run tests: `meson test -C build`
- The build artifacts always live in a directory called `build` or `builddir`.
If `build` doesn't exist, use `-C builddir` in the meson commands.
## Git Workflow
- Main branch: `master`
- Always create feature branches for new work
- Use descriptive commit messages following project conventions
- Reference GitLab MR/issue numbers in commits where applicable
- Never commit build artifacts or temporary files
- Use `glab` CLI tool for GitLab interactions (MRs, issues, etc.)
## Making a release
- Each release always consists of an entry in NEWS.rst, at the top of the file, which describes
the changes between the previous release and the current one. In addition, each release is given
a unique version number, which is present:
1. on the section header of that NEWS.rst entry
2. in the project() command in meson.build
3. on the commit message of the commit that introduces the above 2 changes
4. on the git tag that marks the above commit
- In order to make a release:
- Begin by analyzing the git history and the merged MRs from GitLab between the previous release
and today. GitLab MRs that are relevant always have the new release's version number set as a
"milestone"
- Create a new entry in NEWS.rst describing the changes, in a similar style and format as the
previous entries. Consolidate the changes to larger work items and also reference the relevant
gitlab MR that corresponds to each change and/or the gitlab issues that were addressed by each
change.
- Make sure to move the "Past releases" section header up, so that the only 2 top-level sections
are the new release section and the "Past releases" section.
- Edit meson.build to change the project version to the new release number
- Do not commit anything to git. Let the user review the changes and commit manually.

654
NEWS.rst
View file

@ -1,5 +1,654 @@
WirePlumber 0.5.14
~~~~~~~~~~~~~~~~~~
Additions & Enhancements:
- Added per-device default volume configuration via the
``device.routes.default-{source,sink}-volume`` property, allowing device-specific volume
defaults (e.g. a comfortable default for internal speakers or no attenuation for HDMI) (!772)
- Added Lua 5.5 support; the bundled Lua subproject wrap has also been updated to 5.5.0
(!775, !788)
- Enhanced libcamera monitor to load camera nodes locally within the WirePlumber
process instead of the PipeWire daemon, eliminating race conditions that could occur
during initial enumeration and hotplug events (!790)
- Enhanced Bluetooth loopback nodes to always be created when a device supports both
A2DP and HSP/HFP profiles, simplifying the logic and making the BT profile autoswitch
setting take effect immediately without requiring device reconnection (!782)
- Enhanced Bluetooth loopback nodes to use ``target.object`` property instead of smart
filters, fixing issues that prevented users from setting them as default nodes and
also allowing smart filters to be used with them (#898; !792)
- Enhanced Bluetooth profile autoswitch logic with further robustness improvements,
including better headset profile detection using profile name patterns and resolving
race conditions by running profile switching after ``device/apply-profile`` in a
dedicated event hook (#926, #923; !776, !777, !808)
- Enhanced wpctl ``set-default`` command to accept virtual nodes (e.g.
``Audio/Source/Virtual``) in addition to regular device nodes (#896; !787)
- Improved stream linking to make the full graph rescan optional when linkable items
change, saving CPU on low-end systems and reducing audio startup latency when
connecting multiple streams in quick succession (!800)
- Allowed installation of systemd service units without libsystemd being present,
useful for distributions like Alpine Linux that allow systemd service subpackages
(!793)
- Allowed the ``mincore`` syscall in the WirePlumber systemd sandbox, required for
Mesa/EGL (e.g. for the libcamera GPUISP pipeline)
- Allowed passing ``WIREPLUMBER_CONFIG_DIR`` via the ``wp-uninstalled`` script,
useful for passing additional configuration paths in an uninstalled environment (!801)
Fixes:
- Removed Bluetooth sink loopback node, which was causing issues with KDE and GNOME (!794)
- Fixed default audio source selection to never automatically use ``Audio/Sink`` nodes
as the default source unless explicitly selected by the user (#886; !781)
- Fixed crash in ``state-stream`` when the Format parameter has a Choice for the
number of channels (#903; !795)
- Fixed BAP Bluetooth device set channel properties, where ``audio.position`` was
incorrectly serialized as a pointer address instead of the channel array (!786)
- Fixed memory leaks in ``wp_interest_event_hook_get_matching_event_types`` and in
the Lua ``LocalModule()`` implementation (!784, !810)
- Fixed HFP HF stream media class being incorrectly assigned due to
``api.bluez5.internal=true`` being set on HFP HF streams (!809)
- Fixed Lua 5.4 compatibility in ``state-stream`` script
- Updated translations: Bulgarian, Georgian, Kazakh, Swedish
Past releases
~~~~~~~~~~~~~
WirePlumber 0.5.13
..................
Additions & Enhancements:
- Added internal filter graph support for audio nodes, allowing users to
create audio preprocessing and postprocessing chains without exposing
filters to applications, useful for software DSP (!743)
- Added new Lua Properties API that significantly improves performance by
avoiding constant serialization between WpProperties and Lua tables,
resulting in approximately 40% faster node linking (!757)
- Added WpIterator Lua API for more efficient parameter enumeration (!746)
- Added bash completions for wpctl command (!762)
- Added script to find suitable volume control when using role-based policy,
allowing volume sliders to automatically adjust the volume of the currently
active role (e.g., ringing, call, media) (!711)
- Added experimental HDMI channel detection setting to use HDMI ELD
information for channel configuration (!749)
- Enhanced role-based policy to allow setting preferred target sinks for
media role loopbacks via ``policy.role-based.preferred-target`` (!754)
- Enhanced Bluetooth profile autoswitch logic to be more robust and handle
saved profiles correctly, including support for loopback sink nodes (!739)
- Enhanced ALSA monitor to include ``alsa.*`` device properties on nodes for
rule matching (!761)
- Optimized stream node linking for common cases to reduce latency when new
audio/video streams are added (!760)
- Improved event dispatcher performance by using hash table registration for
event hooks, eliminating performance degradation as more hooks are
registered (!765)
- Increased audio headroom for VMware and VirtualBox virtual machines (!756)
- Added setting to prevent restoring "Off" profiles via
``session.dont-restore-off-profile`` property (!753)
- Added support for 128 audio channels when compiled with a recent version of
PipeWire (pipewire#4995; CI checks in !768)
Fixes:
- Fixed memory leaks and issues in the modem manager module (!770, !764)
- Fixed MPRIS module incorrectly treating GHashTable as GObject (!759)
- Fixed warning messages when process files in ``/proc/<pid>/*`` don't exist,
particularly when processes are removed quickly (#816, !717)
- Fixed MONO audio configuration to only apply to device sink nodes, allowing
multi-channel mixing in the graph (!769)
- Fixed event dispatcher hook registration and removal to avoid spurious
errors (!747)
- Improved logging for standard-link activation failures (!744)
- Simplified event-hook interest matching for better performance (!758)
WirePlumber 0.5.12
..................
Additions & Enhancements:
- Added mono audio configuration support via ``node.features.audio.mono``
setting that can be changed at runtime with wpctl (!721)
- Added automatic muting of ALSA devices when a running node is removed,
helping prevent loud audio on speakers when headsets are unplugged (!734)
- Added notifications API module for sending system notifications (!734)
- Added comprehensive wpctl man page and documentation (!735, #825)
- Enhanced object interest handling for PipeWire properties on session items (!738)
Fixes:
- Fixed race condition during shutdown in the permissions portal module that
could cause crashes in GDBus signal handling (!748)
- Added device validity check in state-routes handling to prevent issues
when devices are removed during async operations (!737, #844)
- Fixed Log.critical undefined function error in device-info-cache (!733)
- Improved device hook documentation and configuration (!736)
WirePlumber 0.5.11
..................
Additions & Enhancements:
- Added modem manager module for tracking voice call status and voice call
device profile selection hooks to improve phone call audio routing on
mobile devices (!722, !729, #819)
- Added MPRIS media player pause functionality that automatically pauses
media playback when the audio target (e.g. headphones) is removed (!699, #764)
- Added support for human-readable names and localization of settings in
``wireplumber.conf`` with ``wpctl`` displaying localized setting descriptions (!712)
- Improved default node selection logic to use both session and route
priorities when nodes have equal session priorities (!720)
- Increased USB device priority in the ALSA monitor (!719)
Fixes:
- Fixed multiple Lua runtime issues including type confusion bugs, stack
overflow prevention, and SPA POD array/choice builders (!723, !728)
- Fixed proxy object lifecycle management by properly clearing the
OWNED_BY_PROXY flag when proxies are destroyed to prevent dangling
pointers (!732)
- Fixed state-routes handling to prevent saving unavailable routes and
eliminate race conditions during profile switching (!730, #762)
- Fixed some memory leaks in the script tester and the settings iterator (!727, !726)
- Fixed a potential crash caused by module-loopback destroying itself when the
pipewire connection is closed (#812)
- Fixed profile saving behavior in ``wpctl set-profile`` command (#808)
- Fixed GObject introspection closure annotation
WirePlumber 0.5.10
..................
Fixed a critical crash in ``linking-utils.haveAvailableRoutes`` that was
introduced accidentally in 0.5.9 and caused loss of audio output on affected
systems (#797, #799, #800, !713)
WirePlumber 0.5.9
.................
Additions & Enhancements:
- Added a new audio node grouping functionality using an external command line
tool (!646)
- The libcamera monitor now supports devices that are not associated with
device ids (!701)
- The wireplumber user systemd service is now associated with dbus.service to
avoid strange warnings when dbus exits (!702)
- Added "SYSLOG_IDENTIFIER", "SYSLOG_FACILITY", "SYSLOG_PID" and "TID" to log
messages that are sent to the journal (!709)
Fixes:
- Fixed a crash of ``wpctl set-default`` on 32-bit architectures (#773)
- Fixed a crash when a configuration component had no 'provides' field (#771)
- Reduced the log level of some messages that didn't need to be as high (!695)
- Fixed another nil reference issue in the alsa.lua monitor script (!704)
- Fixed name deduplication of v4l2 and libcamera devices (!705)
- Fixed an issue with wpctl not being able to save settings sometimes (!708, #749)
WirePlumber 0.5.8
.................
Additions & Enhancements:
- Added support for handling UCM SplitPCM nodes in the ALSA monitor, which
allows native PipeWire channel remapping using loopbacks for devices that
use this feature (!685)
- Introduced new functions to mark WpSpaDevice child objects as pending.
This allows properly associating asynchronously created loopback nodes with
their parent WpSpaDevice without losing ObjectConfig events (!687, !689)
- Improved the node name deduplication logic in the ALSA monitor to prevent
node names with .2, .3, etc appended to them in some more cases (!688)
- Added a new script to populate ``session.services``. This is a step towards
implementing detection of features that PipeWire can service (!686)
Fixes:
- Fixed an issue that was causing duplicate Bluetooth SCO (HSP/HFP) source
nodes to be shown in UIs (#701, !683)
- In the BlueZ monitor, marked the source loopback node as non-virtual,
addressing how it appears on UIs (#729)
- Disabled stream-restore for device loopback nodes to prevent unwanted
property changes (!691)
- Fixed ``wp_lua_log_topic_copy()`` to correctly copy topic names (#757)
- Updated script tests to handle differences in object identifiers
(``object.serial`` vs ``node.id``), ensuring proper test behavior (#761)
WirePlumber 0.5.7
.................
Highlights:
- Fixed an issue that would cause random profile switching when an application
was trying to capture from non-Bluetooth devices (#715, #634, !669)
- Fixed an issue that would cause strange profile selection issues [choices
not being remembered or unavailable routes being selected] (#734)
- Added a timer that delays switching Bluetooth headsets to the HSP/HFP
profile, avoiding needless rapid switching when an application is trying to
probe device capabilities instead of actually capturing audio (!664)
- Improved libcamera/v4l2 device deduplication logic to work with more complex
devices (!674, !675, #689, #708)
Fixes:
- Fixed two memory leaks in module-mixer-api and module-dbus-connection
(!672, !673)
- Fixed a crash that could occur in module-reserve-device (!680, #742)
- Fixed an issue that would cause the warning "[string "alsa.lua"]:182:
attempt to concatenate a nil value (local 'node_name')" to appear in the
logs when an ALSA device was busy, breaking node name deduplication (!681)
- Fixed an issue that could make find-preferred-profile.lua crash instead of
properly applying profile priority rules (#751)
WirePlumber 0.5.6
.................
Additions:
- Implemented before/after dependencies for components, to ensure correct
load order in custom configurations (#600)
- Implemented profile inheritance in the configuration file. This allows
profiles to inherit all the feature specifications of other profiles, which
is useful to avoid copying long lists of features just to make small changes
- Added multi-instance configuration profiles, tested and documented them
- Added a ``main-systemwide`` profile, which is now the default for instances
started via the system-wide systemd service and disables features that
depend on the user session (#608)
- Added a ``wp_core_connect_fd`` method, which allows making a connection to
PipeWire via an existing open socket (useful for portal-based connections)
Fixes:
- The Bluetooth auto-switch script now uses the common event source object
managers, which should improve its stability (!663)
- Fix an issue where switching between Bluetooth profiles would temporarily
link active audio streams to the internal speakers (!655)
WirePlumber 0.5.5
.................
Highlights:
- Hotfix release to address crashes in the Bluetooth HSP/HFP autoswitch
functionality that were side-effects of some changes that were part
of the role-based linking policy (#682)
Improvements:
- wpctl will now properly show a '*' in front of sink filters when they are
selected as the default sink (!660)
WirePlumber 0.5.4
.................
Highlights:
- Refactored the role-based linking policy (previously known also as
"endpoints" or "virtual items" policy) to blend in with the standard desktop
policy. It is now possible use role-based sinks alongside standard desktop
audio operations and they will only be used for streams that have a
"media.role" defined. It is also possible to force streams to have a
media.role, using a setting. Other features include: blending with smart
filters in the graph and allowing hardware DSP nodes to be also used easily
instead of requiring software loopbacks for all roles. (#610, !649)
Improvements:
- Filters that are not declared as smart will now behave again as normal
application streams, instead of being treated sometimes differently (!657)
Fixes:
- Fixed an issue that would cause WirePlumber to crash at startup if an
empty configuration file was present in one of the search paths (#671)
- Fixed Bluetooth profile auto-switching when a filter is permanently linked
to the Bluetooth source (!650)
- Fixed an issue in the software-dsp script that would cause DSP filters to
stay around and cause issues after their device node was destroyed (!651)
- Fixed an issue in the autoswitch-bluetooth-profile script that could cause
an infinite loop of switching between profiles (!652, #617)
- Fixed a rare issue that could cause WirePlumber to crash when dealing with
a device object that didn't have the "device.name" property set (#674)
WirePlumber 0.5.3
.................
Fixes:
- Fixed a long standing issue that would cause many device nodes to have
inconsistent naming, with a '.N' suffix (where N is a number >= 2) being
appended at seemingly random times (#500)
- Fixed an issue that would cause unavailable device profiles to be selected
if they were previously stored in the state file, sometimes requiring users
to manually remove the state file to get things working again (#613)
- Fixed an occasional crash that could sometimes be triggered by hovering
the volume icon on the KDE taskbar, and possibly other similar actions
(#628, !644)
- Fixed camera device deduplication logic when the same device is available
through both V4L2 and libcamera, and the libcamera one groups multiple V4L2
devices together (#623, !636)
- Fixed applying the default volume on streams that have no volume previously
stored in the state file (#655)
- Fixed an issue that would prevent some camera nodes - in some cases -
from being destroyed when the camera device is removed (#640)
- Fixed an issue that would cause video stream nodes to be linked with audio
smart filters, if smart audio filters were configured (!647)
- Fixed an issue that would cause WP to re-activate device profiles even
though they were already active (!639)
- Configuration files in standard JSON format (starting with a '{', among
other things) are now correctly parsed (#633)
- Fixed overriding non-container values when merging JSON objects (#653)
- Functions marked with WP_PRIVATE_API are now also marked as
non-introspectable in the gobject-introspection metadata (#599)
Improvements:
- Logging on the systemd journal now includes the log topic and also the log
level and location directly on the message string when the log level is
high enough, which is useful for gathering additional context in logs
submitted by users (!640)
- Added a video-only profile in wireplumber.conf, for systems where only
camera & screensharing are to be used (#652)
- Improved seat state monitoring so that Bluetooth devices are only enabled
when the user is active on a local seat, instead of allowing remote users
as well (!641)
- Improved how main filter nodes are detected for the smart filters (!642)
- Added Lua method to merge JSON containers (!637)
WirePlumber 0.5.2
.................
Highlights:
- Added support for loading configuration files other than the default
wireplumber.conf within Lua scripts (!629)
- Added support for loading single-section configuration files, without
fragments (!629)
- Updated the node.software-dsp script to be able to load filter-chain graphs
from external configuration files, which is needed for Asahi Linux audio
DSP configuration (!629)
Fixes:
- Fixed destroying camera nodes when the camera device is removed (#627, !631)
- Fixed an issue with Bluetooth BAP device set naming (!632)
- Fixed an issue caused by the pipewire event loop not being "entered" as
expected (!634, #638)
- A false positive warning about no modules being loaded is now suppressed
when using libpipewire >= 1.0.5 (#620)
- Default nodes can now be selected using priority.driver when
priority.session is not set (#642)
Changes:
- The library version is now generated following pipewire's versioning scheme:
libwireplumber-0.5.so.0.5.2 becomes libwireplumber-0.5.so.0.0502.0 (!633)
WirePlumber 0.5.1
.................
Highlights:
- Added a guide documenting how to migrate configuration from 0.4 to 0.5,
also available online at:
https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/migration.html
If you are packaging WirePlumber for a distribution, please consider
informing users about this.
Fixes:
- Fixed an odd issue where microphones would stop being usable when a
Bluetooth headset was connected in the HSP/HFP profile (#598, !620)
- Fixed an issue where it was not possible to store the volume/mute state of
system notifications (#604)
- Fixed a rare crash that could occur when a node was destroyed while the
'select-target' event was still being processed (!621)
- Fixed deleting all the persistent settings via ``wpctl --delete`` (!622)
- Fixed using Bluetooth autoswitch with A2DP profiles that have an input route
(!624)
- Fixed sending an error to clients when linking fails due to a format
mismatch (!625)
Additions:
- Added a check that prints a verbose warning when old-style 0.4.x Lua
configuration files are found in the system. (#611)
- The "policy-dsp" script, used in Asahi Linux to provide a software DSP
for Apple Sillicon devices, has now been ported to 0.5 properly and
documented (#619, !627)
WirePlumber 0.5.0
.................
Changes:
- Bumped the minimum required version of PipeWire to 1.0.2, because we
make use of the 'api.bluez5.internal' property of the BlueZ monitor (!613)
- Improved the naming of Bluetooth nodes when the auto-switching loopback
node is present (!614)
- Updated the documentation on "settings", the Bluetooth monitor, the Access
configuration, the file search locations and added a document on how to
modify the configuration file (#595, !616)
Fixes:
- Fixed checking for available routes when selecting the default node (!609)
- Fixed an issue that was causing an infinite loop storing routes in the
state file (!610)
- Fixed the interpretation of boolean values in the alsa monitor rules (#586, !611)
- Fixes a Lua crash when we have 2 smart filters, one with a target and one
without (!612)
- Fixed an issue where the default nodes would not be updated when the
currently selected default node became unavailable (#588, !615)
- Fixed an issue that would cause the Props (volume, mute, etc) of loopbacks
and other filter nodes to not be restored at startup (#577, !617)
- Fixed how some constants were represented in the gobject-introspection file,
mostly by converting them from defines to enums (#540, #591)
- Fixed an issue using WirePlumber headers in other projects due to
redefinition of G_LOG_DOMAIN (#571)
WirePlumber 0.4.90
..................
This is the first release candidate (RC1) of WirePlumber 0.5.0.
Highlights:
- The configuration system has been changed back to load files from the
WirePlumber configuration directories, such as ``/etc/wireplumber`` and
``$XDG_CONFIG_HOME/wireplumber``, unlike in the pre-releases. This was done
because issues were observed with installations that use a different prefix
for pipewire and wireplumber. If you had a ``wireplumber.conf`` file in
``/etc/pipewire`` or ``$XDG_CONFIG_HOME/pipewire``, you should move it to
``/etc/wireplumber`` or ``$XDG_CONFIG_HOME/wireplumber`` respectively (!601)
- The internal base directories lookup system now also respects the
``XDG_CONFIG_DIRS`` and ``XDG_DATA_DIRS`` environment variables, and their
default values as per the XDG spec, so it is possible to install
configuration files also in places like ``/etc/xdg/wireplumber`` and
override system-wide data paths (!601)
- ``wpctl`` now has a ``settings`` subcommand to show, change and delete
settings at runtime. This comes with changes in the ``WpSettings`` system to
validate settings using a schema that is defined in the configuration file.
The schema is also exported on a metadata object, so it is available to any
client that wants to expose WirePlumber settings (!599, !600)
- The ``WpConf`` API has changed to not be a singleton and support opening
arbitrary config files. The main config file now needs to be opened prior to
creating a ``WpCore`` and passed to the core using a property. The core uses
that without letting the underlying ``pw_context`` open and read the default
``client.conf``. The core also closes the ``WpConf`` after all components
are loaded, which means all the config loading is done early at startup.
Finally, ``WpConf`` loads all sections lazily, keeping the underlying files
memory mapped until it is closed and merging them on demand (!601, !606)
WirePlumber 0.4.82
..................
This is a second pre-release of WirePlumber 0.5.0, made available for testing
purposes. This is not API/ABI stable yet and there is still pending work to do
before the final 0.5.0 release, both in the codebase and the documentation.
Highlights:
- Bluetooth auto-switching is now implemented with a virtual source node. When
an application links to it, the actual device switches to the HSP/HFP
profile to provide the real audio stream. This is a more robust solution
that works with more applications and is more user-friendly than the
previous application whitelist approach
- Added support for dynamic log level changes via the PipeWire ``settings``
metadata. Also added support for log level patterns in the configuration
file
- The "persistent" (i.e. stored) settings approach has changed to use two
different metadata objects: ``sm-settings`` and ``persistent-sm-settings``.
Changes in the former are applied in the current session but not stored,
while changes in the latter are stored and restored at startup. Some work
was also done to expose a ``wpctl`` interface to read and change these
settings, but more is underway
- Several WirePlumber-specific node properties that used to be called
``target.*`` have been renamed to ``node.*`` to match the PipeWire
convention of ``node.dont-reconnect``. These are also now fully documented
Other changes:
- Many documentation updates
- Added support for SNAP container permissions
- Fixed multiple issues related to restoring the Route parameter of devices,
which includes volume state (#551)
- Smart filters can now be targetted by specific streams directly when
the ``filter.smart.targetable`` property is set (#554)
- Ported the mechanism to override device profile priorities in the
configuration, which is used to re-prioritize Bluetooth codecs
- WpSettings is no longer a singleton class and there is a built-in component
to preload an instance of it
WirePlumber 0.4.81
~~~~~~~~~~~~~~~~~~~
..................
This is a preliminary release of WirePlumber 0.5.0, which is made available
for testing purposes. Please test it and report feedback (merge requests are
@ -55,9 +704,6 @@ Highlights:
the same camera device, which can cause confusion when looking at the list
of available cameras in applications.
Past releases
~~~~~~~~~~~~~
WirePlumber 0.4.17
..................

View file

@ -7,9 +7,6 @@ WirePlumber
.. image:: https://scan.coverity.com/projects/21488/badge.svg
:alt: Coverity Scan Build Status
.. image:: https://img.shields.io/tokei/lines/gitlab.freedesktop.org/pipewire/wireplumber
:alt: Lines of code
.. image:: https://img.shields.io/badge/license-MIT-green
:alt: License

View file

@ -1240,15 +1240,6 @@ HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
# page will contain the date and time when the page was generated. Setting this
# to YES can help to show when doxygen was last run and thus if the
# documentation is up to date.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_TIMESTAMP = NO
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will
@ -1543,17 +1534,6 @@ HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
# generated for formulas are transparent PNGs. Transparent PNGs are not
# supported properly for IE 6.0, but are supported on all modern browsers.
#
# Note that when changing this option you need to delete any form_*.png files in
# the HTML output directory before the changes have effect.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
FORMULA_TRANSPARENT = YES
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
# to create new LaTeX commands to be used in formulas as building blocks. See
# the section "Including formulas" for details.
@ -1865,14 +1845,6 @@ LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
# page will contain the date and time when the page was generated. Setting this
# to NO can help when comparing the output of multiple runs.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_TIMESTAMP = NO
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the
@ -2265,23 +2237,6 @@ HAVE_DOT = YES
DOT_NUM_THREADS = 0
# When you want a differently looking font in the dot files that doxygen
# generates you can specify the font name using DOT_FONTNAME. You need to make
# sure dot is able to find the font, which can be done by putting it in a
# standard location or by setting the DOTFONTPATH environment variable or by
# setting DOT_FONTPATH to the directory containing the font.
# The default value is: Helvetica.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_FONTNAME = Helvetica
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
# dot graphs.
# Minimum value: 4, maximum value: 24, default value: 10.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_FONTSIZE = 10
# By default doxygen will tell dot to use the default font as specified with
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
# the path where dot can find it using this tag.
@ -2518,18 +2473,6 @@ DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
# background. This is disabled by default, because dot on Windows does not seem
# to support this out of the box.
#
# Warning: Depending on the platform used, enabling this option may lead to
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
# read).
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_TRANSPARENT = NO
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support

View file

@ -1,11 +1,15 @@
# -- Project information -----------------------------------------------------
project = 'WirePlumber'
copyright = '2021-2024, Collabora'
author = 'Collabora'
copyright = '2020-2025, Collabora & contributors'
author = 'The WirePlumber Developers'
release = '@VERSION@'
version = '@VERSION@'
# -- General configuration ---------------------------------------------------
smartquotes = False
# -- Breathe configuration ---------------------------------------------------
extensions = [
@ -43,3 +47,9 @@ html_css_files = ['custom.css']
graphviz_output_format = "svg"
pygments_style = "friendly"
# -- Options for manual page output -----------------------------------------
man_pages = [
('tools/wpctl', 'wpctl', 'WirePlumber Control CLI', ['The WirePlumber Developers'], 1)
]

View file

@ -130,7 +130,7 @@ class DoxygenProcess(object):
def __process_element(self, xml):
s = ""
if xml.text and re.search('\S', xml.text):
if xml.text and re.search(r'\S', xml.text):
s += xml.text
for n in xml.getchildren():
if n.tag == "emphasis":
@ -143,7 +143,7 @@ class DoxygenProcess(object):
s += " - " + self.__process_element(n)
if n.tag == "para":
p = self.__process_element(n)
if re.search('\S', p):
if re.search(r'\S', p):
s += p + "\n"
if n.tag == "ref":
s += n.text if n.text else ""
@ -168,7 +168,7 @@ class DoxygenProcess(object):
if n.tag == "htmlonly":
s += ""
if n.tail:
if re.search('\S', n.tail):
if re.search(r'\S', n.tail):
s += n.tail
if n.tag.startswith("param"):
pass # parameters are handled separately in DoxyFunction::from_memberdef()
@ -319,6 +319,8 @@ class DoxyFunction(DoxyElement):
d = normalize_text(d)
e = DoxyFunction(name, d)
if (xml.get("prot") == "private"):
e.extra = "(skip)"
e.add_brief(xml.find("briefdescription"))
e.add_detail(xml.find("detaileddescription"))
for p in xml.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):

View file

@ -102,6 +102,26 @@ if build_doc
install_dir: wireplumber_doc_dir,
build_by_default: true,
)
# Generate man pages directory with sphinx
custom_target('manpages',
command: [sphinx_p,
'-q', # quiet
'-E', # rebuild from scratch
'-b', 'man', # man page builder
'-d', '@PRIVATE_DIR@', # doctrees dir
'-c', '@OUTDIR@', # conf.py dir
'@CURRENT_SOURCE_DIR@/rst', # source dir
'@OUTDIR@', # output directory
],
depend_files: [
sphinx_conf, sphinx_files,
],
output: ['wpctl.1'],
install: true,
install_dir: get_option('mandir') / 'man1',
build_by_default: true,
)
endif
# Build GObject introspection

View file

@ -20,12 +20,11 @@ the various options available.
configuration/conf_file.rst
configuration/components_and_profiles.rst
configuration/configuration_option_types.rst
configuration/modifying_configuration.rst
configuration/migration.rst
configuration/features.rst
configuration/settings.rst
configuration/locations.rst
configuration/main.rst
configuration/multi_instance.rst
configuration/alsa.rst
configuration/bluetooth.rst
configuration/policy.rst
configuration/access.rst

View file

@ -3,56 +3,123 @@
Access configuration
====================
wireplumber.conf.d/access.conf
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
WirePlumber includes a "client access" policy which defines access control
rules for PipeWire clients.
Using a similar format as the :ref:`ALSA monitor <config_alsa>`, this
configuration file is charged to configure the client objects created by
PipeWire.
Rules
-----
* *Settings*
This policy can be configured with rules that can be used to match clients and
apply default permissions to them.
Example:
Example:
.. code-block::
.. code-block::
wireplumber.settings = {
access-enable-flatpak-portal = true
}
access.rules = [
{
matches = [
{
access = "flatpak"
media.category = "Manager"
}
]
actions = {
update-props = {
access = "flatpak-manager"
default_permissions = "all",
}
}
}
{
matches = [
{
access = "flatpak"
}
]
actions = {
update-props = {
default_permissions = "rx"
}
}
}
]
The above example sets to ``true`` the ``access-enable-flatpak-portal``
property.
Possible permissions are any combination of:
The list of valid properties are:
* ``r``: client is allowed to **read** objects, i.e. "see" them on the registry
and list their properties
* ``w``: client is allowed to **write** objects, i.e. call methods that modify
their state
* ``x``: client is allowed to **execute** methods on objects; the ``w`` flag
must also be present to call methods that modify the object
* ``m``: client is allowed to set **metadata** on objects
* ``l``: nodes of this client are allowed to **link** to other nodes that the
client can't "see" (i.e. the client doesn't have ``r`` permission on them)
.. code-block::
The special value ``all`` is also supported and it is synonym for ``rwxm``
access-enable-flatpak-portal = true,
Permission Managers
-------------------
Whether to enable the flatpak portal or not.
For more advanced use cases, WirePlumber supports *permission managers* that can
apply per-object permissions dynamically based on rules and object interests.
Permission managers are defined in the ``access.permission-managers`` section
and then referenced by name in ``access.rules``.
* *rules*
Example:
Example::
.. code-block::
access = [
{
matches = [
{
pipewire.access = "flatpak"
}
]
actions = {
update-props = {
default_permissions = "rx"
}
}
}
]
access.permission-managers = [
{
name = "custom"
default_permissions = "all"
core_permissions = "rx"
rules = [
{
matches = [
{
media.class = "Audio/Source"
}
]
actions = {
set-permissions = "-"
}
}
]
}
]
This grants read and execute permissions to all clients that have the
``pipewire.access`` property set to ``flatpak``.
access.rules = [
{
matches = [
{
application.name = "paplay"
}
]
actions = {
update-props = {
permission_manager_name = "custom"
}
}
}
]
Possible permissions are any combination of ``r``, ``w`` and ``x`` for read,
write and execute; or ``all`` for all kind of permissions.
Each permission manager supports the following properties:
* ``name``: (required) a unique name used to reference the manager from
``access.rules``
* ``default_permissions``: the fallback permissions applied to all objects
that don't match any rule (applied as ``PW_ID_ANY``)
* ``core_permissions``: permissions applied specifically to the PipeWire core
object (``PW_ID_CORE``, ID 0). This is useful when you want to allow a
client to interact with the core (e.g. enumerate objects, subscribe to
events) while restricting access to individual objects. If not set, the
``default_permissions`` value is used for the core as well.
* ``rules``: a list of match rules with ``set-permissions`` actions that
grant specific permissions to objects matching the given constraints
When both ``default_permissions`` and ``permission_manager_name`` are set in
a rule's ``update-props`` action, ``default_permissions`` takes precedence and
the permission manager is ignored.

View file

@ -3,384 +3,423 @@
ALSA configuration
==================
Modifying the default configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
One of the components of WirePlumber is the ALSA monitor. This monitor is
responsible for creating PipeWire devices and nodes for all the ALSA cards that
are available on the system. It also manages the configuration of these devices.
ALSA devices are created and managed by the session manager with the *alsa.lua*
monitor script. In the default configuration, this script is loaded by
``wireplumber.conf.d/alsa.conf``, which also specifies its settings and
rules.
The ALSA monitor is enabled by default and can be disabled using the
``monitor.alsa`` :ref:`feature <config_features>` in the configuration file.
* *Settings*
Example:
.. code-block::
wireplumber.settings = {
alsa_monitor.alsa.jack-device = true
alsa_monitor.alsa.reserve = true
}
The above example will configure the ALSA monitor to not enable the JACK
device, and do ALSA device reservation using the mentioned DBus interface.
A list of valid settings are:
.. code-block::
alsa_monitor.alsa.jack-device = true
Creates a JACK device if set to ``true``. This is not enabled by default
because it requires that the PipeWire JACK replacement libraries are not used
by the session manager, in order to be able to connect to the real JACK
server.
.. code-block::
alsa_monitor.alsa.reserve = true
Reserve ALSA devices via *org.freedesktop.ReserveDevice1* on D-Bus.
.. code-block::
alsa_monitor.alsa.reserve-priority = -20
The used ALSA device reservation priority.
.. code-block::
alsa_monitor.alsa.reserve-application-name = WirePlumber
The used ALSA device reservation application name.
* *rules*
Example:
.. code-block::
wireplumber.settings = {
alsa_monitor = [
{
matches = [
{
# This matches the needed sound card.
device.name = "<sound_card_name>"
}
]
actions = {
update-props = {
# Apply all the desired device settings here.
api.alsa.use-acp = true
}
}
}
{
matches = [
# This matches the needed node.
{
node.name = "<node_name>"
}
]
actions = {
# Apply all the desired node specific settings here.
update-props = {
node.nick = "My Node"
priority.driver = 100
session.suspend-timeout-seconds = 5
}
}
}
]
}
Device settings
^^^^^^^^^^^^^^^^^
All the possible settings that you can apply to devices and nodes of the
ALSA monitor are described here.
PipeWire devices correspond to the ALSA cards.
The following settings can be configured on devices created by the monitor:
.. code-block::
api.alsa.use-acp = true
Use the ACP (alsa card profile) code to manage the device. This will probe the
device and configure the available profiles, ports and mixer settings. The
code to do this is taken directly from PulseAudio and provides devices that
look and feel exactly like the PulseAudio devices.
.. code-block::
api.alsa.use-ucm = true
By default, the UCM configuration is used when it is available for your device.
With this option you can disable this and use the ACP profiles instead.
.. code-block::
api.alsa.soft-mixer = false
Setting this option to true will disable the hardware mixer for volume control
and mute. All volume handling will then use software volume and mute, leaving
the hardware mixer untouched. The hardware mixer will still be used to mute
unused audio paths in the device.
.. code-block::
api.alsa.ignore-dB = false
Setting this option to true will ignore the decibel setting configured by the
driver. Use this when the driver reports wrong settings.
.. code-block::
device.profile-set = "profileset-name"
This option can be used to select a custom profile set name for the device.
Usually this is configured in Udev rules but it can also be specified here.
.. code-block::
device.profile = "default profile name"
The default active profile name.
.. code-block::
api.acp.auto-profile = false
Automatically select the best profile for the device. Normally this option is
disabled because the session manager will manage the profile of the device.
The session manager can save and load previously selected profiles. Enable
this if your session manager does not handle this feature.
.. code-block::
api.acp.auto-port = false
Automatically select the highest priority port that is available. This is by
default disabled because the session manager handles the task of selecting and
restoring ports. It can, for example, restore previously saved volumes. Enable
this here when the session manager does not handle port restore.
.. code-block:: lua
["api.acp.probe-rate"] = 48000
Sets the samplerate used for probing the ALSA devices and collecting the profiles
and ports.
.. code-block:: lua
["api.acp.pro-channels"] = 64
Sets the number of channels to use when probing the Pro Audio profile. Normally,
the maximum amount of channels will be used but with this setting this can be
reduced, which can make it possible to use other samplerates on some devices.
Some of the other settings that might be configured on devices:
.. code-block::
device.nick = "My Device",
device.description = "My Device"
``device.description`` will show up in most apps when a device name is shown.
Node Settings
^^^^^^^^^^^^^^^
Nodes are sinks or sources in the PipeWire graph. They correspond to the ALSA
devices. In addition to the generic stream node configuration options, there are
some alsa specific options as well:
.. code-block::
priority.driver = 2000
This configures the node driver priority. Nodes with higher priority will be
used as a driver in the graph. Other nodes with lower priority will have to
resample to the driver node when they are joined in the same graph. The default
value is set based on some heuristics.
.. code-block::
priority.session = 1200
This configures the priority of the node when selecting a default node.
Higher priority nodes will be more likely candidates as a default node.
The monitor, as with all device monitors, is implemented as a SPA plugin and is
part of PipeWire. WirePlumber merely loads the plugin and lets it do its work.
The plugin then monitors UDev and creates device and node objects for all the
ALSA cards that are available on the system.
.. note::
By default, sources have a ``priority.session`` value around 1600-2000 and
sinks have a value around 600-1000. If you are increasing the priority of a
sink, it is **not advised** to use a value higher than 1500, as it may cause
a sink's monitor to be selected as a default source.
One thing worth remembering here is that in ALSA, a "card" represents a
physical sound controller device, and a "device" is a logical access point
that represents a set of inputs and/or outputs that are part of the card. In
PipeWire, a "device" is the direct equivalent of an ALSA "card" and a "node"
is almost equivalent (close, but not quite) of an ALSA "device".
Properties
----------
The ALSA monitor SPA plugin (``api.alsa.enum.udev``) supports properties that
can be used to configure it when it is loaded. These properties can be set in
the ``monitor.alsa.properties`` section of the WirePlumber configuration file.
Example:
.. code-block::
node.pause-on-idle = false
monitor.alsa.properties = {
alsa.use-acp = true
}
Pause-on-idle will stop the node when nothing is linked to it anymore.
This is by default false because some devices cause a pop when they are
opened/closed. The node will, normally, pause and suspend after a timeout
(see suspend-node.lua).
.. describe:: alsa.use-acp
A boolean that controls whether the ACP (alsa card profile) code is to be
the default manager of the device. This will probe the device and configure
the available profiles, ports and mixer settings. The code to do this is
taken directly from PulseAudio and provides devices that look and feel
exactly like the PulseAudio devices.
Rules
-----
When device and node objects are created by the ALSA monitor, they can be
configured using rules. These rules allow matching the existing properties of
these objects and updating them with new values. This is the main way of
configuring ALSA device settings.
These rules can be set in the ``monitor.alsa.rules`` section of the WirePlumber
configuration file.
Example:
.. code-block::
session.suspend-timeout-seconds = 5 -- 0 disables suspend
monitor.alsa.rules = [
{
matches = [
{
# This matches the value of the 'device.name' property of the device.
device.name = "~alsa_card.*"
}
]
actions = {
update-props = {
# Apply all the desired device settings here.
api.alsa.use-acp = true
}
}
}
{
matches = [
# This matches the value of the 'node.name' property of the node.
{
node.name = "~alsa_output.*"
}
]
actions = {
# Apply all the desired node specific settings here.
update-props = {
node.nick = "My Node"
priority.driver = 100
session.suspend-timeout-seconds = 5
}
}
}
]
This option configures a different suspend timeout on the node.
By default this is 5 seconds. For some devices (HiFi amplifiers, for example)
it might make sense to set a higher timeout because they might require some
time to restart after being idle.
Device properties
^^^^^^^^^^^^^^^^^
A value of 0 disables suspend for a node and will leave the ALSA device busy.
The device can then manually be suspended with ``pactl suspend-sink|source``.
The following properties can be configured on devices created by the monitor:
**The following properties can be used to configure the format used by the
ALSA device:**
.. describe:: api.alsa.use-acp
.. code-block::
Use the ACP (alsa card profile) code to manage this device. This will probe
the device and configure the available profiles, ports and mixer settings.
The code to do this is taken directly from PulseAudio and provides devices
that look and feel exactly like the PulseAudio devices.
audio.format = "S16LE"
:Default value: ``true``
:Type: boolean
By default, PipeWire will use a 32 bits sample format but a different format
can be set here.
.. describe:: api.alsa.use-ucm
The Audio rate of a device can be set here:
When ACP is enabled and a UCM configuration is available for a device, by
default it is used instead of the ACP profiles. This option allows you to
disable this and use the ACP profiles instead.
.. code-block::
This option does nothing if ``api.alsa.use-acp`` is set to ``false``.
audio.rate = 44100
:Default value: ``true``
:Type: boolean
By default, the ALSA device will be configured with the same samplerate as the
global graph. If this is not supported, or a custom values is set here,
resampling will be used to match the graph rate.
.. describe:: api.alsa.soft-mixer
.. code-block::
Setting this option to ``true`` will disable the hardware mixer for volume
control and mute. All volume handling will then use software volume and mute,
leaving the hardware mixer untouched. The hardware mixer will still be used
to mute unused audio paths in the device.
audio.channels = 2
audio.position = "FL,FR"
:Type: boolean
By default the channels and their position are determined by the selected
Device profile. You can override this setting here and optionally swap or
reconfigure the channel positions.
.. describe:: api.alsa.ignore-dB
.. code-block::
Setting this option to ``true`` will ignore the decibel setting configured by
the driver. Use this when the driver reports wrong settings.
api.alsa.use-chmap = false
:Type: boolean
Use the channel map as reported by the driver. This is disabled by default
because it is often wrong and the ACP code handles this better.
.. describe:: device.profile-set
.. code-block::
This option can be used to select a custom ACP profile-set name for the
device. This can be configured in UDev rules, but it can also be specified
here. The default is to use "default.conf".
api.alsa.disable-mmap = true
:Type: string
PipeWire will by default access the memory of the device using mmap.
This can be disabled and force the usage of the slower read and write access
modes in case the mmap support of the device is not working properly.
.. describe:: device.profile
.. code-block::
The initial active profile name. The default is to start from the "Off"
profile and then let WirePlumber select the best profile based on its
policy.
channelmix.normalize = true
:Type: string
Makes sure that during such mixing & resampling original 0 dB level is
preserved, so nothing sounds wildly quieter/louder.
.. describe:: api.acp.auto-profile
.. code-block::
Automatically select the best profile for the device. Normally this option is
disabled because WirePlumber will manage the profile of the device.
WirePlumber can save and load previously selected profiles. Enable this in
custom configurations where the relevant WirePlumber components are disabled.
channelmix.mix-lfe = true
:Type: boolean
Creates "center" channel for X.0 recordings from front stereo on X.1 setups and
pushes some low-frequency/bass from "center" from X.1 recordings into front
stereo on X.0 setups.
.. describe:: api.acp.auto-port
.. code-block::
Automatically select the highest priority port that is available ("port" is a
PulseAudio/ACP term, the equivalent of a "Route" in PipeWire). This is by
default disabled because WirePlumber handles the task of selecting and
restoring Routes. Enable this in custom configurations where the relevant
WirePlumber components are disabled.
monitor.channel-volumes = false
:Type: boolean
By default, the volume of the sink/source does not influence the volume on the
monitor ports. Set this option to true to change this. PulseAudio has
inconsistent behaviour regarding this option, it applies channel-volumes only
when the sink/source is using software volumes.
.. describe:: api.acp.probe-rate
Sets the samplerate used for probing the ALSA devices and collecting the
profiles and ports.
:Type: integer
.. describe:: api.acp.pro-channels
Sets the number of channels to use when probing the "Pro Audio" profile.
Normally, the maximum amount of channels will be used but with this setting
this can be reduced, which can make it possible to use other samplerates on
some devices.
:Type: integer
Some of the other properties that can be configured on devices:
.. describe:: device.nick
A short name for the device.
.. describe:: device.description
A longer, user-friendly name of the device. This will show up in most
user interfaces as the device's name.
.. describe:: device.disabled
Disables the device. PipeWire will remove it from the list of cards or
devices.
:Type: boolean
Node properties
^^^^^^^^^^^^^^^
The following properties can be configured on nodes created by the monitor:
.. describe:: priority.driver
This configures the node driver priority. Nodes with higher priority will be
used as a driver in the graph. Other nodes with lower priority will have to
resample to the driver node when they are joined in the same graph. The
default value is set based on some heuristics.
:Type: integer
.. describe:: priority.session
This configures the priority of the node when selecting a default node
(default sink/source as a link target for streams). Higher priority nodes
will be more likely candidates for becoming the default node.
:Type: integer
.. note::
By default, sources have a ``priority.session`` value around 1600-2000 and
sinks have a value around 600-1000. If you are increasing the priority of
a sink, it is **not advised** to use a value higher than 1500, as it may
cause a sink's monitor to be selected as the default source.
.. describe:: node.pause-on-idle
Pause the node when nothing is linked to it anymore. This is by default false
because some devices make a "pop" sound when they are opened/closed.
The node will normally pause and suspend after a timeout (see below).
:Type: boolean
.. describe:: session.suspend-timeout-seconds
This option configures a different suspend timeout on the node. By default
this is ``5`` seconds. For some devices (HiFi amplifiers, for example) it
might make sense to set a higher timeout because they might require some time
to restart after being idle.
A value of ``0`` disables suspend for a node and will leave the ALSA device
busy. The device can then be manually suspended with
``pactl suspend-sink|source``.
:Type: integer
.. describe:: audio.format
The sample format of the device. By default, PipeWire will use a 32 bits
sample format but a different format can be set here.
:Type: string (``"S16LE"``, ``"S32LE"``, ``"F32LE"``, ...)
.. describe:: audio.rate
The sample rate of the device. By default, the ALSA device will be configured
with the same samplerate as the global graph. If this is not supported, or a
custom value is set here, resampling will be used to match the graph rate.
:Type: integer
.. describe:: audio.channels
The number of channels of the device. By default the channels and their
position are determined by the selected device profile. You can override
this setting here.
:Type: integer
.. describe:: audio.position
The position of the channels. By default the number of channels and their
position are determined by the selected device profile. You can override
this setting here and optionally swap or reconfigure the channel positions.
:Type: array of strings (example: ``["FL", "FR", "LFE", "FC", "RL", "RR"]``)
.. describe:: api.alsa.use-chmap
Use the channel map as reported by the driver. This is disabled by default
because it is often wrong and the ACP code handles this better.
:Type: boolean
.. describe:: api.alsa.disable-mmap
Disable the use of mmap for the ALSA device. By default, PipeWire will access
the memory of the device using mmap. This can be disabled and force the usage
of the slower read and write access modes, in case the mmap support of the
device is not working properly.
:Type: boolean
.. describe:: channelmix.normalize
Normalize the channel volumes when mixing & resampling, making sure that the
original 0 dB level is preserved so that nothing sounds wildly
quieter/louder. This is disabled by default.
:Type: boolean
.. describe:: channelmix.mix-lfe
Creates a "center" channel for X.0 recordings from the front stereo on X.1
setups and pushes some low-frequency/bass from the "center" of X.1 recordings
into the front stereo on X.0 setups. This is disabled by default.
:Type: boolean
.. describe:: monitor.channel-volumes
By default, the volume of the sink/source does not influence the volume on
the monitor ports. Set this option to true to change this. PulseAudio has
inconsistent behaviour regarding this option, it applies channel-volumes only
when the sink/source is using software volumes.
:Type: boolean
.. describe:: node.disabled
Disables the node. Pipewire will remove it from the list of the nodes.
:Type: boolean
ALSA buffer properties
^^^^^^^^^^^^^^^^^^^^^^
......................
PipeWire uses a timer to consume and produce samples to/from ALSA devices.
After every timeout, it queries the device hardware pointers of the device and
uses this information to set a new timeout. See also this example program.
PipeWire by default uses a timer to consume and produce samples to/from ALSA
devices. After every timeout, it queries the hardware pointers of the device and
uses this information to set a new timeout. This works well for most devices,
but there is a class of devices, so called "batch" devices, that need extra
buffering and timing tweaks to work properly. This is because batch devices only
get their hardware pointers updated after each hardware interrupt. When the
hardware interrupt frequency and the timer frequency are aligned, it is possible
for the hardware pointers to be updated just after the timer has expired,
resulting in sometimes wrong timing information being returned by the query. In
contrast, non-batch devices get pointer updates independent of the interrupt.
By default, PipeWire handles ALSA batch devices differently from non-batch
devices. Batch devices only get their hardware pointers updated after each
hardware interrupt. Non-batch devices get updates independent of the interrupt.
This means that for batch devices we need to set the interrupt at a sufficiently
high frequency (at the cost of CPU usage) while for non-batch devices we want to
set the interrupt frequency as low as possible (to save CPU).
high frequency, at the cost of CPU usage, while for non-batch devices we want to
set the interrupt frequency as low as possible to save CPU. For batch devices
we also need to take the extra buffering into account caused by the delayed
updates of the hardware pointers.
For batch devices we also need to take the extra buffering into account caused
by the delayed updates of the hardware pointers.
.. note::
Most USB devices are batch devices and will be handled as such by PipeWire by
default.
Most USB devices are batch devices and will be handled as such by PipeWire by
default.
There are 2 tunable parameters to control the buffering and timeouts in a
device
device:
.. code-block::
.. describe:: api.alsa.period-size
api.alsa.period-size = 1024
This sets the device interrupt to every period-size samples for non-batch
devices and to half of this for batch devices. For batch devices, the other
half of the period-size is used as extra buffering to compensate for the
delayed update. So, for batch devices, there is an additional period-size/2
delay. It makes sense to lower the period-size for batch devices to reduce
this delay.
This sets the device interrupt to every period-size samples for non-batch
devices and to half of this for batch devices. For batch devices, the other
half of the period-size is used as extra buffering to compensate for the delayed
update. So, for batch devices, there is an additional period-size/2 delay.
It makes sense to lower the period-size for batch devices to reduce this delay.
:Type: integer (samples)
.. code-block::
.. describe:: api.alsa.headroom
api.alsa.headroom = 0
This adds extra delay between the hardware pointers and software pointers.
In most cases this can be set to 0. For very bad devices or emulated devices
(like in a VM) it might be necessary to increase the headroom value.
:Type: integer (samples)
.. describe:: api.alsa.period-num
This configures the number of periods in the hardware buffer, which controls
its size. Note that this is multiplied by the period of the device to
determine the size, so for batch devices, the total buffer size is
effectively period-num * period-size/2.
:Type: integer
This adds extra delay between the hardware pointers and software pointers.
In most cases this can be set to 0. For very bad devices or emulated devices
(like in a VM) it might be necessary to increase the headroom value.
In summary, this is the overview of buffering and timings:
============== ============================================ ==========================================
Property Batch Non-Batch
============== ============================================ ==========================================
IRQ Frequency api.alsa.period-size/2 api.alsa.period-size
Extra Delay api.alsa.headroom + api.alsa.period-size/2 api.alsa.headroom
Buffer Size api.alsa.period-num * api.alsa.period-size/2 api.alsa.period-num * api.alsa.period-size
============== ============================================ ==========================================
============== ========================================== =========
Property Batch Non-Batch
============== ========================================== =========
IRQ Frequency api.alsa.period-size/2 api.alsa.period-size
Extra Delay api.alsa.headroom + api.alsa.period-size/2 api.alsa.headroom
============== ========================================== =========
Finally, it is possible to disable the batch device tweaks with:
It is possible to disable the batch device tweaks with:
.. describe:: api.alsa.disable-batch
.. code-block::
This disables the batch device tweaks. It removes the extra delay added of
period-size/2 if the device can support this. For batch devices it is also a
good idea to lower the period-size (and increase the IRQ frequency) to get
smaller batch updates and lower latency.
api.alsa.disable-batch"] = true
It removes the extra delay added of period-size/2 if the device can support this.
For batch devices it is also a good idea to lower the period-size
(and increase the IRQ frequency) to get smaller batch updates and lower latency.
:Type: boolean
ALSA extra latency properties
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.............................
Extra internal delay in the DAC and ADC converters of the device itself can be
set with the ``latency.internal.*`` properties:
.. code-block::
latency.internal.rate"] = 256
latency.internal.ns"] = 0
latency.internal.rate = 256
latency.internal.ns = 0
You can configure a latency in samples (relative to rate with
``latency.internal.rate``) or in nanoseconds (``latency.internal.ns``).
@ -419,44 +458,28 @@ Set the internal latency to 256 samples:
remote 0 port 76 changed
Startup tweaks
^^^^^^^^^^^^^^
..............
Some devices need some time before they can report accurate hardware pointer
positions. In those cases, an extra start delay can be added that is used to
compensate for this startup delay:
.. describe:: api.alsa.start-delay
.. code-block::
Some devices need some time before they can report accurate hardware pointer
positions. In those cases, an extra start delay can be added to compensate
for this startup delay. This sets the startup delay in samples. The default
is 0.
["api.alsa.start-delay"] = 0
It is unsure when this tunable should be used.
:Type: integer (samples)
IEC958 (S/PDIF) passthrough
^^^^^^^^^^^^^^^^^^^^^^^^^^^
...........................
S/PDIF passthrough will only be enabled when the accepted codecs are configured
on the ALSA device.
.. describe:: iec958.codecs
This can be done in 3 different ways:
S/PDIF passthrough will only be enabled when the accepted codecs are configured
on the ALSA device. This can be done by setting the list of supported codecs
on this property.
1. Use pavucontrol and toggle the codecs in the output advanced section.
Note that it is possible to also configure this property at runtime, either
with tools like pavucontrol or with the ``pw-cli`` tool, like this:
``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'``
2. Modify the ``["iec958.codecs"]`` node property to contain supported codecs.
Example ``~/.config/wireplumber/main.lua.d/51-alsa-spdif.lua``:
.. code-block:: lua
table.insert (alsa_monitor.rules, {
matches = {
{
{ "node.name", "matches", "alsa_output.*" },
},
},
apply_properties = {
["iec958.codecs"] = "[ PCM DTS AC3 EAC3 TrueHD DTS-HD ]",
}
})
3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify
the codecs at runtime.
:Type: array of strings (example: ``[ "PCM", "DTS", "AC3", "EAC3", "TrueHD", "DTS-HD" ]``)

View file

@ -3,174 +3,438 @@
Bluetooth configuration
=======================
Using the same format as the :ref:`ALSA monitor <config_alsa>`, the
configuration file ``wireplumber.conf.d/bluetooth.conf`` is charged
to configure the Bluetooth devices and nodes created by WirePlumber.
Bluetooth audio and MIDI devices are managed by the BlueZ and BlueZ-MIDI
monitors, respectively.
* *Settings*
Both monitors are enabled by default and can be disabled using the
``monitor.bluez`` and ``monitor.bluez-midi`` :ref:`features <config_features>`
in the configuration file.
Example:
As with all device monitors, both of these monitors are implemented as SPA
plugins and are part of PipeWire. WirePlumber merely loads the plugins and lets
them do their work. These plugins then monitor the BlueZ system-wide D-Bus
service and create device and node objects for all the connected Bluetooth audio
and MIDI devices.
.. code-block::
Logind integration
------------------
wireplumber.properties = {
bluez5.enable-msbc = true,
}
The BlueZ monitors are integrated with logind to ensure that only one user at a
time can use the Bluetooth audio devices. This is because on most Linux desktop
systems, the graphical login manager (GDM, SDDM, etc.) is running as a separate
user and runs its own instance of PipeWire and Wireplumber. This means that if a
user logs in graphically, the Bluetooth audio devices will be automatically
grabbed by the PipeWire/WirePlumber instance of the graphical login manager,
and the user that logs in will not get access to them.
This example will enable the MSBC codec in connected Bluetooth devices that
support it.
To overcome this, the BlueZ monitors are integrated with logind and are only
allowed to create device and node objects for Bluetooth audio devices if the
user is currently on the "active" logind session.
The list of all valid properties are:
In some cases, however, this behavior is not desired. For example, if you
manually switch to a TTY and log in there, you may want to keep the Bluetooth
audio devices connected to the now inactive graphical session. Or you may want
to have a dedicated user that is always allowed to use the Bluetooth audio
devices, regardless of the active logind session, for example for a (possibly
headless) music player daemon.
.. code-block::
To disable this behavior, you can set the ``monitor.bluez.seat-monitoring``
:ref:`feature <config_features>` to ``disabled``.
Example configuration :ref:`fragment <config_conf_file_fragments>` file:
.. code-block::
wireplumber.profiles = {
main = {
monitor.bluez.seat-monitoring = disabled
}
}
.. note::
If logind is not installed on the system, this functionality is disabled
automatically.
Monitor Properties
------------------
The BlueZ monitor SPA plugin (``api.bluez5.enum.dbus``) supports properties that
can be used to configure it when it is loaded. These properties can be set in
the ``monitor.bluez.properties`` section of the WirePlumber configuration file.
Example:
.. code-block::
monitor.bluez.properties = {
bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
bluez5.codecs = [ sbc sbc_xq aac ]
bluez5.enable-sbc-xq = true
Enables the SBC-XQ codec in connected Blueooth devices that support it
.. code-block::
bluez5.enable-msbc = true
Enables the MSBC codec in connected Blueooth devices that support it
.. code-block::
bluez5.enable-hw-volume = true
Enables hardware volume controls in Bluetooth devices that support it
.. code-block::
bluez5.headset-roles = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]"
Enabled headset roles (default: [ hsp_hs hfp_ag ]), this property only applies
to native backend. Currently some headsets (Sony WH-1000XM3) are not working
with both hsp_ag and hfp_ag enabled, disable either hsp_ag or hfp_ag to work
around it.
Supported headset roles: ``hsp_hs`` (HSP Headset), ``hsp_ag`` (HSP Audio
Gateway), ``hfp_hf`` (HFP Hands-Free) and ``hfp_ag`` (HFP Audio Gateway)
.. code-block::
bluez5.codecs = "[ sbc sbc_xq aac ]"
Enables ``sbc``, ``sbc_zq`` and ``aac`` A2DP codecs.
Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
``faststream_duplex``.
All codecs are supported by default.
.. code-block::
bluez5.hfphsp-backend = "native"
}
HFP/HSP backend (default: native). Available values: ``any``, ``none``,
``hsphfpd``, ``ofono`` or ``native``.
.. describe:: bluez5.roles
.. code-block::
Enabled roles.
bluez5.default.rate = 48000
Currently some headsets (e.g. Sony WH-1000XM3) do not work with both
``hsp_ag`` and ``hfp_ag`` enabled, so by default we enable only HFP.
The bluetooth default audio rate.
Supported roles:
.. code-block::
- ``hsp_hs`` (HSP Headset)
- ``hsp_ag`` (HSP Audio Gateway),
- ``hfp_hf`` (HFP Hands-Free),
- ``hfp_ag`` (HFP Audio Gateway)
- ``a2dp_sink`` (A2DP Audio Sink)
- ``a2dp_source`` (A2DP Audio Source)
- ``bap_sink`` (LE Audio Basic Audio Profile Sink)
- ``bap_source`` (LE Audio Basic Audio Profile Source)
bluez5.default.channels = 2
:Default value: ``[ a2dp_sink a2dp_source bap_sink bap_source hfp_hf hfp_ag ]``
:Type: array of strings
The bluetooth default number of channels.
.. describe:: bluez5.codecs
* *Rules*
Enabled A2DP codecs.
Example:
Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
``faststream_duplex``, ``lc3plus_h3``, ``opus_05``, ``opus_05_51``,
``opus_05_71``, ``opus_05_duplex``, ``opus_05_pro``, ``lc3``.
.. code-block::
:Default value: all available codecs
:Type: array of strings
wireplumber.settings = {
bluez_monitor = [
{
matches = [
{
# This matches the needed sound card.
device.name = "<bluez_sound_card_name>"
}
]
actions = {
update-props = {
# Apply all the desired device settings here.
bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
}
}
}
{
matches = [
# This matches the needed node.
{
node.name = "<node_name>"
}
]
actions = {
# Apply all the desired node specific settings here.
update-props = {
node.nick = "My Node"
priority.driver = 100
session.suspend-timeout-seconds = 5
}
}
}
]
}
.. describe:: bluez5.enable-msbc
This will set the auto-connect property to ``hfp_hf``, ``hsp_hs`` and
``a2dp_sink`` on bluetooth devices whose name matches the ``bluez_card.*``
pattern.
Enable mSBC codec (wideband speech codec for HFP/HSP).
A list of valid properties are:
This does not work on all headsets, so it is enabled based on the hardware
quirks database. By explicitly setting this option you can force it to be
enabled or disabled regardless.
.. code-block::
:Default value: ``true``
:Type: boolean
bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
.. describe:: bluez5.enable-sbc-xq
Auto-connect device profiles on start up or when only partial profiles have
connected. Disabled by default if the property is not specified.
Enable SBC-XQ codec (high quality SBC codec for A2DP).
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
``hsp_ag`` and ``a2dp_source``.
This does not work on all headsets, so it is enabled based on the hardware
quirks database. By explicitly setting this option you can force it to be
enabled or disabled regardless.
.. code-block::
:Default value: ``true``
:Type: boolean
bluez5.hw-volume = "[ hfp_ag hsp_ag a2dp_source ]"
.. describe:: bluez5.enable-hw-volume
Hardware volume controls (default: ``hfp_ag``, ``hsp_ag``, and ``a2dp_source``)
Enable hardware volume controls.
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
``hsp_ag`` and ``a2dp_source``.
This does not work on all headsets, so it is enabled based on the hardware
quirks database. By explicitly setting this option you can force it to be
enabled or disabled regardless.
.. code-block::
:Default value: ``true``
:Type: boolean
bluez5.a2dp.ldac.quality = "auto"
.. describe:: bluez5.hfphsp-backend
LDAC encoding quality.
HFP/HSP backend.
Available values: ``auto`` (Adaptive Bitrate, default),
``hq`` (High Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and
``mq`` (Mobile use Quality, 330/303kbps).
Available values: ``any``, ``none``, ``hsphfpd``, ``ofono`` or ``native``.
.. code-block::
:Default value: ``native``
:Type: string
bluez5.a2dp.aac.bitratemode = 0
.. describe:: bluez5.hfphsp-backend-native-modem
AAC variable bitrate mode.
Modem to use for native HFP/HSP backend ModemManager support. When enabled,
PipeWire will forward HFP commands to the specified ModemManager device.
This corresponds to the 'Device' property of the
``org.freedesktop.ModemManager1.Modem`` interface. May also be ``any`` to
use any available modem device.
Available values: 0 (cbr, default), 1-5 (quality level).
:Default value: ``none``
:Type: string
.. code-block::
.. describe:: bluez5.hw-offload-sco
device.profile = "a2dp-sink"
HFP/HSP hardware offload SCO support.
Profile connected first.
Using this feature requires a custom WirePlumber script that handles audio
routing in a platform-specific way. See ``tests/examples/bt-pinephone.lua``
for an example.
Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.
:Default value: ``false``
:Type: boolean
.. describe:: bluez5.default.rate
The default audio rate for the A2DP codec configuration.
:Default value: ``48000``
:Type: integer
.. describe:: bluez5.default.channels
The default number of channels for the A2DP codec configuration.
:Default value: ``2``
:Type: integer
.. describe:: bluez5.dummy-avrcp-player
Register dummy AVRCP player. Some devices have wrongly functioning volume or
playback controls if this is not enabled. Disabled by default.
:Default value: ``false``
:Type: boolean
.. describe:: Opus Pro Audio mode settings
.. code-block::
bluez5.a2dp.opus.pro.channels = 3
bluez5.a2dp.opus.pro.coupled-streams = 1
bluez5.a2dp.opus.pro.locations = [ FL,FR,LFE ]
bluez5.a2dp.opus.pro.max-bitrate = 600000
bluez5.a2dp.opus.pro.frame-dms = 50
bluez5.a2dp.opus.pro.bidi.channels = 1
bluez5.a2dp.opus.pro.bidi.coupled-streams = 0
bluez5.a2dp.opus.pro.bidi.locations = [ FC ]
bluez5.a2dp.opus.pro.bidi.max-bitrate = 160000
bluez5.a2dp.opus.pro.bidi.frame-dms = 400
Options for the PipeWire-specific multichannel Opus codec, which can be used
to transport audio over Bluetooth between devices running PipeWire.
MIDI Monitor Properties
-----------------------
The BlueZ MIDI monitor SPA plugin (``api.bluez5.midi.enum``) may, in the future,
support properties that can be used to configure it when it is loaded. These
properties can be set in the ``monitor.bluez-midi.properties`` section of the
WirePlumber configuration file. At the moment of writing, there are no
properties that can be set there.
In addition, the BlueZ MIDI monitor supports a list of MIDI server node names
that can be used to create Bluetooth LE MIDI service instances. These
server node names can be set in the ``monitor.bluez-midi.servers`` section of
the WirePlumber configuration file.
Example:
.. code-block::
monitor.bluez-midi.servers = [ "bluez_midi.server" ]
.. note::
Typical BLE MIDI instruments have one service instance, so adding more than
one here may confuse some clients.
Rules
-----
When device and node objects are created by the BlueZ monitor, they can be
configured using rules. These rules allow matching the existing properties of
these objects and updating them with new values. This is the main way of
configuring Bluetooth device settings.
These rules can be set in the ``monitor.bluez.rules`` section of the WirePlumber
configuration file.
Example:
.. code-block::
monitor.bluez.rules = [
{
matches = [
{
## This matches all bluetooth devices.
device.name = "~bluez_card.*"
}
]
actions = {
update-props = {
bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
bluez5.hw-volume = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
bluez5.a2dp.ldac.quality = "auto"
bluez5.a2dp.aac.bitratemode = 0
bluez5.a2dp.opus.pro.application = "audio"
bluez5.a2dp.opus.pro.bidi.application = "audio"
}
}
}
{
matches = [
{
## Matches all sources.
node.name = "~bluez_input.*"
}
{
## Matches all sinks.
node.name = "~bluez_output.*"
}
]
actions = {
update-props = {
bluez5.media-source-role = "input"
# Common node & audio adapter properties may also be set here
node.nick = "My Node"
priority.driver = 100
priority.session = 100
node.pause-on-idle = false
resample.quality = 4
channelmix.normalize = false
channelmix.mix-lfe = false
session.suspend-timeout-seconds = 5
monitor.channel-volumes = false
}
}
}
]
Device properties
^^^^^^^^^^^^^^^^^
The following properties can be set on device objects:
.. describe:: bluez5.auto-connect
Auto-connect device profiles on start up or when only partial profiles have
connected. Disabled by default if the property is not specified.
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
``hsp_ag`` and ``a2dp_source``.
:Default value: ``[]``
:Type: array of strings
.. describe:: bluez5.hw-volume
Enable hardware volume controls on these profiles.
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
``hsp_ag`` and ``a2dp_source``.
:Default value: ``[ hfp_ag hsp_ag a2dp_source ]``
:Type: array of strings
.. describe:: bluez5.a2dp.ldac.quality
LDAC encoding quality.
Available values: ``auto`` (Adaptive Bitrate, default), ``hq`` (High
Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and ``mq``
(Mobile use Quality, 330/303kbps).
:Default value: ``auto``
:Type: string
.. describe:: bluez5.a2dp.aac.bitratemode
AAC variable bitrate mode.
Available values: 0 (cbr, default), 1-5 (quality level).
:Default value: ``0``
:Type: integer
.. describe:: bluez5.a2dp.opus.pro.application
Opus Pro Audio encoding mode.
Available values: ``audio``, ``voip``, ``lowdelay``.
:Default value: ``audio``
:Type: string
.. describe:: bluez5.a2dp.opus.pro.bidi.application
Opus Pro Audio encoding mode for bidirectional audio.
Available values: ``audio``, ``voip``, ``lowdelay``.
:Default value: ``audio``
:Type: string
.. describe:: device.profile
The profile that is activated initially when the device is connected.
Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.
:Default value: ``a2dp-sink``
:Type: string
Node properties
^^^^^^^^^^^^^^^
The following properties can be set on node objects:
.. describe:: bluez5.media-source-role
Media source role, ``input`` or ``playback``. This controls how a media
source device, such as a smartphone, is used by the system. Defaults to
``playback``, playing the incoming stream out to speakers. Set to ``input``
to use the smartphone as an input for apps (like a microphone).
:Default value: ``playback``
:Type: string
MIDI Rules
----------
Similarly to the above rules, the BlueZ MIDI monitor also supports rules that
can be used to configure MIDI nodes when they are created.
These rules can be set in the ``monitor.bluez-midi.rules`` section of the
WirePlumber configuration file.
Example:
.. code-block::
monitor.bluez-midi.rules = [
{
matches = [
{
node.name = "~bluez_midi.*"
}
]
actions = {
update-props = {
node.nick = "My Node"
priority.driver = 100
priority.session = 100
node.pause-on-idle = false
session.suspend-timeout-seconds = 5
node.latency-offset-msec = 0
}
}
}
]
.. note::
It is possible to also match MIDI server nodes by testing the ``node.name``
property against the server node names that were set in the
``monitor.bluez-midi.servers`` section of the WirePlumber configuration file.
MIDI-specific properties
^^^^^^^^^^^^^^^^^^^^^^^^
.. describe:: node.latency-offset-msec
Latency adjustment to apply on the node. Larger values add a
constant latency, but reduces timing jitter caused by Bluetooth
transport.
:Default value: ``0``
:Type: integer (milliseconds)

View file

@ -66,7 +66,7 @@ The main types of components are:
A PipeWire module, which is also a shared library that can be loaded
dynamically, but extends the functionality of the underlying *libpipewire*
library. Loading PipeWire modules in the WirePlumber context can be useful
to load custom protocol extensions or to offload some funcitonality from
to load custom protocol extensions or to offload some functionality from
the PipeWire daemon.
* **virtual**

View file

@ -1,11 +1,11 @@
.. _config_conf_file:
Configuration file
==================
The configuration file
======================
WirePlumber's configuration file is by default ``wireplumber.conf`` and resides
in the ``pipewire`` configuration directory (see :ref:`config_locations` for
more details on that).
in one of the WirePlumber specific
:ref:`configuration file search locations <config_locations>`.
The default configuration file can be changed on the command line by passing
the ``--config-file`` or ``-c`` option:
@ -14,19 +14,20 @@ the ``--config-file`` or ``-c`` option:
$ wireplumber --config-file=custom.conf
.. note::
.. important::
Starting with WirePlumber 0.5, this is the only file that WirePlumber reads
to load configuration (together with its fragments - see below). In the past,
WirePlumber also used to read Lua configuration files that were referenced
from ``wireplumber.conf`` and all the heavy lifting was done in Lua. This is
no longer the case, and the Lua configuration files are no longer supported.
no longer the case, and the **Lua configuration files are no longer supported.**
See :ref:`config_migration`.
Note that Lua is still the scripting language for WirePlumber, but it is only
used for actual scripting and not for configuration.
Format
------
The SPA-JSON Format
-------------------
The format of this configuration file is a variant of JSON that is also
used in PipeWire configuration files (also known as SPA-JSON). The file consists
@ -89,6 +90,8 @@ Examples of valid SPA-JSON files:
"val1", "val2", "val3"
]
.. _config_conf_file_fragments:
Fragments
---------
@ -101,17 +104,18 @@ When loading the configuration file, WirePlumber will also look for
additional files in the directory that has the same name as the configuration
file suffixed with ``.d`` and will load all of them as well. For example,
loading ``wireplumber.conf`` will also load any ``.conf`` files under
``wireplumber.conf.d/``. This directory is searched in all the search paths
for configuration files (see :ref:`config_locations`) and the fragments are
loaded from *all* of them.
``wireplumber.conf.d/``. This directory is searched in all the configuration
search locations and the fragments are loaded from *all* of them, starting
from the most system-wide locations and moving towards the most user-specific
locations, in alphanumerical order within each location (see also
:ref:`config_locations_fragments`).
The fragments are loaded in alphabetical order, after the main configuration
file. When a JSON object appears in multiple files, the properties of the
objects are merged together. When a JSON array appears in multiple files, the
arrays are concatenated together. When merging objects, if specific properties
appear in many of those objects, the last one to be parsed always overwrites
previous ones, unless the value is also an object or array; if it is, then the
value is recursively merged using the same rules.
When a JSON object appears in multiple files, the properties of the objects are
merged together. When a JSON array appears in multiple files, the arrays are
concatenated together. When merging objects, if specific properties appear in
many of those objects, the last one to be parsed always overwrites previous
ones, unless the value is also an object or array; if it is, then the value is
recursively merged using the same rules.
Sections
--------
@ -124,6 +128,12 @@ file:
This section is an array that lists components that can be loaded by
WirePlumber. For more information, see :ref:`config_components_and_profiles`.
* *wireplumber.components.rules*
This section is an array containing rules that can be used to modify entries
of the *wireplumber.components* array. This is useful to inject changes
to the components list without having to modify the main configuration file.
* *wireplumber.profiles*
This section is an object that defines profiles that can be loaded by
@ -134,6 +144,13 @@ file:
This section is an object that defines settings that can be used to
alter WirePlumber's behavior. For more information, see :ref:`config_settings`.
* *wireplumber.settings.schema*
This section is an object that defines the schema for the settings that
can be listed in *wireplumber.settings*. This is used to validate the
settings when they are modified at runtime. For more information, see
:ref:`config_configuration_option_types`.
In addition, there are many sections that are specific to certain components,
mostly hardware monitors, such as *monitor.alsa.properties*,
*monitor.alsa.rules*, etc. These are documented further on, in the respective
@ -187,9 +204,18 @@ by libpipewire to configure the PipeWire context:
.. note::
PipeWire modules can also be loaded as :ref:`components <config_components_and_profiles>`,
which may be preferrable since it allows you to load them conditionally
which may be preferable since it allows you to load them conditionally
based on the profile and component dependencies.
.. admonition:: Remember
Modules listed in *context.modules* are always loaded before attempting a
connection to the PipeWire daemon, while modules listed in
*wireplumber.components* are always loaded after the connection is
established. It is important to load the PipeWire protocol-native module
and any extensions (such as module-metadata) in the *context.modules*
section, so that the connection can be done properly.
Each module is described by a JSON object containing the module's *name*,
its arguments (*args*) and a combination of *flags*, which can be ``ifexists``
and ``nofail``.

View file

@ -0,0 +1,125 @@
.. _config_configuration_option_types:
Configuration option types
==========================
As seen in the previous sections, WirePlumber can be partly configured by
enabling or disabling features, which affect which components are getting
loaded. These components, however, can be further configured to fine-tune their
behavior. This section describes the different types of configuration options
that can be used to configure WirePlumber components.
Dynamic options ("Settings")
----------------------------
Dynamic options (also simply referred to as "settings") are configuration
options that can be changed at runtime. They are typically simple values like
booleans, integers, strings, etc. and are all located under the
``wireplumber.settings`` section in the configuration file. Their purpose is to
allow the user to change simple behavioral aspects of WirePlumber.
As the name suggests, these options are dynamic and can be changed at runtime
using ``wpctl`` or the :ref:`settings_api` API. For example, setting the
``device.routes.default-sink-volume`` setting to ``0.5`` can be done like this:
.. code-block:: bash
$ wpctl settings device.routes.default-sink-volume 0.5
Under the hood, when WirePlumber starts, the ``metadata.sm-settings`` component
(provided by ``libwireplumber-module-settings``) reads this section from the
configuration file and populates the ``sm-settings`` metadata object, which is
exported to PipeWire. In addition, it reads the ``wireplumber.settings.schema``
section and populates the ``schema-sm-settings`` metadata object, which is used
by the API to validate the settings. Any options that are missing from
``wireplumber.settings`` are also populated in ``sm-settings`` from their
default values in the schema. Then the rest of the components read their
configuration options from this metadata object via the :ref:`settings_api` API.
Most of the components that use such dynamic options make sure to listen
to changes in the metadata object so that they can immediately adapt their
behavior. Other components, however, do not react immediately and the changes
only take effect the next time the option is needed. For instance, some options
affect created objects in a way that cannot be changed after the object has been
created, so when the option is changed it applies only to new objects and not
existing ones.
Changing the settings at runtime in the ``sm-settings`` metadata object is
a non-persistent change. The changes will be lost when WirePlumber is
restarted. However, the :ref:`settings_api` API also supports saving settings
to a state file, which will be loaded again when WirePlumber starts and
override the settings from the configuration file. This is done by using yet
another metadata object called ``persistent-sm-settings``. When a setting is
changed in the ``persistent-sm-settings`` metadata object, WirePlumber
automatically saves the change to the state file and also changes the value in
the ``sm-settings`` metadata object immediately.
To make such a persistent change using ``wpctl``, the ``--save`` option can be
used. For example, to set the ``device.routes.default-sink-volume`` setting to
``0.5`` and save it to the state file:
.. code-block:: bash
$ wpctl settings --save device.routes.default-sink-volume 0.5
With ``wpctl``, it is also possible to restore a setting to its default value
(taken from the schema), by using the ``--reset`` option. For example, to reset
the ``device.routes.default-sink-volume`` setting, the following command can be
used:
.. code-block:: bash
$ wpctl settings --reset device.routes.default-sink-volume
In addition, the ``--delete`` option can be used to delete a setting from the
``persistent-sm-settings`` metadata object, which will also remove it from the
state file. After deleting, the value from the ``wireplumber.settings`` section
of the configuration file will be used again. For example, to delete the
``device.routes.default-sink-volume`` setting, the following command can be
used:
.. code-block:: bash
$ wpctl settings --delete device.routes.default-sink-volume
A list of all the available settings can be found in the :ref:`config_settings`
section.
Static options
--------------
Static options are more complex configuration structures that reside only in the
configuration file and cannot be changed at runtime. They are typically used to
configure device monitors and provide rules that match objects and perform
actions such as update their properties.
While these options could also in theory be stored in the metadata object and
be made dynamic, this is not supported because these options are both complex
and therefore hard to change on the command line, but also because they are
typically used to configure objects that are created at startup and cannot be
changed later.
Static options are located in their own top-level sections. Examples of such
sections are ``monitor.alsa.properties`` and ``monitor.alsa.rules`` that are
used to configure the ``monitor.alsa`` component. The next sections of this
documentation describe in detail all the available static options.
Component arguments
~~~~~~~~~~~~~~~~~~~
Components can also be configured statically by passing arguments to them when
they are loaded. This is done by adding an ``arguments`` key to the component
description in the ``wireplumber.components`` section (see
:ref:`config_components_and_profiles`).
The arguments are mostly meant as a way to instantiate multiple instances of the
same module or script with slightly different configuration to create a new
unique component. For example, the ``metadata.lua`` script can be instantiated
multiple times to create multiple metadata objects, each with a different name.
The name of the metadata object is passed as an argument to the script.
While many more static options could be passed as arguments, this is not
recommended because it is not possible to override the arguments by adding
:ref:`fragment<config_conf_file_fragments>` configuration files. Therefore, it
is recommended to use component-specific top-level sections, unless the option
is not meant to be changed by the user.

View file

@ -11,6 +11,9 @@ can be confusing to go through them. This list here is meant to be a quick
reference for the most common ones that actually make sense to be toggled in
a configuration file in order to customize WirePlumber's behavior.
For more information on what features are and how they work, refer to the
previous section: :ref:`config_components_and_profiles`.
Hardware monitors
-----------------
@ -39,6 +42,12 @@ Audio
Enables the ALSA MIDI device monitor.
.. describe:: node.software-dsp
Enables software DSP based on pre-configured hardware rules.
See :ref:`policies_software_dsp` for more information.
Bluetooth
~~~~~~~~~
@ -131,9 +140,9 @@ Policies
for enabling devices, linking streams, granting permissions to clients,
etc, as appropriate for a desktop system.
.. describe:: policy.role-priority-system
.. describe:: policy.role-based
Enables the role priority system policy. This system creates virtual sinks
Enables the role based priority system policy. This system creates virtual sinks
that group streams based on their ``media.role`` property, and assigns a
priority to each role. Depending on the priority configuration, lower
priority roles may be corked or ducked when a higher priority role stream

View file

@ -1,90 +0,0 @@
.. _config_locations:
Locations of files
==================
Location of configuration files
-------------------------------
WirePlumber's default locations of its configuration files are the same as
pipewire. Typically, those end up being
``$XDG_CONFIG_DIR/pipewire``, ``/etc/pipewire``, and
``/usr/share/pipewire``, in that order of priority.
The three locations are intended for custom user configuration,
host-specific configuration and distribution-provided configuration,
respectively. At runtime, WirePlumber will search the directories
for the highest-priority directory to contain the needed configuration file.
This allows a user or system administrator to easily override the distribution
provided configuration files by placing an equally named file in the respective
directory.
It is also possible to override the configuration directory by setting the
``WIREPLUMBER_CONFIG_DIR`` environment variable::
WIREPLUMBER_CONFIG_DIR=src/config wireplumber
For convenience, the behaviour of the ``WIREPLUMBER_CONFIG_DIR`` environment
variable is the same as the ``PIPEWIRE_CONFIG_DIR`` environment variable.
If ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
configuration files are *only* looked up in this directory.
Location of scripts
-------------------
WirePlumber's default locations of its scripts are the same ones as for the
configuration files, but with the ``scripts`` directory appended.
Typically, these end up being ``$XDG_CONFIG_DIR/wireplumber/scripts``,
``/etc/wireplumber/scripts``, and ``/usr/share/wireplumber/scripts``,
in that order of priority.
The three locations are intended for custom user scripts,
host-specific scripts and distribution-provided scripts, respectively.
At runtime, WirePlumber will search the directories for the highest-priority
directory to contain the needed script.
It is also possible to override the scripts directory by setting the
``WIREPLUMBER_DATA_DIR`` environment variable::
WIREPLUMBER_DATA_DIR=src wireplumber
The "data" directory is a somewhat more generic path that may be used for
other kinds of data files in the future. For scripts, WirePlumber still expects
to find a ``scripts`` subdirectory in this "data" directory, so in the above
example the scripts would be in ``src/scripts``.
If ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
scripts are *only* looked up in this directory.
Location of modules
-------------------
WirePlumber modules
^^^^^^^^^^^^^^^^^^^
Like with configuration files, WirePlumber's default location of its modules is
determined at compile time by the build system. Typically, it ends up being
``/usr/lib/wireplumber-0.4`` (or ``/usr/lib/<arch-triplet>/wireplumber-0.4`` on
multiarch systems)
In more detail, this is controlled by the ``--libdir`` meson option. When
this is set to an absolute path, such as ``/lib``, the location of the
modules is set to be ``$libdir/wireplumber-$abi_version``. When this is set
to a relative path, such as ``lib``, then the installation prefix (``--prefix``)
is prepended to the path: ``$prefix/$libdir/wireplumber-$abi_version``.
It is possible to override this directory at runtime by setting the
``WIREPLUMBER_MODULE_DIR`` environment variable::
WIREPLUMBER_MODULE_DIR=build/modules wireplumber
PipeWire and SPA modules
^^^^^^^^^^^^^^^^^^^^^^^^
PipeWire and SPA modules are not loaded from the same location as WirePlumber's
modules. They are loaded from the location that PipeWire loads them.
It is also possible to override these locations by using environment variables:
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
PipeWire's documentation.

View file

@ -1,478 +0,0 @@
.. _config_main:
Main configuration file
=======================
The main configuration file is by default called ``wireplumber.conf``. This can
be changed on the command line by passing the ``--config-file`` or ``-c`` option::
wireplumber --config-file=bluetooth.conf
The ``--config-file`` option is useful to run multiple instances of wireplumber
that do separate tasks each. For more information on this subject, see the
:ref:`Multiple Instances <config_multi_instance>` section.
The format of this configuration file is the variant of JSON that is also
used in PipeWire configuration files. Note that this is subject to change
in the future.
All sections are essentially JSON objects. Lines starting with *#* are treated
as comments and ignored. The list of all possible section JSON objects are:
Common configs are present in the main configuration file(wireplumber.conf),
rest of the configs that can be grouped logically are grouped into separate
files and are placed under ``wireplumber.conf.d/``. More on this below.
* *context.properties*
Used to define properties to configure the PipeWire context and some modules.
Example::
context.properties = {
application.name = WirePlumber
log.level = 2
}
This sets the daemon's name to *WirePlumber* and the log level to *2*, which
only displays errors and warnings. See the
:ref:`Debug Logging <daemon_logging>` section for more details.
* *context.spa-libs*
Used to find spa factory names. It maps a spa factory name regular expression
to a library name that should contain that factory. The object property names
are the regular expression, and the object property values are the actual
library name::
<factory-name regex> = <library-name>
Example::
context.spa-libs = {
api.alsa.* = alsa/libspa-alsa
audio.convert.* = audioconvert/libspa-audioconvert
}
In this example, we instruct wireplumber to only any *api.alsa.** factory name
from the *libspa-alsa* library, and also any *audio.convert.** factory name
from the *libspa-audioconvert* library.
* *context.modules*
Used to load PipeWire modules. This does not affect the PipeWire daemon by any
means. It exists simply to allow loading *libpipewire* modules in the PipeWire
core that runs inside WirePlumber. This is usually useful to load PipeWire
protocol extensions, so that you can export custom objects to PipeWire and
other clients.
Users can also pass key-value pairs if the specific module has arguments, and
a combination of 2 flags: ``ifexists`` flag is given, the module is ignored when
not found; if ``nofail`` is given, module initialization failures are ignored::
{
name = <module-name>
[ args = { <key> = <value> ... } ]
[ flags = [ [ ifexists ] [ nofail ] ]
}
Example::
context.modules = [
{ name = libpipewire-module-adapter }
{
name = libpipewire-module-metadata,
flags = [ ifexists ]
}
]
The above example loads both PipeWire adapter and metadata modules. The
metadata module will be ignored if not found because of its ``ifexists`` flag.
* *wireplumber.components*
Used to load WirePlumber components. Components can be either WirePlumber
modules written in C or WirePlumber scripts written in Lua.
Syntax::
{ name = <component-name>, type = <component-type>, deps = <dependent-setting>, flags = <flags> }
* type:
Valid component types include:
* ``module``: A WirePlumber shared object module
* ``script/lua``: A WirePlumber Lua script
(all Lua Scripts implicitly requires libwireplumber-module-lua-scripting module)
Example::
wireplumber.components = [
{ name = libwireplumber-module-lua-scripting, type = module }
{ name = monitors/alsa.lua, type = script/lua }
]
* deps: components can be loaded with a dependency on a wireplumber setting.
* flags: ifexists & nofail flags are supported in this section as well.
* `ifexists` - signals wireplumber to ignore if the module is not found.
* `nofail` - signals wireplumber to ignore module initialization failures.
More Examples::
wireplumber.components = [
# Load `libwireplumber-module-si-node` which is of type `module`.
{ name = libwireplumber-module-si-node , type = module }
# Load `libwireplumber-module-reserve-device` module, only if the setting `alsa_monitor.alsa.reserve` is defined as true.
{ name = libwireplumber-module-reserve-device , type = module, deps = alsa_monitor.alsa.reserve }
# Load `alsa.lua` which is of type `script/lua`.
{ name = monitors/alsa.lua, type = script/lua }
# Load `alsa-midi.lua` Lua Script only if `alsa_monitor.alsa.midi` setting is defined as true.
{ name = monitors/alsa-midi.lua, type = script/lua, deps = alsa_monitor.alsa.midi }
# Load `libwireplumber-module-logind` module if the setting `bluez-enable-logind` is true.
{ name = libwireplumber-module-logind , type = module, deps = bluez-enable-logind, flags = [ ifexists ] }
]
.. note::
- `name` & `type` keys are mandatory, while `deps` and `flags` keys are optional
- All the components are loaded during the bootup and failure in finding them or any error during the loading process is a fatal error and WirePlumber will exit.
* *wireplumber.settings*
All the Wireplumber configuration settings are now grouped under this
section. They are moved away from Lua.
All the default settings are distributed into different
files(\*settings.conf) under ``wireplumber.conf.d\``
All the settings are loaded into ``sm-settings`` metadata. Apart from the
settings JSON files, Metadata interface can be used to change them.
:ref:`WpSettings <settings_api>` provides APIs to its clients
(modules, lua scripts etc) to access and track them.
Settings can be persistent, more on this below.
There can be two types of settings namely plain settings(called just settings
for reasons of simplicity) and rules.
* `Settings`
Syntax::
wireplumber.settings = {
<setting1> = <value>
<setting2> = <value>
..
}
Examples::
wireplumber.settings = {
alsa_monitor.alsa.reserve = true
alsa_monitor.alsa.midi = "true"
default-policy-duck.level = 0.3
bt-policy-media-role.applications = ["Firefox", "Chromium input"]
}
Value can be string, int, float, boolean and can even be a JSON array.
WpSettings exposes the `wp_settings_get_{string|int|float|boolean}()` APIs
to access the values.
Lua scripts, modules use these APIs to access settings.
The client accessing the setting should know which API to use to access
the setting accurately.
If the Setting is a JSON array like `bt-policy-media-role.applications`
_get_string() API need to be used and the obtained JSON element will have
to be parsed using the :ref:`JSON APIs. <spa_json_api>`
Persistent Behavior::
wireplumber.settings = {
persistent.settings = true
}
Persistent behavior can be enabled with the above syntax.
When enabled, the settings will be read from conf file only once and for
subsequent reboots they will be read from the state(cache) files, till the
time the setting is set back to false in the .conf file.
Settings can be changed through metadata, so when they are updated through
metadata and if the user desires those settings to be persistent between
reboots this persistent option can be used.
wp_settings_register_{callback|closure} () API can be used by clients to
keep track of the changes to settings.
The persistent behavior is disabled by default.
* `Rules`
Rules are dynamic logic based settings.
Syntax
Simple Syntax::
wireplumber.settings = {
<rule-name> = [
{
matches = [
{
<pipewire property1> = <value>
<pipewire property2> = <value>
}
]
actions = {
update-props = {
<pipewire property> = <value>,
<wireplumber setting> = <value>,
}
}
}
]
}
Simple Example::
wireplumber.settings = {
stream_default = [
{
matches = [
# Matches all devices
{ application.name = "pw-play" }
]
actions = {
update-props = {
state.restore-props = false
state.restore-target = false
}
}
}
]
}
Stream_default rule scans for pw-play app and if found it applies the two
properties listed above.
Advanced Syntax::
# Nested behavior
wireplumber.settings = {
<rule-name> = [
{
matches = [
{
# Logical AND behavior with the JSON object
<pipewire property1> = <value>
<pipewire property2> = <value>
}
# Logical OR behavior across the JSON objects.
{
<pipewire property3> = <value>
}
]
actions = {
update-props = {
<pipewire property> = <value>,
<wireplumber setting> = <value>,
}
}
}
]
}
# Use of regular expressions
wireplumber.settings = {
<rule-name> = [
{
matches = [
{
# if a value starts with ``~`` it triggers regular expression evaluation
<pipewire property1> = <~value*>
}
]
actions = {
update-props = {
<pipewire property> = <value>,
<wireplumber setting> = <value>,
}
}
}
]
}
# Multiple Matches with in a single rule is possible.
wireplumber.settings = {
<rule-name> = [
{
# Match 1
matches = [
{
<pipewire property1> = <~value*>
}
]
actions = {
update-props = {
<pipewire property1> = <value>,
}
}
# Match 2
matches = [
{
<pipewire property2> = <~value*>
}
]
actions = {
update-props = {
<pipewire property2> = <value>,
}
}
}
]
}
Advanced Example::
wireplumber.settings = {
alsa_monitor = [
{
matches = [
{
# This matches all sound cards.
device.name = "~alsa_card.*"
}
]
actions = {
update-props = {
# and applies these properties.
api.alsa.use-acp = true
}
}
}
{
matches = [
# Matches either input nodes or output nodes
{
node.name = "~alsa_input.*"
}
{
node.name = "~alsa_output.*"
}
]
actions = {
update-props = {
node.nick = "My Node"
priority.driver = 100
session.suspend-timeout-seconds = 5
}
}
}
]
}
* wp_settings_apply_rule () is WpSettings API for rules.
* *wireplumber.virtuals*
Virtual session items are a way of grouping different kinds of clients or
applications(for example Music, Voice, Navigation, Gaming etc).
The actual grouping is done based on the `media.role` of the client
stream node.
Virtual session items allows for that actions to be taken up at group level
rather than at individual stream level, which can be cumbersome.
For example imagine the following scenarios.
* Incoming Navigation message needs to duck the volume of
Audio playback(all the apps playing audio).
* Incoming voice/voip call needs to stop(cork) the Audio playback.
Virtual session items realize this functionality with ease.
* *Defining Virtual session items*
Example::
virtual-items = {
virtual-item.capture = {
media.class = "Audio/Source"
role = "Capture"
}
virtual-item.multimedia = {
media.class = "Audio/Sink"
role = "Multimedia"
}
virtual-item.navigation = {
media.class = "Audio/Sink"
role = "Navigation"
}
This example creates 3 virtual session items, with names
``virtual-item.capture``, ``virtual-item.multimedia`` and
``virtual-item.navigation`` and assigned roles ``Capture``, ``Multimedia``
and ``Navigation`` respectively.
First virtual item has a media class of ``Audio/Source`` used for capture
and rest of the virtual items have ``Audio/Sink`` media class, and so are
only used for playback.
* *Virtual session items config*
Example::
Capture = {
alias = [ "Multimedia", "Music", "Voice", "Capture" ]
priority = 25
action.default = "cork"
action.capture = "mix"
media.class = "Audio/Source"
}
Multimedia = {
alias = [ "Movie" "Music" "Game" ]
priority = 25
action.default = "cork"
}
Navigation = {
priority = 50
action.default = "duck"
action.Navigation = "mix"
}
The above example defines actions for both ``Multimedia`` and ``Navigation``
roles. Since the Navigation role has more priority than the Multimedia
role, when a client connects to the Navigation virtual session item, it
will ``duck`` the volume of all Multimedia clients. If Multiple Navigation
clients want to play audio, their audio will be mixed.
Possible values of actions are: ``mix`` (Mixes audio),
``duck`` (Mixes and lowers the audio volume) or ``cork`` (Pauses audio).
Virtual session items are not used for desktop use cases, it is more suitable
for embedded use cases.
* *Split Configuration files*
The Main configuration file is split into multiple files. When loading the main
JSON configuration file, WirePlumber will also look for additional files in the
same directory suffixed with ``.d`` and will load all of them as well. For
example, loading ``wireplumber.conf`` will also load any files under
``wireplumber.conf.d/``. It will load all the JSON config files there. All the
configurations are logically split into files and placed in this directory.

View file

@ -2,13 +2,12 @@
sphinx_files += files(
'conf_file.rst',
'components_and_profiles.rst',
'configuration_option_types.rst',
'modifying_configuration.rst',
'migration.rst',
'features.rst',
'settings.rst',
'locations.rst',
'main.rst',
'multi_instance.rst',
'alsa.rst',
'bluetooth.rst',
'policy.rst',
'access.rst',
)

View file

@ -0,0 +1,305 @@
.. _config_migration:
Migrating configuration from 0.4
================================
The configuration file format has changed in version 0.5. No automatic migration
of old configuration files is performed, so you will have to manually update
them. This document describes the changes and how to update your configuration.
wireplumber.conf
----------------
In WirePlumber 0.4, there used to be a ``.conf`` file, typically
``wireplumber.conf``, using the SPA-JSON format, that would list some Lua
scripts in the ``wireplumber.components`` section. These scripts were of type
``config/lua`` and they were called by default ``main.lua``, ``policy.lua`` and
``bluetooth.lua``.
Typical ``wireplumber.components`` section of a ``wireplumber.conf`` file in 0.4
would look like this:
.. code-block::
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file(s)
# Other components are loaded from there
{ name = main.lua, type = config/lua }
{ name = policy.lua, type = config/lua }
{ name = bluetooth.lua, type = config/lua }
]
These Lua "configuration" scripts were then looked up in the standard
configuration directories (``/usr/share/wireplumber``, ``/etc/wireplumber`` and
``~/.config/wireplumber``). The system also supported fragments of these scripts
to be placed in directories called ``main.lua.d``, ``policy.lua.d`` and
``bluetooth.lua.d`` respectively, in the same locations.
.. attention::
Starting with WirePlumber 0.5, Lua "configuration" files are **no longer
supported**.
If you attempt to start it with a ``wireplumber.conf`` that still
lists ``config/lua`` components in its ``wireplumber.components`` section, you
will see the following error message on the output:
Failed to load configuration: The configuration file at '...' is likely an
old WirePlumber 0.4 config and is not supported anymore. Try removing it.
As the message says, to resolve this you should remove the old
``wireplumber.conf`` file from the designated location. This should allow the
new WirePlumber to start using the default configuration that it ships with.
Lua configuration scripts
-------------------------
If you had custom Lua configuration scripts in the standard configuration
directories, such as *"main.lua.d"*, *"policy.lua.d"* or *"bluetooth.lua.d"*,
**you need to port them**.
Locations of files
~~~~~~~~~~~~~~~~~~
The first thing you need to know is that the new files should be placed in the
``~/.config/wireplumber/wireplumber.conf.d/`` directory instead of
``~/.config/wireplumber/main.lua.d/`` and such ...
In addition, since the new files are in the SPA-JSON format, they should have
the ``.conf`` extension instead of ``.lua``.
See also :ref:`config_locations`.
Porting device/node rules
~~~~~~~~~~~~~~~~~~~~~~~~~
One of the most common use-cases for these scripts was to set up properties
for devices and nodes using rules. Here is an example of an old rules script:
.. code-block:: lua
:caption: ~/.config/wireplumber/main.lua.d/51-alsa-pro-audio.lua
local rule = {
matches = {
{
{ "device.name", "matches", "alsa_card.*" },
},
},
apply_properties = {
["api.alsa.use-acp"] = false,
["device.profile"] = "pro-audio",
["api.acp.auto-profile"] = false,
["api.acp.auto-port"] = false,
},
}
table.insert(alsa_monitor.rules, rule)
This equivalent of this script in the new configuration format would look like
this:
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/51-alsa-pro-audio.conf
monitor.alsa.rules = [
{
matches = [
{
device.name = "~alsa_card.*"
}
]
actions = {
update-props = {
api.alsa.use-acp = false,
device.profile = "pro-audio"
api.acp.auto-profile = false
api.acp.auto-port = false
}
}
}
]
Another example of Bluetooth node rules:
.. code-block:: lua
:caption: ~/.config/wireplumber/bluetooth.lua.d/51-headphones.lua
local rule = {
matches = {
{
{ "node.name", "equals", "bluez_output.02_11_45_A0_B3_27.a2dp-sink" },
},
},
apply_properties = {
["node.nick"] = "Headphones",
},
}
table.insert(bluez_monitor.rules, rule)
This equivalent of this script in the new configuration format would look like:
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/51-headphones.conf
monitor.bluez.rules = [
{
matches = [
{
node.name = "bluez_output.02_11_45_A0_B3_27.a2dp-sink"
}
]
actions = {
update-props = {
node.nick = "Headphones"
}
}
}
]
See also :ref:`config_modifying_configuration_rules`.
Porting properties configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you had configuration scripts that were setting properties in tables such
as ``alsa_monitor.properties`` or ``bluez_monitor.properties``, then in many
cases porting to the new format can be done as follows:
.. code-block:: lua
:caption: ~/.config/wireplumber/bluetooth.lua.d/80-bluez-properties.lua
bluez_monitor.properties["bluez5.roles"] = "[ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]"
bluez_monitor.properties["bluez5.hfphsp-backend"] = "native"
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-bluez-properties.conf
monitor.bluez.properties = {
bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
bluez5.hfphsp-backend = "native"
}
See also :ref:`config_modifying_configuration_static`.
In a lot of cases, however, these properties have been promoted to become either
:ref:`Settings <config_modifying_configuration_settings>` or
:ref:`Features <config_modifying_configuration_features>`.
Here are some common examples:
Disabling the D-Bus device reservation API in the ALSA monitor:
* Old format:
.. code-block:: lua
:caption: ~/.config/wireplumber/main.lua.d/80-disable-alsa-reserve.lua
alsa_monitor.properties["alsa.reserve"] = false
* New format:
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-disable-alsa-reserve.conf
wireplumber.profiles = {
main = {
monitor.alsa.reserve-device = disabled
}
}
Disabling seat monitoring via logind in the BlueZ monitor:
* Old format:
.. code-block:: lua
:caption: ~/.config/wireplumber/bluetooth.lua.d/80-disable-logind.lua
bluez_monitor.properties["with-logind"] = false
* New format:
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-disable-logind.conf
wireplumber.profiles = {
main = {
monitor.bluez.seat-monitoring = disabled
}
}
See also :ref:`config_modifying_configuration_features`.
Linking policy configuration (moved to settings and renamed):
* Old format:
.. code-block:: lua
:caption: ~/.config/wireplumber/policy.lua.d/80-policy.lua
default_policy.policy = {
["move"] = false,
["follow"] = false,
}
* New format:
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-policy.conf
wireplumber.settings = {
linking.allow-moving-streams = false
linking.follow-default-target = false
}
See also :ref:`config_modifying_configuration_settings` and remember that
settings can also be changed at runtime via :command:`wpctl`.
Loading custom scripts
~~~~~~~~~~~~~~~~~~~~~~
If you had custom Lua scripts that were loaded by the old configuration file,
you need to port the old ``load_script()`` commands into component descriptions.
For example, if you had a script that was loaded like this:
.. code-block:: lua
:caption: ~/.config/wireplumber/main.lua.d/99-my-script.lua
load_script("my-script.lua")
You should now create a new component description in the configuration file
and also make sure to require it in the profile:
.. code-block::
:caption: ~/.config/wireplumber/wireplumber.conf.d/99-my-script.conf
wireplumber.components = [
{
name = my-script.lua, type = script/lua
provides = custom.my-script
}
]
wireplumber.profiles = {
main = {
custom.my-script = required
}
}
.. attention::
Another important thing to mention here is the location of custom scripts. In
0.4, scripts could be loaded in configuration locations such as
``~/.config/wireplumber/scripts/`` and ``/etc/wireplumber/scripts/``. In 0.5,
the XDG base directory specification for data files is honored, so the new
location for custom scripts is ``~/.local/share/wireplumber/scripts/`` and
anything else specified in ``$XDG_DATA_HOME`` and ``$XDG_DATA_DIRS``. See
:ref:`daemon_file_locations` for more information.

View file

@ -0,0 +1,365 @@
.. _config_modifying_configuration:
Modifying configuration
=======================
WirePlumber is a heavily modular daemon that depends on its configuration
file to operate. If you were to start WirePlumber with an empty configuration
file, it would fail to start. This is why the default configuration file is
installed in the system-wide application data directory, which prevents it from
being modified by the user.
It is technically possible, if you wish, to copy the default configuration
file in one of the other :ref:`configuration search locations <config_locations>`
and modify it. However, this is **not recommended**, as it may lead to issues
when upgrading WirePlumber.
In the :ref:`Configuration file <config_conf_file>` section, we saw that
configuration files support fragments, which allow you to override or extend the
default configuration. This is the recommended way to modify the configuration.
Working with fragments
----------------------
The easiest way to add :ref:`fragments <config_conf_file_fragments>` to
modify the default configuration is to create a directory called
``~/.config/wireplumber/wireplumber.conf.d`` and place your fragments there.
All fragment files need to have the ``.conf`` extension and must be valid
SPA-JSON files. The fragments are loaded in alphanumerical order, so you can
control the order in which they are loaded by naming them accordingly. It is
recommended to use a numeric prefix for the file names, e.g.
``10-my-fragment.conf``, ``20-my-other-fragment.conf``, etc., so that you can
easily control the order in which they are loaded.
.. _config_modifying_configuration_features:
Customizing the loaded features
-------------------------------
As seen in the :ref:`Components & Profiles <config_components_and_profiles>`
section, the list of components that are loaded can be customized by enabling or
disabling :ref:`well-known features <config_features>` in the profile that is
in use by WirePlumber.
The default profile of WirePlumber is called ``main``, so a fragment that
enables or disables a specific feature in the default configuration should look
like this:
.. code-block::
wireplumber.profiles = {
main = {
some.feature.name = disabled
some.other.feature.name = required
}
}
Remember that features can be ``required``, ``optional`` or ``disabled``. See
the :ref:`Components & Profiles <config_components_and_profiles>` for details.
.. _config_modifying_configuration_settings:
Modifying dynamic options ("settings")
--------------------------------------
As seen in the :ref:`Configuration option types <config_configuration_option_types>`
section, WirePlumber components can be partly configured with dynamic options
(referred to as "settings"). These settings can either be modified permanently
in the configuration file, or they can be modified at runtime using the
``wpctl`` command-line tool.
To modify a setting in the configuration file, you can use a fragment like this:
.. code-block::
wireplumber.settings = {
some.setting.name = value
}
For example, setting the ``device.routes.default-sink-volume`` setting to
``0.5`` can be done like this:
.. code-block::
wireplumber.settings = {
device.routes.default-sink-volume = 0.5
}
.. note::
Since the configuration file is only read at startup, this will only take
effect after restarting WirePlumber.
If you would prefer to change the setting at runtime, you can use ``wpctl`` as
follows:
.. code-block:: bash
$ wpctl settings device.routes.default-sink-volume 0.5
Updated setting 'device.routes.default-sink-volume' to: 0.5
The above command changes the setting immediately, but for the current
WirePlumber instance only. If you want the setting to be applied every time
WirePlumber is started, you may also use the ``--save`` option:
.. code-block:: bash
$ wpctl settings --save device.routes.default-sink-volume 0.5
Updated and saved setting 'device.routes.default-sink-volume' to: 0.5
This will save the setting persistently in WirePlumber's state storage.
Even though it is not in the configuration file, this saved value will be
applied automatically when WirePlumber is started.
.. attention::
When a setting's value is saved, it will override the value from the
configuration file. Changing the value in the configuration file will
have no effect until the saved value is removed. Use the ``--delete``
switch in ``wpctl`` to remove a saved value (see below).
With ``wpctl``, it is also possible to restore a setting to its default value
(taken from the schema), by using the ``--reset`` option. For example, to reset
the ``device.routes.default-sink-volume`` setting, the following command can be
used:
.. code-block:: bash
$ wpctl settings --reset device.routes.default-sink-volume
Reset setting 'device.routes.default-sink-volume' successfully
$ wpctl settings device.routes.default-sink-volume
Value: 0.064 (Saved: 0.5)
Note that the ``--reset`` option will only reset the setting to its default
value, but it will not remove the saved value from the state file. If you want
to remove the saved value, you can use the ``--delete`` option:
.. code-block:: bash
$ wpctl settings --delete device.routes.default-sink-volume
Deleted setting 'device.routes.default-sink-volume' successfully
$ wpctl settings device.routes.default-sink-volume
Value: 0.064
A list of all the available settings can be found in the :ref:`config_settings`
section.
.. _config_modifying_configuration_static:
Modifying static options
------------------------
Static options always live in their own section of the configuration file.
Sections can be of two types: either a JSON object or a JSON array.
When dealing with a **JSON object**, you can add or modify a key-value pair by
creating a fragment like this:
.. code-block::
wireplumber.some-section = {
some.option = new_value
}
This is similar to what we have seen also above for modifying profile features
and settings (because both are JSON objects).
When dealing with a **JSON array**, any values that you define in a fragment
will be appended to the array. For example, to add a new rule to the
``monitor.alsa.rules`` array, you can create a fragment like this:
.. code-block::
monitor.alsa.rules = [
{
matches = [
{
device.name = "~alsa_card.*"
}
]
actions = {
update-props = {
api.alsa.use-ucm = false
}
}
}
]
This will add a new rule to the ``monitor.alsa.rules`` array, which will
be evaluated **after** all other rules that were parsed before. This is where
the order in which fragments are loaded actually matters.
If you don't want to append a new rule, but rather override the entire array
with a new one, you can do so by using the ``override.`` prefix on the array
name:
.. code-block::
override.monitor.alsa.rules = [
{
matches = [
{
device.name = "~alsa_card.*"
}
]
actions = {
update-props = {
api.alsa.use-ucm = false
}
}
}
]
This will now replace the entire ``monitor.alsa.rules`` array with this new one.
.. attention::
If you want to remove a rule from the array, you will need to override the
whole array with a new one that does not contain the rule you want to remove.
There is no way to remove a specific element from an array using fragments.
Another thing worth remembering here is that this behavior of appending values
to arrays also works in arrays that are nested inside other arrays or objects.
For example, consider this fragment:
.. code-block::
monitor.bluez.properties = {
bluez5.codecs = [ sbc_xq aac ldac ]
}
If this is the first time that the ``bluez5.codecs`` array is being defined, it
will be created with the given values. If it already exists, the given values
will be appended to the existing array. If you want to make sure that this
fragment will override the existing array, you need to use the ``override.``
prefix on the array name:
.. code-block::
monitor.bluez.properties = {
override.bluez5.codecs = [ sbc_xq aac ldac ]
}
The ``override.`` prefix may also be used in JSON object keys, to override the
entire object with a new one. For example, to override the entire
``monitor.bluez.properties`` object, you can use a fragment like this:
.. code-block::
override.monitor.bluez.properties = {
bluez5.codecs = [ sbc_xq aac ldac ]
}
Here, the entire ``monitor.bluez.properties`` object will be replaced with the
new one, and all previous key-value pairs configured will be discarded. This
also means that the ``bluez5.codecs`` array will be replaced with the new one
and does not require the ``override.`` prefix.
.. note::
Even though WirePlumber uses PipeWire's syntax for configuration files, the
``override.`` prefix is a WirePlumber extension and does not work in
PipeWire.
.. _config_modifying_configuration_rules:
Working with rules
------------------
Some of the static option sections in the configuration file are used to define
rules that are evaluated by WirePlumber at runtime. These rules are typically
used to match objects and perform actions on them. For example, the
``monitor.alsa.rules`` section is used to define rules that are evaluated by
the ALSA monitor to match ALSA devices and update their properties.
The syntax of these rules is the same as the syntax of
`PipeWire's rules <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire#rules>`_.
A rule is always a JSON object with two keys: ``matches`` and ``actions``. The
``matches`` key is used to define the conditions that need to be met for the
rule to be evaluated as true, and the ``actions`` key is used to define the
actions that are performed when the rule is evaluated as true.
The ``matches`` key is always a JSON array of objects, where each object
defines a condition that needs to be met. Each condition is a list of key-value
pairs, where the key is the name of the property that is being matched, and the
value is the value that the property needs to have. Within a condition, all
the key-value pairs are combined with a logical AND, and all the conditions in
the ``matches`` array are combined with a logical OR.
The ``actions`` key is always a JSON object, where each key-value pair defines
an action that is performed when the rule is evaluated as true. The action
name is specific to the rule and is defined by the rule's documentation, but
most frequently you will see the ``update-props`` action, which is used to
update the properties of the matched object.
For example:
.. code-block::
some.theoretical.rules = [
{
matches = [
{
object.name = "my_object"
object.profile.name = "my_profile"
}
{
object.name = "other_object"
}
]
actions = {
update-props = {
object.tag = "matched_by_my_rule"
}
}
}
]
This rule is equivalent to the following expression:
.. code-block:: python
if (properties["object.name"] == "my_object" and properties["object.profile.name"] == "my_profile") or (properties["object.name"] == "other_object"):
properties["object.tag"] = "matched_by_my_rule"
In the ``matches`` array, it is also possible to use regular expressions to match
property values. For example, to match all nodes with a name that starts with
``my_``, you can use the following condition:
.. code-block::
matches = [
{
node.name = "~my_.*"
}
]
The ``~`` character signifies that the value is a regular expression. The exact
syntax of the regular expressions is the POSIX extended regex syntax, as
described in the `regex (7)` man page.
In addition to regular expressions, you may also use the ``!`` character to
negate a condition. For example, to match all nodes with a name that does not
start with ``my_``, you can use the following condition:
.. code-block::
matches = [
{
node.name = "!~my_.*"
}
]
The ``!`` character can be used with or without a regular expression. For
example, to match all nodes with a name that is not equal to ``my_node``,
you can use the following condition:
.. code-block::
matches = [
{
node.name = "!my_node"
}
]

View file

@ -1,45 +0,0 @@
.. _config_multi_instance:
Running multiple instances
==========================
WirePlumber has the ability to run either as a single instance daemon or as
multiple instances, meaning that there can be multiple processes, each one
doing a different task.
In the default configuration, both setups are supported. The default is to run
in single-instance mode.
In single-instance mode, WirePlumber reads ``wireplumber.conf``, which is the
default configuration file, and from there it loads ``main.lua``, ``policy.lua``
and ``bluetooth.lua``, which are lua configuration files (deployed as directories)
that enable all the relevant functionality.
In multi-instance mode, WirePlumber is meant to be started with the
``--config-file`` command line option 3 times:
.. code-block:: console
$ wireplumber --config-file=main.conf
$ wireplumber --config-file=policy.conf
$ wireplumber --config-file=bluetooth.conf
That loads one process which reads ``main.conf``, which then loads ``main.lua``
and enables core functionality. Then another process that reads ``policy.conf``,
which then loads ``policy.lua`` and enables policy functionality... and so on.
To make this easier to work with, a template systemd unit is provided, which is
meant to be started with the name of the main configuration file as a
template argument:
.. code-block:: console
$ systemctl --user disable wireplumber # disable the single instance
$ systemctl --user enable wireplumber@main
$ systemctl --user enable wireplumber@policy
$ systemctl --user enable wireplumber@bluetooth
It is obviously possible to start as many instances as desired, with manually
crafted configuration files, as long as it is ensured that these instances
serve a different purpose and they do not conflict with each other.

View file

@ -1,347 +0,0 @@
.. _config_policy:
Policy Configuration
====================
wireplumber.conf.d/policy.conf
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This file contains generic default policy properties that can be configured.
* *Settings*
Example:
.. code-block::
wireplumber.properties = {
default-policy-move = true
}
The above example will set the ``move`` policy property to ``true``.
The list of supported properties are:
.. code-block::
default-policy-move = true
Moves session items when metadata ``target.node`` changes.
.. code-block::
default-policy-follow = true
Moves session items to the default device when it has changed.
.. code-block::
default-policy-audio.no-dsp = false
Set to ``true`` to disable channel splitting & merging on nodes and enable
passthrough of audio in the same format as the format of the device. Note that
this breaks JACK support; it is generally not recommended.
.. code-block::
default-policy-duck.level = 0.3
How much to lower the volume of lower priority streams when ducking. Note that
this is a linear volume modifier (not cubic as in PulseAudio).
Filters
^^^^^^^
* *Introduction*
A pair of nodes will be considered filter nodes by wireplumber if they have the
"node.link-group" property set to a common value. This propery is always set by
PipeWire when creating filter nodes if they are defined in the PipeWire's
configuration file. The pair of nodes always consist of a stream node, and a
main node. When using the filter nodes, the main node acts as a virtual device,
where the audio is sent or captured to/from; and the stream node acts as a
virtual stream, where the audio is sent or received to/from the next node in the
graph.
For example, the media class of the nodes for a input filter would be:
- main node: Audio/Sink
- stream node: Stream/Output/Audio
And, if this filter is used between an application stream, and the default audio
device, the graph would look like this:
.. code-block::
application stream node -> filter main node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
filter stream node -> default device node
(Stream/Output/Audio) (Audio/Sink)
On the other hand, the media class of the nodes for an output filter would be:
- main node: Audio/Source
- stream node: Stream/Input/Audio
And the same logic is applied if they are used, but in the opposite direction.
This is how the graph would look like if an application wants to capture audio
from a device that uses an input filter.
.. code-block::
application stream node <- filter main node
(Stream/Input/Audio) (Audio/Source)
.. code-block::
filter stream node <- default device node
(Stream/Input/Audio) (Audio/Source)
Finally, if multiple filters have the same direction, they can also be chained
together so that the audio of a filter is sent to the input of the next filter.
Example of existing filters in PipeWire are echo-cancel, filter-chain and
loopback nodes.
The next section will describe how we can define filter properties so that they
are automatically linked by the wirepluber policy in any way we want.
* *Filter properties*
Currently, if a filter node is created, wireplumber will check the following
optional node properties on the main node:
- filter.smart:
Boolean indicating whether smart policy will be used in the filter nodes or
not. This is disabled by default, therefore filter nodes will be treated as
regular nodes, without applying any kid of extra logic. On the other hand, if
this property is set to true, automatic (smart) filter policy will be used
when linking filters. The properties below will instruct the smart policy how
to link the filters automatically.
- filter.smart.name:
The unique name of the filter. WirePlumber will use the "node.link-group"
property as filter name if this property is not set.
- filter.smart.disabled:
Boolean indicating whether the filter should be disabled at all or not. A
disabled filter will never be used in any circumstances. If the property is
not set, wireplumber will consider the filter not disabled by default.
- filter.smart.target:
A JSON object that defines the matching properties of the filter's target node.
A filter target can never be another filter node (wireplumber will ignore it),
and must always be a device node. If this property is not set, WirePlumber will
use the default node as target.
- filter.smart.before:
A JSON array with the filters names that are supposed to be used before this
filter. If not set, wireplumber will link the filters by order of creation.
- filter.smart.after:
A JSON array with the filters names that are supposed to be used after this
filter. If not set, wireplumber will link the filters by order of creation.
Note that these properties must be set in the filter's main node, not the
filter's stream node.
As an example, we will describe here how to create 2 loopback filters in the
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
linked with the default audio device, and use loopback-2 filter as the last
filter in the chain.
The PipeWire configuration files for the 2 filters should be like this:
- /usr/share/pipewire/pipewire.conf.d/loopback-1.conf:
.. code-block::
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-1-sink
node.description = "Loopback 1 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart.name = loopback-1
filter.smart.disabled = false
filter.smart.before = [ loopback-2 ]
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
node.dont-remix = true
}
}
}
]
- /usr/share/pipewire/pipewire.conf.d/loopback-2.conf:
.. code-block::
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-2-sink
node.description = "Loopback 2 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart.name = loopback-2
filter.smart.disabled = false
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
node.dont-remix = true
}
}
}
]
Finally, if we restart PipeWire and WirePlumber to apply the configuration
changes, and play a test.wave audio file with paplay to see if wireplumber links
the filter nodes properly, the graph should look like this:
.. code-block::
paplay node -> loopback-1 main node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
loopback-1 stream node -> loopback-1 main node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
loopback-2 stream node -> default device node
(Stream/Output/Audio) (Audio/Sink)
If we remove `filter.smart.before = [ loopback-2 ]` property from the loopback-1
filter, and add a `filter.smart.before = [ loopback-1 ]` property in the loopback-2
filter configuration file. WirePlumber should link the loopback-1 filter as the last
filter in the chain, like this:
.. code-block::
paplay node -> loopback-2 main node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
loopback-2 stream node -> loopback-1 main node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
loopback-1 stream node -> default device node
(Stream/Output/Audio) (Audio/Sink)
On the other hand, the filters can have different targets. For example, we can
define the filters like this:
- `/usr/share/pipewire/pipewire.conf.d/loopback-1.conf`:
.. code-block::
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-1-sink
node.description = "Loopback 1 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart.name = loopback-1
filter.smart.disabled = false
filter.smart.before = [ loopback-2 ]
filter.smart.target = { node.name = "not-default-audio-device-name" }
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
node.dont-remix = true
}
}
}
]
- `/usr/share/pipewire/pipewire.conf.d/loopback-2.conf`:
.. code-block::
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-2-sink
node.description = "Loopback 2 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart.name = loopback-2
filter.smart.disabled = false
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
node.dont-remix = true
}
}
}
]
If this is the case, WirePlumber will link the filters like this when using
paplay:
.. code-block::
paplay node -> loopback-2 main node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
loopback-2 stream node -> default device node
(Stream/Output/Audio) (Audio/Sink)
.. code-block::
loopback-1 stream node -> not-default-audio-device-name device node
(Stream/Output/Audio) (Audio/Sink)
The loopback-1 main node will only be used if an application wants to play audio
on the device node with node name "not-default-audio-device-name".
* *Filters metadata*
Similar to the default metadata, it is also possible to override the filter
properties by using the "filters" metadata. This allow users to change the filters
policy at runtime.
For example, if loopback-1 main node Id is `40`, we can disable the filter by
setting its "filter.smart.disabled" metadata key to true using the `pw-metadata`
tool:
.. code-block::
$ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON
We can also change the target of a filter at runtime:
.. code-block::
$ pw-metadata -n filters 40 "filter.smart.target" { node.name = "new-target-node-name" } Spa:String:JSON
Every time a key in the filters metadata changes, all filters are unlinked and
re-linked properly by the policy.

View file

@ -1,7 +1,7 @@
.. _config_settings:
WirePlumber Settings
====================
Well-known settings
===================
This section describes the settings that can be configured on WirePlumber.
@ -9,4 +9,220 @@ Settings can be either configured statically in the configuration file
by setting them under the ``wireplumber.settings`` section, or they can be
configured dynamically at runtime by using metadata.
.. include:: ../../../../src/scripts/lib/SETTINGS.rst
For more information on what "settings" are and how they work, refer to the
previous section: :ref:`config_configuration_option_types`.
.. describe:: device.restore-profile
When a device profile is changed manually (e.g. via pavucontrol), WirePlumber
stores the selected profile and restores it when the device appears again
(e.g. after a reboot). If this setting is disabled, WirePlumber will always
pick the best profile for the device based on profile priorities and
availability (or custom rules, if any).
:Default value: ``true``
.. describe:: device.restore-routes
When a device route is changed manually (e.g. via pavucontrol), WirePlumber
stores the selected route and restores it when the same profile is
selected for this device. If this setting is disabled, WirePlumber will
always pick the best route for this device profile based on route priorities
and availability (or custom rules, if any).
This setting also enables WirePlumber to restore properties of the device
route when the route is restored. This includes the volume levels of sources
and sinks, as well as the IEC958 codecs selected (for routes that support
encoded streams, such as HDMI).
:Default value: ``true``
.. describe:: device.routes.default-sink-volume
This option allows to set the default volume for sinks that are part of a
device route (e.g. ALSA PCM sinks). This is used when the route is restored
and the sink does not have a previously stored volume.
It is possible to override the value on a per-device basis with a property
(*not* a setting, so this would go into a configuration file) on the device
named ``device.routes.default-sink-volume``.
:Default value: ``0.4 ^ 3`` (40% on the cubic scale)
.. describe:: device.routes.default-source-volume
This option allows to set the default volume for sources that are part of a
device route (e.g. ALSA PCM sources). This is used when the route is restored
and the source does not have a previously stored volume.
It is possible to override the value on a per-device basis with a property
(*not* a setting, so this would go into a configuration file) on the device
named ``device.routes.default-source-volume``.
:Default value: ``1.0`` (100%)
.. describe:: linking.allow-moving-streams
This option allows moving streams by overriding their target via metadata.
When enabled, WirePlumber monitors the "default" metadata for changes in the
``target.object`` key of streams and if this key is set to a valid node name
(``node.name``) or serial (``object.serial``), the stream is moved to that
target node.
This is used by applications such as pavucontrol and is recommended for
compatibility with PulseAudio.
.. note::
On the metadata, the ``target.node`` key is also supported for
compatibility with older versions of PipeWire, but it is deprecated.
Please use the ``target.object`` key instead.
:Default value: ``true``
:See also: ``node.stream.restore-target``
.. describe:: linking.follow-default-target
When a stream was started with the ``target.object`` property, WirePlumber
normally links that stream to that target node and ignores the "default"
target for that direction. However, if this option is enabled, WirePlumber
will check if the designated target node *is* the "default" target and if so,
it will act as if the stream did not have that property.
In practice, this means that if the "default" target changes at runtime,
the stream will be moved to the new "default" target.
This is what Pulseaudio does and is implemented here for compatibility
with some applications that do start with a ``target.object`` property
set to the "default" target and expect the stream to be moved when the
"default" target changes.
Note that this logic is only applied on client (i.e. application) streams
and *not* on filters.
:Default value: ``true``
.. describe:: linking.pause-playback
When an audio sink is removed, pause media players that have streams
playing to it. Pausing is done via MPRIS interface.
:Default value: ``true``
.. describe:: node.features.audio.no-dsp
When this option is set to ``true``, audio nodes will not be configured
in dsp mode, meaning that their channels will *not* be split into separate
ports and that the audio data will *not* be converted to the float 32 format
(F32P). Instead, devices will be configured in passthrough mode and streams
will be configured in convert mode, so that their audio data is converted
directly to the format that the device is expecting.
This may be useful if you are trying to minimize audio processing for an
embedded system, but it is not recommended for general use.
.. warning::
This option **will break** compatibility with JACK applications
and may also break certain patchbay applications. Do not enable, unless
you understand what you are doing.
:Default value: ``false``
.. describe:: node.features.audio.monitor-ports
This enables the creation of "monitor" ports for audio nodes. Monitor ports
are created on nodes that have input ports (i.e. sinks and capture streams)
and allow monitoring of the audio data that is being sent to the node.
This is mostly used by monitoring applications, such as pavucontrol.
:Default value: ``true``
.. describe:: node.features.audio.control-port
This enables the creation of a "control" port for audio nodes. Control ports
allow sending MIDI data to the node, allowing for control of certain node's
parameters (such as volume) via external controllers.
:Default value: ``false``
.. describe:: node.stream.restore-props
WirePlumber stores stream parameters such as volume and mute status for each
client (i.e. application) stream. If this setting is enabled, WirePlumber
will restore the previously stored stream parameters when the stream is
activated. If it is disabled, stream parameters will be initialized to their
default values.
:Default value: ``true``
.. describe:: node.stream.restore-target
When a client (i.e. application) stream is manually moved to a different
target node (e.g. via pavucontrol), the target node is stored by WirePlumber.
If this setting is enabled, WirePlumber will restore the previously stored
target node when the stream is activated.
.. note::
This does not restore manual links made by patchbay applications. This
is only meant to restore the ``target.object`` property in the "default"
metadata, which is manipulated by applications such as pavucontrol when
a stream is moved to a different target.
:Default value: ``true``
:See also: ``linking.allow-moving-streams``
.. describe:: node.stream.default-playback-volume
The default volume for playback streams to be applied when the stream is
activated. This is only applied when ``node.stream.restore-props`` is
``true`` and the stream does not have a previously stored volume.
:Default value: ``1.0``
:Range: ``0.0`` to ``1.0``
.. describe:: node.stream.default-capture-volume
The default volume for capture streams to be applied when the stream is
activated. This is only applied when ``node.stream.restore-props`` is
``true`` and the stream does not have a previously stored volume.
:Default value: ``1.0``
:Range: ``0.0`` to ``1.0``
.. describe:: node.filter.forward-format
When a "filter" pair of nodes (such as echo-cancel or filter-chain) is
linked to a device node that has a different channel map than the filter
nodes, this option allows the channel map of the filter nodes to be changed
to match the channel map of the device node. The change is applied to both
ends of the "filter", so that any streams linked to the filter are also
reconfigured to match the target channel map.
This is useful, for instance, to make sure that an application will be
properly configured to output surround audio to a surround device, even
when going through a filter that was not explicitly configured to have
a surround channel map.
:Default value: ``false``
.. describe:: node.restore-default-targets
This setting enables WirePlumber to store and restore the "default" source
and sink targets of the graph. In PulseAudio terminology, this is also known
as the "fallback" source and sink.
When this setting is enabled, WirePlumber will store the "default" source
and sink targets when they are changed manually (e.g. via pavucontrol) and
restore them when the available nodes change or after a reload/restart.
It will also store a history of past selected "default" targets and restore
previously selected ones if the currently selected are not available.
If this is disabled, WirePlumber will pick the best available source
and sink targets based on their priorities, but it will also respect
manual user selections that are done at runtime - it will just not remember
them so that it can restore them at a later time.
:Default value: ``true``

View file

@ -8,8 +8,8 @@ Dependencies
In order to compile WirePlumber you will need:
* GLib >= 2.62
* PipeWire 0.3 (>= 0.3.43)
* GLib >= 2.68
* PipeWire >= 1.0
* Lua 5.3 or 5.4
Lua is optional in the sense that if it is not found in the system, a bundled

View file

@ -0,0 +1,175 @@
.. _daemon_file_locations:
Locations of WirePlumber's files
================================
.. _config_locations:
Location of configuration files
-------------------------------
WirePlumber's default locations of its configuration files are the following,
in order of priority:
1. ``$XDG_CONFIG_HOME/wireplumber``
2. ``$XDG_CONFIG_DIRS/wireplumber``
3. ``$sysconfdir/wireplumber``
4. ``$XDG_DATA_DIRS/wireplumber``
5. ``$datadir/wireplumber``
Notes:
* ``$syscondir`` and ``$datadir`` refer to
`meson's directory options <https://mesonbuild.com/Builtin-options.html#directories>`_
and are hardcoded at build time
* ``$XDG_`` variables refer to the
`XDG Base Directory Specification <https://specifications.freedesktop.org/basedir-spec/latest/index.html>`_
It is recommended that user specific overrides are placed in
``$XDG_CONFIG_HOME/wireplumber``, while host-specific configuration is placed in
``$XDG_CONFIG_DIRS/wireplumber`` or ``$sysconfdir/wireplumber`` and
distribution-provided configuration is placed in ``$XDG_DATA_DIRS/wireplumber``
or ``$datadir/wireplumber``.
At runtime, WirePlumber will seek out the directory with the highest priority
that contains the required configuration file. This setup allows a user or
system administrator to effortlessly override the configuration files provided
by the distribution. They can achieve this by placing a file with an identical
name in a higher priority directory.
It is also possible to override the configuration directory by setting the
``WIREPLUMBER_CONFIG_DIR`` environment variable:
.. code-block:: bash
WIREPLUMBER_CONFIG_DIR=src/config wireplumber
``WIREPLUMBER_CONFIG_DIR`` supports listing multiple directories, using the
standard path list separator ``:``. If multiple directories are specified,
the first one has the highest priority and the last one has the lowest.
.. note::
When the configuration directory is overridden with
``WIREPLUMBER_CONFIG_DIR``, the default locations are ignored and
configuration files are *only* looked up in the directories specified by this
variable.
.. _config_locations_fragments:
Configuration fragments
^^^^^^^^^^^^^^^^^^^^^^^
WirePlumber also supports configuration fragments. These are configuration files
that are loaded in addition to the main configuration file, allowing to
override or extend the configuration without having to copy the whole file.
See also the :ref:`config_conf_file_fragments` section for semantics.
Configuration fragments are always loaded from subdirectories of the main search
directories that have the same name as the configuration file, with the ``.d``
suffix appended. For example, if WirePlumber loads ``wireplumber.conf``, it will
also load ``wireplumber.conf.d/*.conf``. Note also that the fragment files need
to have the ``.conf`` suffix.
When WirePlumber loads a configuration file from the default locations, it will
also load all configuration fragments that are present in all of the default
locations, but following the reverse order of priority. This allows
configuration fragments that are installed in more system-wide locations to be
overridden by the system administrator or the users.
For example, assuming WirePlumber loads ``wireplumber.conf``, from any of the
search locations, it will also locate and load the following fragments, in this
order:
1. ``$datadir/wireplumber/wireplumber.conf.d/*.conf``
2. ``$XDG_DATA_DIRS/wireplumber/wireplumber.conf.d/*.conf``
3. ``$sysconfdir/wireplumber/wireplumber.conf.d/*.conf``
4. ``$XDG_CONFIG_DIRS/wireplumber/wireplumber.conf.d/*.conf``
5. ``$XDG_CONFIG_HOME/wireplumber/wireplumber.conf.d/*.conf``
Within each search location that contains fragments, the individual fragment
files are opened in alphanumerical order. This can be important to know, because
the parsing order matters in merging. See :ref:`config_conf_file_fragments`
.. note::
When ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
fragment files are *only* looked up in the directories specified by this
variable.
.. _config_locations_scripts:
Location of scripts
-------------------
WirePlumber's default locations of its data files are the following,
in order of priority:
1. ``$XDG_DATA_HOME/wireplumber``
2. ``$XDG_DATA_DIRS/wireplumber``
3. ``$datadir/wireplumber``
At runtime, WirePlumber will search the directories for the highest-priority
directory to contain the needed data file.
Scripts are a specific kind of "data" files and are expected to be located
within a ``scripts`` subdirectory in the above data search locations. The "data"
directory is a somewhat more generic path that may be used for other kinds of
data files in the future.
It is also possible to override the data directory by setting the
``WIREPLUMBER_DATA_DIR`` environment variable:
.. code-block:: bash
WIREPLUMBER_DATA_DIR=src wireplumber
As with the default data directories, script files in particular are expected
to be located within a ``scripts`` subdirectory, so in the above example the
scripts would actually reside in ``src/scripts``.
``WIREPLUMBER_DATA_DIR`` supports listing multiple directories, using the
standard path list separator ``:``. If multiple directories are specified,
the first one has the highest priority and the last one has the lowest.
.. note::
When ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
scripts are *only* looked up in the directories specified by this variable.
Location of modules
-------------------
WirePlumber modules
^^^^^^^^^^^^^^^^^^^
WirePlumber's default location of its modules is
``$libdir/wireplumber-$api_version``, where ``$libdir`` is set at compile time
by the build system. Typically, it ends up being ``/usr/lib/wireplumber-0.5``
(or ``/usr/lib/<arch-triplet>/wireplumber-0.5`` on multiarch systems)
It is possible to override this directory at runtime by setting the
``WIREPLUMBER_MODULE_DIR`` environment variable:
.. code-block:: bash
WIREPLUMBER_MODULE_DIR=build/modules wireplumber
``WIREPLUMBER_MODULE_DIR`` supports listing multiple directories, using the
standard path list separator ``:``. If multiple directories are specified, the
first one has the highest priority and the last one has the lowest.
.. note::
When ``WIREPLUMBER_MODULE_DIR`` is set, the default locations are ignored and
scripts are *only* looked up in the directories specified by this variable.
PipeWire and SPA modules
^^^^^^^^^^^^^^^^^^^^^^^^
PipeWire and SPA modules are not loaded from the same location as WirePlumber's
modules. They are loaded from the location that PipeWire loads them.
It is also possible to override these locations by using environment variables:
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
PipeWire's documentation.

View file

@ -127,6 +127,42 @@ Above, ``<ID>`` should be replaced by the WirePlumber daemon client ID.
Note that PipeWire daemon log levels must be specified by numbers, not
letter codes.
Changing log level via static configuration
-------------------------------------------
If you need to capture logs from WirePlumber at startup or in other circumstances
where changing the level at runtime or setting an environment variable is not
feasible, then you may also set the log level in the configuration file.
The log level changes via the ``log.level`` key in the ``context.properties``
section:
.. code::
context.properties = {
log.level = "D"
}
You may use the same syntax as in ``WIREPLUMBER_DEBUG`` to describe the exact
logging you want to achieve. For instance, to log debug messages from all
scripts and informational messages from everywhere else:
.. code::
context.properties = {
log.level = "I,s-*:D"
}
The easiest way to configure this is to drop a
:ref:`fragment file <config_conf_file_fragments>` that contains just this.
.. code-block:: bash
$ mkdir -p ~/.config/wireplumber/wireplumber.conf.d
$ echo 'context.properties = { log.level = "D" }' > ~/.config/wireplumber/wireplumber.conf.d/log.conf
See also :ref:`config_modifying_configuration`
Examples
--------

View file

@ -3,7 +3,9 @@ sphinx_files += files(
'installing.rst',
'running.rst',
'configuration.rst',
'locations.rst',
'logging.rst',
'multi_instance.rst',
)
subdir('configuration')

View file

@ -0,0 +1,88 @@
.. _daemon_multi_instance:
Running multiple instances
==========================
WirePlumber has the ability to run either as a single instance daemon or as
multiple instances, meaning that there can be multiple processes, each one
doing a different task.
The most common use case for such a setup is to separate the graph orchestration
tasks from the device monitoring and object creation ones. This can be useful
for robustness and security reasons, as it allows restarting the device monitors
or running them in different security contexts without affecting the rest of the
session management functionality.
To achieve a multi-instance setup, WirePlumber can be started multiple times
with a different :ref:`profile<config_components_and_profiles>` loaded in each
instance. This can be achieved using the ``--profile`` command line option to
select the profile to load:
.. code-block:: console
$ wireplumber --profile=custom
When no particular profile is specified, the ``main`` profile is loaded.
For multi-instance configuration, the default ``wireplumber.conf`` specifies 4
profiles:
.. describe:: policy
This profile runs all the policy scripts, i.e. ones that monitor changes
in the graph and execute actions to link nodes, select default devices,
create new nodes or configure existing ones differently.
.. describe:: audio
The audio profile runs the ALSA and ALSA MIDI monitors, which make audio &
MIDI devices available to PipeWire.
.. describe:: bluetooth
The bluetooth profile runs the BlueZ and BlueZ MIDI monitors, which enable
Bluetooth audio & MIDI devices and other Bluetooth functionality tied to the
A2DP, HSP, HFP and BAP profiles, using BlueZ.
.. describe:: video-capture
The video-capture profile runs the V4L2 and libcamera monitors, which make
video capture devices, such as cameras and HDMI capture cards, available
to PipeWire.
.. note::
The ``main`` profile includes all the functionality of the ``policy``,
``audio``, ``video-capture`` and ``bluetooth`` profiles combined (i.e. it is
the default for a standard single instance configuration). You should never
load the ``main`` profile alongside these other 4 profiles, as their
functionality will conflict.
.. warning::
Always ensure that the instances you load serve a different purpose and they
do not conflict with each other. Conflicting components executed in parallel
will have undefined behavior.
Systemd integration
-------------------
To make this easier to work with, a template systemd unit is provided, which is
meant to be started with the name of the profile as a template argument:
.. code-block:: console
$ systemctl --user disable wireplumber # disable the "main" profile instance
$ systemctl --user enable wireplumber@policy
$ systemctl --user enable wireplumber@audio
$ systemctl --user enable wireplumber@video-capture
$ systemctl --user enable wireplumber@bluetooth
.. note::
In WirePlumber 0.4, the template argument was the name of the configuration
file to load, since profiles did not exist. In WirePlumber 0.5, the template
argument is the name of the profile and the configuration file is always
``wireplumber.conf``. To change the name of the configuration file you need
to craft custom systemd unit files and use the ``--config-file`` command line
option as needed.

View file

@ -40,7 +40,7 @@ Synopsis:
$ meson -Dsession-managers="[ 'wireplumber' ]" build
$ ninja -C build
$ make run
$ make -C build run
Run independently or without installing
---------------------------------------

View file

@ -3,7 +3,7 @@
Events and Hooks
================
Session management is all about reacting to events and taking neccessary
Session management is all about reacting to events and taking necessary
actions. This is why WirePlumber's logic is all built on events and hooks.
Events
@ -56,7 +56,7 @@ There are two main types of hooks: ``SimpleEventHook`` and ``AsyncEventHook``.
* ``AsyncEventHook`` contains multiple functions, combined together in a state
machine using ``WpTransition`` underneath. The hook is completed only after
the state machine reaches its final state and this can take any amount of time
neccessary.
necessary.
Every hook also has a name, which can be an arbitrary string of characters.
Additionally, it has two arrays of names, which declare dependencies between

View file

@ -10,7 +10,9 @@ Table of Contents
daemon/installing.rst
daemon/running.rst
daemon/configuration.rst
daemon/locations.rst
daemon/logging.rst
daemon/multi_instance.rst
.. toctree::
:maxdepth: 2
@ -20,6 +22,14 @@ Table of Contents
design/understanding_wireplumber.rst
design/events_and_hooks.rst
.. toctree::
:maxdepth: 2
:caption: WirePlumber's Policies
policies/linking.rst
policies/smart_filters.rst
policies/software_dsp.rst
.. toctree::
:maxdepth: 2
:caption: The WirePlumber Library
@ -32,6 +42,13 @@ Table of Contents
scripting/lua_api.rst
scripting/existing_scripts.rst
scripting/custom_scripts.rst
.. toctree::
:maxdepth: 2
:caption: Tools
tools/wpctl.rst
.. toctree::
:maxdepth: 2

View file

@ -23,6 +23,7 @@ C API Documentation
c_api/link_api.rst
c_api/device_api.rst
c_api/client_api.rst
c_api/permission_manager_api.rst
c_api/metadata_api.rst
c_api/spa_device_api.rst
c_api/impl_node_api.rst
@ -39,3 +40,4 @@ C API Documentation
c_api/si_interfaces_api.rst
c_api/si_factory_api.rst
c_api/state_api.rst
c_api/base_dirs_api.rst

View file

@ -0,0 +1,6 @@
.. _base_dirs_api:
Base Directories File Lookup
============================
.. doxygengroup:: wpbasedirs
:content-only:

View file

@ -1,5 +1,6 @@
# you need to add here any files you add to the api directory as well
sphinx_files += files(
'base_dirs_api.rst',
'client_api.rst',
'component_loader_api.rst',
'conf_api.rst',
@ -17,6 +18,7 @@ sphinx_files += files(
'obj_manager_api.rst',
'object_api.rst',
'pipewire_object_api.rst',
'permission_manager_api.rst',
'plugin_api.rst',
'port_api.rst',
'properties_api.rst',

View file

@ -7,6 +7,7 @@ PipeWire Metadata
digraph inheritance {
rankdir=LR;
GBoxed -> WpMetadataItem
GObject -> WpObject;
WpObject -> WpProxy;
WpProxy -> WpGlobalProxy;
@ -14,6 +15,8 @@ PipeWire Metadata
WpMetadata-> WpImplMetadata;
}
.. doxygenstruct:: WpMetadataItem
.. doxygenstruct:: WpMetadata
.. doxygenstruct:: WpImplMetadata

View file

@ -0,0 +1,17 @@
.. _permission_manager_api:
WpPermissionManager
===================
.. graphviz::
:align: center
digraph inheritance {
rankdir=LR;
GObject -> WpObject;
WpObject -> WpPermissionManager;
}
.. doxygenstruct:: WpPermissionManager
.. doxygengroup:: wppermissionmanager
:content-only:

View file

@ -7,10 +7,16 @@ Settings
digraph inheritance {
rankdir=LR;
GBoxed -> WpSettingsSpec;
GBoxed -> WpSettingsItem;
GObject -> WpObject;
WpObject -> WpSettings;
}
.. doxygenstruct:: WpSettingsSpec
.. doxygenstruct:: WpSettingsItem
.. doxygenstruct:: WpSettings
.. doxygengroup:: wpsettings

View file

@ -5,6 +5,8 @@ sphinx_files += files(
subdir('daemon')
subdir('design')
subdir('policies')
subdir('library')
subdir('scripting')
subdir('tools')
subdir('resources')

View file

@ -0,0 +1,140 @@
.. _policies_linking:
Linking Policy
==============
Introduction
------------
The linking policy in WirePlumber is the logic charged to link a PipeWire stream
node with a PipeWire device node (most cases), or with another PipeWire stream
node (monitoring applications).
PipeWire stream nodes always have one of the following media classes:
- Stream/Output/Audio: For audio playback applications (Eg pw-play).
- Stream/Input/Audio: For audio capture applications (Eg pw-record).
- Stream/Input/Video: For video capture applications (Eg cheese).
And Pipewire device nodes always have one of the following media classes:
- Audio/Sink: For audio playback devices (Eg Speakers).
- Audio/Source: For audio capture devices (Eg Microphones).
- Video/Source: For video capture devices (Eg Cameras).
By default, since in most cases we want to link a stream node with a device
node, the linking policy logic when linking 2 nodes always follows the following
assignments:
.. graphviz::
digraph nodes {
rankdir=LR;
APS [shape=box label=<audio playback stream<BR/>(Stream/Output/Audio)>];
APD [shape=box label=<audio playback device<BR/>(Audio/Sink)>];
ACS [shape=box label=<audio capture stream<BR/>(Stream/Input/Audio)>];
ACD [shape=box label=<audio capture device<BR/>(Audio/Source)>];
VCS [shape=box label=<video capture stream<BR/>(Stream/Input/Video)>];
VCD [shape=box label=<video capture device<BR/>(Video/Source)>];
APS -> APD;
ACD -> ACS;
VCD -> VCS;
}
After that, once the media class of a device node has been selected for a
particular stream node, and there are more than 1 device node matching such
media class, WirePlumber will select one based on a set of priorities:
First, it will check if there is a default configured device node for the
selected device media class. If there is one, and the node exists, it will link
the stream node with such configured default node. Users can easily configure
default device nodes for all the 3 different device media classes using tools
such as ``pavucontrol`` or ``wpctl``. The logic is implemented in the
``linking/find-default-target.lua`` Lua script.
If there isn't any default node configured, or there is a default node
configured but the node does not exist, WirePlumber will instead select the
best device node available. The best device node is the node with highest
session priority and available routes to the physical device. The logic is
implemented in the ``linking/find-best-target.lua`` Lua script.
If the best node could not be found because the system does not have any,
WirePlumber won't link the stream and will send a "no target node available"
error to the client.
Stream node linking properties
------------------------------
The above default linking logic behavior can be changed by setting specific
properties on the nodes.
.. note::
These properties must be set in the **stream** nodes (not the device nodes),
otherwise they won't have any effect.
- **target.object**:
  The name of the desired node for this stream to be linked with.
  If this property is present, WirePlumber will try to find such node, see if it
  can be linked with the stream, and if so, will use it instead of the default
  node or best node. The logic is implemented in the ``linking/find-defined-target.lua``
  Lua script. Since this property is not set by default, WirePlumber will always
  link stream nodes to the default or best device node found. This property can be
  easily set using tools such as ``pw-play`` with the ``--target`` flag.
  Note that any node name can be specified there, even if the name is not a device
  node name, but another stream node name. If this is the case, WirePlumber will
  link 2 stream nodes together. An example of this case is the monitoring nodes
  created by ``pavucontrol`` to monitor audio of all audio devices and streams.
- **node.dont-reconnect**:
  Boolean indicating whether the stream node should not be reconnected to a new
  node if its current linked node (target) was destroyed or not. By default it
  is set to ``false``, so if the property is not present in the stream node, WirePlumber
  will always try to reconnect the stream node to a new target instead of sending
  an error to the client. The logic is implemented in the ``linking/prepare-link.lua``
  Lua script.
- **node.dont-move**:
  Boolean indicating whether the stream node should not be movable or not at runtime
  using the metadata. If a stream node is not movable, it means that users cannot
  relink the stream node to a new target at runtime (using tools such as ``pavucontrol``
  or ``pw-metadata``) when the stream node is already linked to a different node. By
  default it is set to ``false``, so if the property is not present, WirePlumber will
  always move, and therefore link the stream node to a new target if it is defined and
  updated in the ``target.object`` metadata key.
- **node.dont-fallback**:
  Boolean indicating whether the stream node should not fallback to a different
  target if its defined target does not exist (the one defined with the ``target.object``
  property) or not. Therefore, if this property is set to ``true``, WirePlumber sends
  a "defined target not found" error to the client and will also destroy the stream
  node. By default it is set to ``false``, so if the property is not present in the
  stream node, WirePlumber will always fallback to the default or best target if
  the defined target was not found.
- **node.linger**:
  Boolean indicating whether the stream node should linger or not if its defined
  target was not found and the ``node.dont-fallback`` is set to true. Therefore, if
  this property is set to ``true``, the defined target was not found, and the
  ``node.dont-fallback`` is set to true, WirePlumber won't send a "defined target not found"
  error to the client, and won't destroy the stream node. This is useful if we want
  the stream to wait (without processing any data) until its defined target becomes
  available. By default it is set to ``false``, so if the property is not present in the
  stream node, WirePlumber will always destroy the node and send an error to the client
  if its target was not found and ``node.dont-fallback`` was set to true.
Linking settings
----------------
Apart from the above properties, there are also global settings for the linking
policy. See :ref:`config_settings` for more information, the linking settings
are prefixed with ``linking.``.

View file

@ -0,0 +1,6 @@
# you need to add here any files you add to the toc directory as well
sphinx_files += files(
'linking.rst',
'smart_filters.rst',
'software_dsp.rst',
)

View file

@ -0,0 +1,380 @@
.. _policies_smart_filters:
Smart Filters
=============
Introduction
------------
The smart filters policy allows automatically linking filters together, in a
chain, and tied to a specific target node. This is useful when we want to apply
a specific processing chain to a specific device, for example. When a stream is
about to be linked to a target node that is associated with a smart filter
chain, the policy will automatically link the stream with the first filter in
the chain, and the last filter in the chain with the target node. This is done
transparently to the client, allowing users to define a specific processing
chain for a specific device without having to create setups with virtual sinks
(or sources) that must be explicitly targeted by the clients.
Filters, in general, are nodes that are placed in the middle of the graph and
are used to modify the data that passes through them. For example, the
*echo-cancel*, the *filter-chain*, or the *loopback* nodes are filters.
Filters can be implemented either as a single node or as a pair of nodes with
opposite directions. For example, the *null-audio-sink* node can be configured
to be a single-node filter. On the other hand, the *filter-chain* is a pair of
nodes with opposite directions, where one node captures the audio from the graph
and the other node sends the modified audio back to the graph.
For the purpose of the **smart filters** policy, WirePlumber will only consider
pairs of nodes as filters, not single-node ones. More specifically, a pair of
nodes will be considered to be a filter by WirePlumber if they have the
``node.link-group`` property set to a common value. This property is always set
on pairs of nodes that are internally linked together and is a good indicator
that the nodes are implementing a filter.
That pair of nodes **must** always consist of a *stream* node and a *main* node.
The main node acts as a virtual device, where the data is sent or captured
to/from, and the stream node acts as a regular stream, where the data is sent
or received to/from the next node in the graph. This is designated by their
media class, as shown in the table below:
.. list-table::
:widths: 30 35 35
:header-rows: 1
:stub-columns: 1
* -
- Input filter (virtual sink)
- Output filter (virtual source)
* - Main node
- ``Audio/Sink`` (capture)
- ``Audio/Source`` (playback)
* - Stream node
- ``Stream/Output/Audio`` (playback)
- ``Stream/Input/Audio`` (capture)
For instance, if a smart filter is used between an application playback stream
and the default audio sink, the graph would look like this:
.. graphviz::
digraph nodes {
rankdir=LR;
A [shape=box label=<application stream node<BR/>(Stream/Output/Audio)>];
FM [shape=box label=<filter main node<BR/>(Audio/Sink)>];
FS [shape=box label=<filter stream node<BR/>(Stream/Output/Audio)>];
D [shape=box label=<default device node<BR/>(Audio/Sink)>];
A -> FM;
FS -> D;
subgraph cluster_filter {
style="dotted";
FM; FS;
}
}
The same logic is applied if the smart filter is used between an application
capture stream and the default audio source, it is just all in the opposite
direction. This is how the graph would look like in this case:
.. graphviz::
digraph nodes {
rankdir=LR;
A [shape=box label=<application stream node<BR/>(Stream/Input/Audio)>];
FM [shape=box label=<filter main node<BR/>(Audio/Source)>];
FS [shape=box label=<filter stream node<BR/>(Stream/Input/Audio)>];
D [shape=box label=<default device node<BR/>(Audio/Source)>];
D -> FS;
FM -> A;
subgraph cluster_filter {
style="dotted";
FM; FS;
}
}
When multiple filters have the same direction, they can also be chained together
so that the output of one filter is sent to the input of the next filter. The
next section describes how these chains can be described with properties so that
they are automatically linked by WirePlumber in any way we want.
Filter properties
-----------------
When a filter node is created, WirePlumber will check for the presence of the
following optional node properties on the **main** node:
- **filter.smart**
Boolean indicating whether smart policy will be used for these filter nodes or
not. This is disabled by default, therefore filter nodes will be treated as
regular nodes, without applying any kind of extra logic. On the other hand, if
this property is set to ``true``, automatic (smart) filter policy will be used
when linking them. The properties below will then also apply, providing
further instructions.
- **filter.smart.name**
The unique name of the filter. WirePlumber will use the value of the
``node.link-group`` property as the filter name if this property is not set.
- **filter.smart.disabled**
Boolean indicating whether the filter should be disabled or not. A disabled
filter will never be used under any circumstances. If the property is not set,
WirePlumber will consider the filter as enabled (i.e. disabled = false).
- **filter.smart.targetable**
Boolean indicating whether the filter can be directly linked with clients that
have it defined as a target (Eg: ``pw-play --target <filter-name>``) or not.
This can be useful when a client wants to be linked with a filter that is in
the middle of the chain in order to bypass the filters that are placed before
the selected one. If the property is not set, WirePlumber will consider the
filter not targetable by default, meaning filters will never by bypassed by
clients, and clients will always be linked with the first filter in the chain.
- **filter.smart.target**
A JSON object that defines the matching properties of the filter's target
node. A filter target can never be another filter node (WirePlumber will
ignore it), it must be a device or virtual sink (or source, depending on the
direction of the filter). If this property is not set, WirePlumber will use
the default sink/source as the target.
- **filter.smart.before**
A JSON array containing the names of the filters that are supposed to be
chained after this filter (i.e. this filter here should be chained *before*
those). If not set, WirePlumber will link the filters by order of creation.
- **filter.smart.after**
A JSON array containing the names of the filters that are supposed to be
chained before this filter (i.e. this filter here should be chained *after*
those). If not set, WirePlumber will link the filters by order of creation.
.. note::
These properties must be set on the filter's **main** node, not the stream
node.
As an example, we will describe here how to create 2 loopback filters in
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
linked with the default audio device, and use loopback-2 filter as the last
filter in the chain.
The PipeWire configuration files for the 2 filters should be like this:
- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:
.. code-block::
:emphasize-lines: 8-11
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-1-sink
node.description = "Loopback 1 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart = true
filter.smart.name = loopback-1
filter.smart.before = [ loopback-2 ]
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
stream.dont-remix = true
}
}
}
]
- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:
.. code-block::
:emphasize-lines: 8-10
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-2-sink
node.description = "Loopback 2 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart = true
filter.smart.name = loopback-2
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
stream.dont-remix = true
}
}
}
]
After restarting PipeWire to apply the configuration changes, playing a test
wave audio file with paplay to the default device should result in the following
graph:
.. graphviz::
digraph nodes {
rankdir=LR;
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
device [shape=box label=<default device node<BR/>(Audio/Sink)>];
paplay -> L1M;
L1S -> L2M;
L2S -> device;
subgraph cluster_filter1 {
style="dotted";
L1M; L1S;
}
subgraph cluster_filter2 {
style="dotted";
L2M; L2S;
}
}
Now, if we remove the ``filter.smart.before = [ loopback-2 ]`` property from the
loopback-1 filter, and add a ``filter.smart.before = [ loopback-1 ]`` property
in the loopback-2 filter configuration file, WirePlumber should link the
loopback-1 filter as the last filter in the chain, like this:
.. graphviz::
digraph nodes {
rankdir=LR;
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
device [shape=box label=<default device node<BR/>(Audio/Sink)>];
paplay -> L2M;
L2S -> L1M;
L1S -> device;
subgraph cluster_filter1 {
style="dotted";
L1M; L1S;
}
subgraph cluster_filter2 {
style="dotted";
L2M; L2S;
}
}
In addition, the filters can have different targets. For example, we can define
the filters like this:
- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:
.. code-block::
:emphasize-lines: 12
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-1-sink
node.description = "Loopback 1 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart = true
filter.smart.name = loopback-1
filter.smart.after = [ loopback-2 ]
filter.smart.target = { node.name = "not-default-audio-device" }
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
stream.dont-remix = true
}
}
}
]
- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:
.. code-block::
context.modules = [
{ name = libpipewire-module-loopback
args = {
node.name = loopback-2-sink
node.description = "Loopback 2 Sink"
capture.props = {
audio.position = [ FL FR ]
media.class = Audio/Sink
filter.smart = true
filter.smart.name = loopback-2
}
playback.props = {
audio.position = [ FL FR ]
node.passive = true
stream.dont-remix = true
}
}
}
]
In this case, playing a test wave audio file with paplay to the
``not-default-audio-device`` device should result in the following graph:
.. graphviz::
digraph nodes {
rankdir=LR;
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
device [shape=box label=<not-default-audio-device node<BR/>(Audio/Sink)>];
paplay -> L2M;
L2S -> L1M;
L1S -> device;
subgraph cluster_filter1 {
style="dotted";
L1M; L1S;
}
subgraph cluster_filter2 {
style="dotted";
L2M; L2S;
}
}
In this configuration, the loopback-1 filter will only be linked if the
application stream is targeting the device node called
"not-default-audio-device".
Filters metadata
----------------
Similar to the default metadata, it is also possible to override the filter
properties using the "filters" metadata object. This allow users to change the
filters policy at runtime.
For example, assuming the id of the *loopback-1* main node is ``40``, we can
disable the filter by setting its ``filter.smart.disabled`` metadata key to
``true`` using the ``pw-metadata`` tool like this:
.. code-block:: bash
$ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON
We can also change the target of a filter at runtime:
.. code-block:: bash
$ pw-metadata -n filters 40 "filter.smart.target" "{ node.name = new-target-node-name }" Spa:String:JSON
Every time a key in the filters metadata changes, all filters are unlinked and
re-linked properly, following the new policy.

View file

@ -0,0 +1,106 @@
.. _policies_software_dsp:
Automatic Software DSP
======================
Introduction
------------
WirePlumber provides a mechanism for transparently handling oddball and embedded
devices that require software DSP to be done in userspace. Devices such as smartphones,
TVs, portable speakers, and even some laptops implement an audio subsystem designed
under the assumption that the hardware sink/source will be "backed" by some sort
of transparent DSP mechanism. That is, the hardware device itself should not be
directly accessed, and expects to be sent preprocessed/pre-routed samples. Often,
especially with Android handsets, these samples are preprocessed or pre-routed
by the vendor's proprietary userspace.
WirePlumber's automatic software DSP mechanism aims to replicate this functionality in
a standardised and configurable way. The target device sink/source is hidden from
other PipeWire clients, and a virtual node is linked to it. This virtual
node is then presented to clients as *the* node, allowing implementers to specify
any custom processing or routing in a way that is transparent to users, the kernel,
and the hardware.
Activating
----------
In addition to the ``node.software-dsp.rules`` section, the ``node.software-dsp``
:ref:`feature <config_features>` must be enabled in the desired profile(s).
Matching a node
---------------
Matching rules are specified in ``node.software-dsp.rules``. The ``create-filter``
action specifies behaviour at node insertion. All node properties can be matched
on, including any type-specific properties such as ``alsa.id``.
Configurable properties
-----------------------
.. describe:: filter-graph
SPA-JSON object describing the software DSP node. This is passed as-is as
an argument to ``libpipewire-module-filter-chain``. See the
`filter-chain documentation <https://docs.pipewire.org/page_module_filter_chain.html>`_
for details on what options can be set in this object.
.. note::
The ``target.object`` property of the virtual node should be configured
statically to point to the node matched by the rule.
.. describe:: filter-path
Absolute path to a file on disk storing a SPA-JSON object as plain text. This will be
parsed by WirePlumber into a WpConf object with a single section called
``node.software-dsp.graph``, then passed as-is into ``libpipewire-module-filter-chain``.
.. note::
``filter-graph`` and ``filter-path`` are mutually exclusive, with the former taking
precedence if both are present in the matched rule.
.. describe:: hide-parent
Boolean indicating whether or not the matched node should be hidden from
clients. ``node/software-dsp.lua`` will set the permissions for all clients other
than WirePlumber itself to ``'-'``. This prevents use of the node by any
userspace software except for WirePlumber itself.
Examples
--------
.. code-block::
:caption: wireplumber.conf.d/99-my-dsp.conf
node.software-dsp.rules = [
{
matches = [
{ "node.name" = "alsa_output.platform-sound.HiFi__Speaker__sink" }
{ "alsa.id" = "~WeirdHardware*" } # Wildcard match
]
actions = {
create-filter = {
filter-graph = {} # Virtual node goes here
filter-path = "/path/to/spa.json"
hide-parent = true
}
}
}
]
wireplumber.profiles = {
main = {
node.software-dsp = required
}
}
This will match any sinks with the UCM HiFi Speaker profile set or cards
containing the string "WeirdHardware" at the start of their name.

View file

@ -0,0 +1,41 @@
.. _scripting_custom_scripts:
Custom Scripts
==============
The locations where WirePlumber searches for scripts is explained in
:ref:`config_locations_scripts`.
Scripts are not loaded automatically; a component muse be defined for them, and
this component must be included in a profile. See
:ref:`config_components_and_profiles`.
Full example
------------
Let's assume that ``~/.local/share/wireplumber/scripts/90-hello-world.lua``
contains the following script:
.. code-block:: lua
log = Log.open_topic("hello-world")
log.info("Hello world")
In order for it to run, we'll define a component and include it in the default
profile by including the following configuration (for example, in
``~/.config/wireplumber/wireplumber.conf.d/90-hello-world.conf``):
.. code-block::
wireplumber.components = [
{
name = "90-hello-world.lua", type = script/lua
provides = hello-world
}
]
wireplumber.profiles = {
main = {
hello-world = required
}
}

View file

@ -3,6 +3,33 @@
Debug Logging
=============
Constructors
~~~~~~~~~~~~
.. function:: Log.open_topic(topic)
Opens a LogTopic with the given topic name. Well known script topics are
described in :ref:`daemon_logging`, and messages from scripts shall use
**s-***.
Example:
.. code-block:: lua
local obj
log = Log.open_topic ("s-linking")
log:info (obj, "an info message on obj")
log:debug ("a debug message")
Above example shows how to output debug logs.
:param string topic: The log topic to open
:returns: the log topic object
:rtype: Log (:c:struct:`WpLogTopic`)
Methods
~~~~~~~
.. function:: Log.warning(object, message)
Logs a warning message, like :c:macro:`wp_warning_object`

View file

@ -2,6 +2,7 @@
sphinx_files += files(
'lua_api.rst',
'existing_scripts.rst',
'custom_scripts.rst',
)
subdir('lua_api')

View file

@ -0,0 +1,3 @@
sphinx_files += files(
'wpctl.rst',
)

309
docs/rst/tools/wpctl.rst Normal file
View file

@ -0,0 +1,309 @@
wpctl(1)
========
SYNOPSIS
--------
**wpctl** [*COMMAND*] [*COMMAND_OPTIONS*]
DESCRIPTION
-----------
**wpctl** is a command-line control tool for WirePlumber, the PipeWire session
manager. It provides an interface to inspect, control, and configure audio and
video devices, nodes, and their properties within a PipeWire media server.
WirePlumber manages audio and video routing, device configuration, and session
policies. **wpctl** allows users to interact with these components, change
volume levels, set default devices, inspect object properties, and modify
settings.
COMMANDS
--------
status
^^^^^^
**wpctl status** [**-k**\|\ **--nick**] [**-n**\|\ **--name**]
Displays the current state of objects in PipeWire, including devices, sinks,
sources, filters, and streams. Shows a hierarchical view of the audio/video
system.
Options:
**-k**, **--nick**
Display device and node nicknames instead of descriptions
**-n**, **--name**
Display device and node names instead of descriptions
get-volume
^^^^^^^^^^
**wpctl get-volume** *ID*
Displays volume information about the specified node, including current volume
level and mute state.
Arguments:
*ID*
Node ID or special identifier (see `SPECIAL IDENTIFIERS`_)
inspect
^^^^^^^
**wpctl inspect** *ID* [**-r**\|\ **--referenced**] [**-a**\|\ **--associated**]
Displays detailed information about the specified object, including all
properties and metadata.
Arguments:
*ID*
Object ID or special identifier
Options:
**-r**, **--referenced**
Show objects that are referenced in properties
**-a**, **--associated**
Show associated objects
set-default
^^^^^^^^^^^
**wpctl set-default** *ID*
Sets the specified device node to be the default target of its kind (capture or
playback) for new streams that require auto-connection.
Arguments:
*ID*
Sink or source node ID
set-volume
^^^^^^^^^^
**wpctl set-volume** *ID* *VOL*\ [**%**]\ [**-**\|\ **+**] [**-p**\|\ **--pid**] [**-l** *LIMIT*\|\ **--limit** *LIMIT*]
Sets the volume of the specified node.
Arguments:
*ID*
Node ID, special identifier, or PID (with --pid)
*VOL*\ [**%**]\ [**-**\|\ **+**]
Volume specification:
- *VOL* - Set volume to specific value (1.0 = 100%)
- *VOL*\ **%** - Set volume to percentage (50% = 0.5)
- *VOL*\ **+** - Increase volume by value
- *VOL*\ **-** - Decrease volume by value
- *VOL*\ **%+** - Increase volume by percentage
- *VOL*\ **%-** - Decrease volume by percentage
Options:
**-p**, **--pid**
Treat ID as a process ID and affect all nodes associated with it
**-l** *LIMIT*, **--limit** *LIMIT*
Limit final volume to below this value (floating point, 1.0 = 100%)
Examples:
Set volume to 50%: ``wpctl set-volume @DEFAULT_SINK@ 0.5``
Increase volume by 10%: ``wpctl set-volume 42 10%+``
Set volume for all nodes of PID 1234: ``wpctl set-volume --pid 1234 0.8``
set-mute
^^^^^^^^
**wpctl set-mute** *ID* **1**\|\ **0**\|\ **toggle** [**-p**\|\ **--pid**]
Changes the mute state of the specified node.
Arguments:
*ID*
Node ID, special identifier, or PID (with --pid)
**1**\|\ **0**\|\ **toggle**
Mute state: 1 (mute), 0 (unmute), or toggle current state
Options:
**-p**, **--pid**
Treat ID as a process ID and affect all nodes associated with it
set-profile
^^^^^^^^^^^
**wpctl set-profile** *ID* *INDEX*
Sets the profile of the specified device to the given index.
Arguments:
*ID*
Device ID or special identifier
*INDEX*
Profile index (integer, 0 typically means 'off')
set-route
^^^^^^^^^
**wpctl set-route** *ID* *INDEX*
Sets the route of the specified device to the given index.
Arguments:
*ID*
Device node ID or special identifier
*INDEX*
Route index (integer, 0 typically means 'off')
clear-default
^^^^^^^^^^^^^
**wpctl clear-default** [*ID*]
Clears the default configured node. If no ID is specified, clears all default
nodes.
Arguments:
*ID* (optional)
Settings ID to clear (0-2 for Audio/Sink, Audio/Source, Video/Source).
If omitted, clears all defaults.
settings
^^^^^^^^
**wpctl settings** [*KEY*] [*VAL*] [**-d**\|\ **--delete**] [**-s**\|\ **--save**] [**-r**\|\ **--reset**]
Shows, changes, or removes WirePlumber settings.
Arguments:
*KEY* (optional)
Setting key name
*VAL* (optional)
Setting value (JSON format)
Options:
**-d**, **--delete**
Delete the saved setting value (no KEY means delete all)
**-s**, **--save**
Save the setting value (no KEY means save all, no VAL means current value)
**-r**, **--reset**
Reset the setting to its default value
Behavior:
- No arguments: Show all settings
- KEY only: Show specific setting value
- KEY and VAL: Set specific setting value
set-log-level
^^^^^^^^^^^^^
**wpctl set-log-level** [*ID*] *LEVEL*
Sets the log level of a client.
Arguments:
*ID* (optional)
Client ID. If omitted, applies to WirePlumber. Use 0 for PipeWire server.
*LEVEL*
Log level (e.g., ``0``, ``1``, ``2``, ``3``, ``4``, ``5``, ``E``, ``W``, ``N``, ``I``, ``D``, ``T``).
Use ``-`` to unset the log level.
SPECIAL IDENTIFIERS
-------------------
Instead of numeric IDs, **wpctl** accepts these special identifiers for
commonly used defaults:
**@DEFAULT_SINK@**, **@DEFAULT_AUDIO_SINK@**
The current default audio sink (playback device)
**@DEFAULT_SOURCE@**, **@DEFAULT_AUDIO_SOURCE@**
The current default audio source (capture device)
**@DEFAULT_VIDEO_SOURCE@**
The current default video source (camera)
These identifiers are resolved at runtime to the appropriate node IDs.
EXIT STATUS
-----------
**wpctl** returns the following exit codes:
0
Success
1
General error (e.g., invalid arguments, connection failure)
2
Could not connect to PipeWire
3
Command-specific error (e.g., object not found)
EXAMPLES
--------
Display system status::
wpctl status
Set default audio sink::
wpctl set-default 42
Set volume to 75% on default sink::
wpctl set-volume @DEFAULT_SINK@ 75%
Increase volume by 5% on a specific node::
wpctl set-volume 42 5%+
Mute the default source::
wpctl set-mute @DEFAULT_SOURCE@ 1
Toggle mute on default sink::
wpctl set-mute @DEFAULT_SINK@ toggle
Inspect a device with associated objects::
wpctl inspect --associated 30
Show all WirePlumber settings::
wpctl settings
Set a specific setting::
wpctl settings bluetooth.autoswitch true
Save all current settings::
wpctl settings --save
Set log level for WirePlumber to debug::
wpctl set-log-level D
Set log level for a specific client::
wpctl set-log-level 42 W
NOTES
-----
Object IDs can be found using the **status** command. The hierarchical display
shows IDs for devices, nodes, and other objects.
Volume values are floating-point numbers where 1.0 represents 100% volume.
Values can exceed 1.0 to introduce volume amplification.
When using the **--pid** option, **wpctl** will find all audio nodes associated
with the specified process ID and apply the operation to all of them.
SEE ALSO
--------
**pipewire**\ (1), **pw-cli**\ (1), **pw-dump**\ (1), **wireplumber**\ (1)
WirePlumber Documentation: https://pipewire.pages.freedesktop.org/wireplumber/

368
lib/wp/base-dirs.c Normal file
View file

@ -0,0 +1,368 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include "base-dirs.h"
#include "log.h"
#include "wpversion.h"
#include "wpbuildbasedirs.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-base-dirs")
/*!
* \defgroup wpbasedirs Base Directories File Lookup
*/
/* Returns /basedir/subdir/filename, with filename treated as a module
* if WP_BASE_DIRS_FLAG_MODULE is set.
* The basedir is assumed to be either an absolute path or NULL.
* The subdir is assumed to be a path relative to basedir or NULL.
*/
static gchar *
make_path (guint flags, const gchar *basedir, const gchar *subdir,
const gchar *filename)
{
g_autofree gchar *full_basedir = NULL;
g_autofree gchar *full_filename = NULL;
/* merge subdir into basedir, if necessary */
if (subdir) {
full_basedir = g_canonicalize_filename (subdir, basedir);
basedir = full_basedir;
}
if (flags & WP_BASE_DIRS_FLAG_MODULE) {
g_autofree gchar *basename = g_path_get_basename (filename);
g_autofree gchar *dirname = g_path_get_dirname (filename);
const gchar *prefix = "";
const gchar *suffix = "";
if (!g_str_has_prefix (basename, "lib"))
prefix = "lib";
if (!g_str_has_suffix (basename, ".so"))
suffix = ".so";
full_filename = g_strconcat (dirname, G_DIR_SEPARATOR_S,
prefix, basename, suffix, NULL);
filename = full_filename;
}
return g_canonicalize_filename (filename, basedir);
}
static GPtrArray *
lookup_dirs (guint flags, gboolean is_absolute)
{
g_autoptr(GPtrArray) dirs = g_ptr_array_new_with_free_func (g_free);
const gchar *dir;
const gchar *subdir =
(flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ? "wireplumber" : ".";
/* Compile the list of lookup directories in priority order */
if (is_absolute) {
g_ptr_array_add (dirs, NULL);
}
else if ((flags & WP_BASE_DIRS_ENV_CONFIG) &&
(dir = g_getenv ("WIREPLUMBER_CONFIG_DIR"))) {
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
for (guint i = 0; env_dirs[i]; i++) {
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
}
}
else if ((flags & WP_BASE_DIRS_ENV_DATA) &&
(dir = g_getenv ("WIREPLUMBER_DATA_DIR"))) {
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
for (guint i = 0; env_dirs[i]; i++) {
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
}
}
else if ((flags & WP_BASE_DIRS_ENV_MODULE) &&
(dir = g_getenv ("WIREPLUMBER_MODULE_DIR"))) {
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
for (guint i = 0; env_dirs[i]; i++) {
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
}
}
else {
if (flags & WP_BASE_DIRS_XDG_CONFIG_HOME) {
dir = g_get_user_config_dir ();
if (G_LIKELY (g_path_is_absolute (dir)))
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
}
if (flags & WP_BASE_DIRS_XDG_DATA_HOME) {
dir = g_get_user_data_dir ();
if (G_LIKELY (g_path_is_absolute (dir)))
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
}
if (flags & WP_BASE_DIRS_XDG_CONFIG_DIRS) {
const gchar * const *xdg_dirs = g_get_system_config_dirs ();
for (guint i = 0; xdg_dirs[i]; i++) {
if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
}
}
if (flags & WP_BASE_DIRS_BUILD_SYSCONFDIR) {
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_SYSCONFDIR));
}
if (flags & WP_BASE_DIRS_XDG_DATA_DIRS) {
const gchar * const *xdg_dirs = g_get_system_data_dirs ();
for (guint i = 0; xdg_dirs[i]; i++) {
if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
}
}
if (flags & WP_BASE_DIRS_BUILD_DATADIR) {
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_DATADIR));
}
if (flags & WP_BASE_DIRS_BUILD_LIBDIR) {
subdir = (flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ?
"wireplumber-" WIREPLUMBER_API_VERSION : ".";
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_LIBDIR));
}
}
return g_steal_pointer (&dirs);
}
/*!
* \brief Searches for \a filename in the hierarchy of directories specified
* by the \a flags parameter
*
* Returns the highest priority file found in the hierarchy of directories
* specified by the \a flags parameter. The \a subdir parameter is the name
* of the subdirectory to search in, inside the specified directories. If
* \a subdir is NULL, the base path of each directory is used.
*
* The \a filename parameter is the name of the file to search for. If the
* file is found, its full path is returned. If the file is not found, NULL
* is returned. The file is considered found if it is a regular file.
*
* If the \a filename is an absolute path, it is tested for existence and
* returned as is, ignoring the lookup directories in \a flags as well as
* the \a subdir parameter.
*
* \ingroup wpbasedirs
* \param flags flags to specify the directories to look into and other
* options specific to the kind of file being looked up
* \param subdir (nullable): the name of the subdirectory to search in,
* inside the specified directories
* \param filename the name of the file to search for
* \returns (transfer full) (nullable): A newly allocated string with the
* absolute, canonicalized file path, or NULL if the file was not found.
* \since 0.5.0
*/
gchar *
wp_base_dirs_find_file (WpBaseDirsFlags flags, const gchar * subdir,
const gchar * filename)
{
gboolean is_absolute = g_path_is_absolute (filename);
g_autoptr (GPtrArray) dir_paths = lookup_dirs (flags, is_absolute);
gchar *ret = NULL;
/* ignore the subdir if filename is absolute */
if (is_absolute)
subdir = NULL;
for (guint i = 0; i < dir_paths->len; i++) {
g_autofree gchar *path = make_path (flags, g_ptr_array_index (dir_paths, i),
subdir, filename);
wp_trace ("test file: %s", path);
if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) {
ret = g_steal_pointer (&path);
break;
}
}
wp_debug ("lookup '%s', return: %s", filename, ret);
return ret;
}
struct conffile_iterator_item
{
gchar *filename;
gchar *path;
};
static void
conffile_iterator_item_clear (struct conffile_iterator_item *item)
{
g_free (item->filename);
g_free (item->path);
}
struct conffile_iterator_data
{
GArray *items;
guint idx;
};
static void
conffile_iterator_reset (WpIterator *it)
{
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
it_data->idx = 0;
}
static gboolean
conffile_iterator_next (WpIterator *it, GValue *item)
{
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
if (it_data->idx < it_data->items->len) {
const gchar *path = g_array_index (it_data->items,
struct conffile_iterator_item, it_data->idx).path;
it_data->idx++;
g_value_init (item, G_TYPE_STRING);
g_value_set_string (item, path);
return TRUE;
}
return FALSE;
}
static gboolean
conffile_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
gpointer data)
{
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
for (guint i = 0; i < it_data->items->len; i++) {
g_auto (GValue) item = G_VALUE_INIT;
const gchar *path = g_array_index (it_data->items,
struct conffile_iterator_item, i).path;
g_value_init (&item, G_TYPE_STRING);
g_value_set_string (&item, path);
if (!func (&item, ret, data))
return FALSE;
}
return TRUE;
}
static void
conffile_iterator_finalize (WpIterator *it)
{
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
g_clear_pointer (&it_data->items, g_array_unref);
}
static const WpIteratorMethods conffile_iterator_methods = {
.version = WP_ITERATOR_METHODS_VERSION,
.reset = conffile_iterator_reset,
.next = conffile_iterator_next,
.fold = conffile_iterator_fold,
.finalize = conffile_iterator_finalize,
};
static gint
conffile_iterator_item_compare (const struct conffile_iterator_item *a,
const struct conffile_iterator_item *b)
{
return g_strcmp0 (a->filename, b->filename);
}
/*!
* \brief Creates an iterator to iterate over all files that match \a suffix
* within the \a subdir of the directories specified in \a flags
*
* The \a subdir parameter is the name of the subdirectory to search in,
* inside the directories specified by \a flags. If \a subdir is NULL,
* the base path of each directory is used. If \a subdir is an absolute path,
* files are only looked up in that directory and the directories in \a flags
* are ignored.
*
* The \a suffix parameter is the filename suffix to match. If \a suffix is
* NULL, all files are matched.
*
* The iterator will iterate over the absolute paths of all the files
* files found, in the order of priority of the directories, starting from
* the lowest priority directory (e.g. /usr/share/wireplumber) and ending
* with the highest priority directory (e.g. $XDG_CONFIG_HOME/wireplumber).
* Files within each directory are also sorted by filename.
*
* \ingroup wpbasedirs
* \param flags flags to specify the directories to look into and other
* options specific to the kind of file being looked up
* \param subdir (nullable): the name of the subdirectory to search in,
* inside the configuration directories
* \param suffix (nullable): The filename suffix, NULL matches all entries
* \returns (transfer full): a new iterator iterating over strings which are
* absolute & canonicalized paths to the files found
* \since 0.5.0
*/
WpIterator *
wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
const gchar * subdir, const gchar * suffix)
{
g_autoptr (GArray) items =
g_array_new (FALSE, FALSE, sizeof (struct conffile_iterator_item));
g_autoptr (GPtrArray) dir_paths = NULL;
g_array_set_clear_func (items, (GDestroyNotify) conffile_iterator_item_clear);
if (subdir == NULL)
subdir = ".";
/* Note: this list is highest-priority first */
dir_paths = lookup_dirs (flags, g_path_is_absolute (subdir));
/* Run backwards through the list to get files in lowest-priority-first order */
for (guint i = dir_paths->len; i > 0; i--) {
g_autofree gchar *dirpath =
g_canonicalize_filename (subdir, g_ptr_array_index (dir_paths, i - 1));
g_autoptr (GDir) dir = g_dir_open (dirpath, 0, NULL);
if (dir) {
g_autoptr (GArray) dir_items = g_array_new (FALSE, FALSE,
sizeof (struct conffile_iterator_item));
wp_trace ("searching dir: %s", dirpath);
/* Store all filenames with their full path in the local array */
const gchar *filename;
while ((filename = g_dir_read_name (dir))) {
if (filename[0] == '.')
continue;
if (suffix && !g_str_has_suffix (filename, suffix))
continue;
/* verify the file is regular and canonicalize the path */
g_autofree gchar *path = make_path (flags, dirpath, NULL, filename);
if (!g_file_test (path, G_FILE_TEST_IS_REGULAR))
continue;
/* remove item with the same filename from the global items array,
so that lower priority files can be shadowed */
for (guint j = 0; j < items->len; j++) {
struct conffile_iterator_item *item = &g_array_index (items,
struct conffile_iterator_item, j);
if (g_strcmp0 (item->filename, filename) == 0) {
g_array_remove_index (items, j);
break;
}
}
/* append in the local array */
g_array_append_val (dir_items, ((struct conffile_iterator_item) {
.filename = g_strdup (filename),
.path = g_steal_pointer (&path),
}));
}
/* Sort files of the current dir by filename */
g_array_sort (dir_items, (GCompareFunc) conffile_iterator_item_compare);
/* Append the sorted files to the global array */
g_array_append_vals (items, dir_items->data, dir_items->len);
}
}
/* Construct iterator */
WpIterator *it = wp_iterator_new (&conffile_iterator_methods,
sizeof (struct conffile_iterator_data));
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
it_data->items = g_steal_pointer (&items);
it_data->idx = 0;
return g_steal_pointer (&it);
}

89
lib/wp/base-dirs.h Normal file
View file

@ -0,0 +1,89 @@
/* WirePlumber
*
* Copyright © 2024 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_BASE_DIRS_H__
#define __WIREPLUMBER_BASE_DIRS_H__
#include "defs.h"
#include "iterator.h"
G_BEGIN_DECLS
/*!
* \brief Flags to specify lookup directories
* \ingroup wpbasedirs
*
* These flags can be used to specify which directories to look for a file in.
* The flags can be combined to search in multiple directories at once. Some
* flags may also used to specify the type of the file being looked up or other
* lookup parameters.
*
* Lookup is performed in the same order as the flags are listed here. Note that
* if a WirePlumber-specific environment variable is set ($WIREPLUMBER_*_DIR)
* and the equivalent WP_BASE_DIRS_ENV_* flag is specified, the lookup in other
* directories is skipped, even if the file is not found in the
* environment-specified directory.
*/
typedef enum { /*< flags >*/
WP_BASE_DIRS_ENV_CONFIG = (1 << 0), /*!< $WIREPLUMBER_CONFIG_DIR */
WP_BASE_DIRS_ENV_DATA = (1 << 1), /*!< $WIREPLUMBER_DATA_DIR */
WP_BASE_DIRS_ENV_MODULE = (1 << 2), /*!< $WIREPLUMBER_MODULE_DIR */
WP_BASE_DIRS_XDG_CONFIG_HOME = (1 << 8), /*!< XDG_CONFIG_HOME */
WP_BASE_DIRS_XDG_DATA_HOME = (1 << 9), /*!< XDG_DATA_HOME */
WP_BASE_DIRS_XDG_CONFIG_DIRS = (1 << 10), /*!< XDG_CONFIG_DIRS */
WP_BASE_DIRS_BUILD_SYSCONFDIR = (1 << 11), /*!< compile-time $sysconfdir (/etc) */
WP_BASE_DIRS_XDG_DATA_DIRS = (1 << 12), /*!< XDG_DATA_DIRS */
WP_BASE_DIRS_BUILD_DATADIR = (1 << 13), /*!< compile-time $datadir ($prefix/share) */
WP_BASE_DIRS_BUILD_LIBDIR = (1 << 14), /*!< compile-time $libdir ($prefix/lib) */
/*! the file is a loadable module; prepend "lib" and append ".so" if needed */
WP_BASE_DIRS_FLAG_MODULE = (1 << 24),
/*! append "/wireplumber" to the location, except in the case of locations
that are specified via WirePlumber-specific environment variables;
in LIBDIR, append "/wireplumber-$API_version" instead */
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER = (1 << 25),
WP_BASE_DIRS_CONFIGURATION =
(WP_BASE_DIRS_ENV_CONFIG |
WP_BASE_DIRS_XDG_CONFIG_HOME |
WP_BASE_DIRS_XDG_CONFIG_DIRS |
WP_BASE_DIRS_BUILD_SYSCONFDIR |
WP_BASE_DIRS_XDG_DATA_DIRS |
WP_BASE_DIRS_BUILD_DATADIR |
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
WP_BASE_DIRS_DATA =
(WP_BASE_DIRS_ENV_DATA |
WP_BASE_DIRS_XDG_DATA_HOME |
WP_BASE_DIRS_XDG_DATA_DIRS |
WP_BASE_DIRS_BUILD_DATADIR |
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
WP_BASE_DIRS_MODULE =
(WP_BASE_DIRS_ENV_MODULE |
WP_BASE_DIRS_BUILD_LIBDIR |
WP_BASE_DIRS_FLAG_MODULE |
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
} WpBaseDirsFlags;
WP_API
gchar * wp_base_dirs_find_file (WpBaseDirsFlags flags,
const gchar * subdir, const gchar * filename);
WP_API
WpIterator * wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
const gchar * subdir, const gchar * suffix);
G_END_DECLS
#endif

View file

@ -9,6 +9,7 @@
#include "client.h"
#include "log.h"
#include "private/pipewire-object-mixin.h"
#include "private/permission-manager.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
@ -25,6 +26,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
struct _WpClient
{
WpGlobalProxy parent;
GWeakRef permission_manager;
};
static void wp_client_pw_object_mixin_priv_interface_init (
@ -39,6 +41,7 @@ G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
static void
wp_client_init (WpClient * self)
{
g_weak_ref_init (&self->permission_manager, NULL);
}
static void
@ -76,11 +79,27 @@ wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
static void
wp_client_pw_proxy_destroyed (WpProxy * proxy)
{
WpClient *self = WP_CLIENT (proxy);
wp_client_attach_permission_manager (self, NULL);
wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy);
}
static void
wp_impl_node_finalize (GObject * object)
{
WpClient *self = WP_CLIENT (object);
wp_client_attach_permission_manager (self, NULL);
g_weak_ref_clear (&self->permission_manager);
G_OBJECT_CLASS (wp_client_parent_class)->finalize (object);
}
static void
wp_client_class_init (WpClientClass * klass)
{
@ -88,6 +107,7 @@ wp_client_class_init (WpClientClass * klass)
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
WpProxyClass *proxy_class = (WpProxyClass *) klass;
object_class->finalize = wp_impl_node_finalize;
object_class->get_property = wp_pw_object_mixin_get_property;
wpobject_class->get_supported_features =
@ -221,3 +241,30 @@ wp_client_update_properties (WpClient * self, WpProperties * updates)
g_warn_if_fail (client_update_properties_result >= 0);
}
/*!
* \brief Attaches a permission manager in the client to handle permissions
* automatically.
*
* \ingroup wpclient
* \param self the client
* \param pm (transfer none) (nullable): the permission manager to attach, or
* NULL to detach the current permission manager.
*/
void
wp_client_attach_permission_manager (WpClient *self, WpPermissionManager *pm)
{
g_autoptr (WpPermissionManager) curr_pm = NULL;
g_return_if_fail (WP_IS_CLIENT (self));
curr_pm = g_weak_ref_get (&self->permission_manager);
if (curr_pm == pm)
return;
if (curr_pm)
wp_permission_manager_remove_client (curr_pm, self);
if (pm)
wp_permission_manager_add_client (pm, self);
g_weak_ref_set (&self->permission_manager, pm);
}

View file

@ -10,6 +10,7 @@
#define __WIREPLUMBER_CLIENT_H__
#include "global-proxy.h"
#include "permission-manager.h"
G_BEGIN_DECLS
@ -37,6 +38,10 @@ void wp_client_update_permissions_array (WpClient * self,
WP_API
void wp_client_update_properties (WpClient * self, WpProperties * updates);
WP_API
void wp_client_attach_permission_manager (WpClient *self,
WpPermissionManager *pm);
G_END_DECLS
#endif

View file

@ -153,8 +153,8 @@ on_component_loader_load_done (WpComponentLoader * cl, GAsyncResult * res,
* provide if it loads successfully; this can be queried later with
* wp_core_test_feature()
* \param cancellable (nullable): optional GCancellable
* \param callback (scope async): the callback to call when the operation is done
* \param data (closure): data to pass to \a callback
* \param callback (scope async)(closure data): the callback to call when the operation is done
* \param data data to pass to \a callback
*/
void
wp_core_load_component (WpCore * self, const gchar * component,

View file

@ -6,13 +6,14 @@
* SPDX-License-Identifier: MIT
*/
#include "core.h"
#include "conf.h"
#include "log.h"
#include "object-interest.h"
#include "json-utils.h"
#include "base-dirs.h"
#include "error.h"
#include <pipewire/pipewire.h>
#include <spa/utils/result.h>
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
@ -26,19 +27,40 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
* configuration.
*/
typedef struct _WpConfSection WpConfSection;
struct _WpConfSection
{
gchar *name;
WpSpaJson *value;
gchar *location;
};
static void
wp_conf_section_clear (WpConfSection * section)
{
g_free (section->name);
g_clear_pointer (&section->value, wp_spa_json_unref);
g_free (section->location);
}
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpConfSection, wp_conf_section_clear)
struct _WpConf
{
GObject parent;
/* Props */
GWeakRef core;
gchar *name;
WpProperties *properties;
GHashTable *sections;
/* Private */
GArray *conf_sections; /* element-type: WpConfSection */
GPtrArray *files; /* element-type: GMappedFile* */
};
enum {
PROP_0,
PROP_CORE,
PROP_NAME,
PROP_PROPERTIES,
};
G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
@ -46,10 +68,9 @@ G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
static void
wp_conf_init (WpConf * self)
{
g_weak_ref_init (&self->core, NULL);
self->sections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
(GDestroyNotify) wp_spa_json_unref);
self->conf_sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
g_array_set_clear_func (self->conf_sections, (GDestroyNotify) wp_conf_section_clear);
self->files = g_ptr_array_new_with_free_func ((GDestroyNotify) g_mapped_file_unref);
}
static void
@ -59,8 +80,11 @@ wp_conf_set_property (GObject * object, guint property_id,
WpConf *self = WP_CONF (object);
switch (property_id) {
case PROP_CORE:
g_weak_ref_set (&self->core, g_value_get_object (value));
case PROP_NAME:
self->name = g_value_dup_string (value);
break;
case PROP_PROPERTIES:
self->properties = g_value_dup_boxed (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@ -75,8 +99,11 @@ wp_conf_get_property (GObject * object, guint property_id,
WpConf *self = WP_CONF (object);
switch (property_id) {
case PROP_CORE:
g_value_take_object (value, g_weak_ref_get (&self->core));
case PROP_NAME:
g_value_set_string (value, self->name);
break;
case PROP_PROPERTIES:
g_value_take_boxed (value, wp_properties_copy (self->properties));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@ -89,8 +116,11 @@ wp_conf_finalize (GObject * object)
{
WpConf *self = WP_CONF (object);
g_clear_pointer (&self->sections, g_hash_table_unref);
g_weak_ref_clear (&self->core);
wp_conf_close (self);
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_pointer (&self->conf_sections, g_array_unref);
g_clear_pointer (&self->files, g_ptr_array_unref);
g_clear_pointer (&self->name, g_free);
G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object);
}
@ -104,346 +134,469 @@ wp_conf_class_init (WpConfClass * klass)
object_class->set_property = wp_conf_set_property;
object_class->get_property = wp_conf_get_property;
g_object_class_install_property (object_class, PROP_CORE,
g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property(object_class, PROP_NAME,
g_param_spec_string ("name", "name", "The name of the configuration file",
NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property(object_class, PROP_PROPERTIES,
g_param_spec_boxed ("properties", "properties", "WpProperties",
WP_TYPE_PROPERTIES, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
/*!
* \brief Returns the WpConf instance that is associated with the
* given core.
* \brief Creates a new WpConf object
*
* This method will also create the instance and register it with the core
* if it had not been created before.
* This does not open the files, it only creates the object. For most use cases,
* you should use wp_conf_new_open() instead.
*
* \ingroup wpconf
* \param core the core
* \returns (transfer full): the WpConf instance
* \param name the name of the configuration file
* \param properties (transfer full) (nullable): a WpProperties with keys
* specifying how to load the WpConf object
* \returns (transfer full): a new WpConf object
*/
WpConf *
wp_conf_get_instance (WpCore *core)
wp_conf_new (const gchar * name, WpProperties * properties)
{
WpConf *conf = wp_core_find_object (core,
(GEqualFunc) WP_IS_CONF, NULL);
if (G_UNLIKELY (!conf)) {
conf = g_object_new (WP_TYPE_CONF,
"core", core,
NULL);
wp_core_register_object (core, g_object_ref (conf));
wp_info_object (conf, "created wpconf object");
}
return conf;
g_return_val_if_fail (name, NULL);
g_autoptr (WpProperties) props = properties;
return g_object_new (WP_TYPE_CONF, "name", name,
"properties", props,
NULL);
}
static gint
merge_section_cb (void *data, const char *location, const char *section,
const char *str, size_t len)
/*!
* \brief Creates a new WpConf object and opens the configuration file and its
* fragments, keeping them mapped in memory for further access.
*
* \ingroup wpconf
* \param name the name of the configuration file
* \param properties (transfer full) (nullable): a WpProperties with keys
* specifying how to load the WpConf object
* \param error (out) (nullable): return location for a GError, or NULL
* \returns (transfer full) (nullable): a new WpConf object, or NULL
* if an error occurred
*/
WpConf *
wp_conf_new_open (const gchar * name, WpProperties * properties, GError ** error)
{
WpSpaJson **res_section = (WpSpaJson **)data;
g_autoptr (WpSpaJson) json = NULL;
gboolean override;
g_return_val_if_fail (name, NULL);
g_return_val_if_fail (res_section, -EINVAL);
g_autoptr (WpConf) self = wp_conf_new (name, properties);
if (!wp_conf_open (self, error))
return NULL;
return g_steal_pointer (&self);
}
override = g_str_has_prefix (section, OVERRIDE_SECTION_PREFIX);
if (override)
section += strlen (OVERRIDE_SECTION_PREFIX);
static gboolean
detect_old_conf_format (WpConf * self, GMappedFile *file)
{
const gchar *data = g_mapped_file_get_contents (file);
gsize size = g_mapped_file_get_length (file);
wp_debug ("loading section %s (override=%d) from %s", section, override,
location);
/* wireplumber 0.4 used to have components of type = config/lua */
return g_strrstr_len (data, size, "config/lua") ? TRUE : FALSE;
}
/* Only allow sections to be objects or arrays */
json = wp_spa_json_new_wrap_stringn (str, len);
if (!wp_spa_json_is_container (json)) {
wp_warning (
"skipping section %s from %s as it is not JSON object or array",
section, location);
return 0;
static gboolean
open_and_load_sections (WpConf * self, const gchar *path, GError ** error)
{
const gchar *as_section = NULL;
if (self->properties) {
/* as-section="some.name" means that the entire file will be stored as a
single JSON value that will be accessible through wp_conf_get_section()
using "some.name" - no parsing is done; the value is expected to be a
container */
as_section = wp_properties_get (self->properties, "as-section");
wp_debug_object (self, "Reading config file as single section: %s", as_section);
}
/* Merge section if it was defined previously and the 'override.' prefix is
* not used */
if (!override && *res_section) {
g_autoptr (WpSpaJson) merged =
wp_json_utils_merge_containers (*res_section, json);
if (!merged) {
wp_warning (
"skipping merge of %s from %s as JSON values are not compatible",
section, location);
return 0;
g_autoptr (GMappedFile) file = g_mapped_file_new (path, FALSE, error);
if (!file)
return FALSE;
if (!g_mapped_file_get_contents (file) || g_mapped_file_get_length (file) == 0) {
wp_notice_object (self, "Ignoring empty configuration file at '%s'", path);
return TRUE;
}
/* test if the file is a relic from 0.4 */
if (detect_old_conf_format (self, file)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"The configuration file at '%s' is likely an old WirePlumber 0.4 config "
"and is not supported anymore. Try removing it.", path);
return FALSE;
}
g_autoptr (GArray) sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear);
g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (
g_mapped_file_get_contents (file), g_mapped_file_get_length (file));
if (as_section) {
WpConfSection section = { 0, };
if (!wp_spa_json_is_container (json)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"invalid single-section config file start (expected an object or array): %.*s",
(int) wp_spa_json_get_size (json), wp_spa_json_get_data (json));
return FALSE;
}
g_clear_pointer (res_section, wp_spa_json_unref);
*res_section = g_steal_pointer (&merged);
wp_debug ("section %s from %s loaded", location, section);
section.name = g_strdup (as_section);
section.value = g_steal_pointer (&json);
section.location = g_strdup (path);
g_array_append_val (sections, section);
}
/* Otherwise always replace */
else {
g_clear_pointer (res_section, wp_spa_json_unref);
*res_section = g_steal_pointer (&json);
wp_debug ("section %s from %s loaded", location, section);
g_auto (WpConfSection) section = { 0, };
g_autoptr (WpSpaJson) tmp = NULL, toplevel = NULL;
g_autoptr (WpSpaJsonParser) parser = wp_spa_json_parser_new_undefined (json);
/* get the very first token */
tmp = wp_spa_json_parser_get_json (parser);
/* if the top-level token is an object, parse that instead */
if (tmp && wp_spa_json_is_object (tmp)) {
g_clear_pointer (&parser, wp_spa_json_parser_unref);
toplevel = g_steal_pointer (&tmp);
parser = wp_spa_json_parser_new_object (toplevel);
tmp = wp_spa_json_parser_get_json (parser);
}
while (TRUE) {
if (!tmp)
break;
/* if !is_string, but we want to support strings without quotes */
if (wp_spa_json_is_container (tmp) ||
wp_spa_json_is_int (tmp) ||
wp_spa_json_is_float (tmp) ||
wp_spa_json_is_boolean (tmp) ||
wp_spa_json_is_null (tmp))
{
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"invalid section name (not a string): %.*s",
(int) wp_spa_json_get_size (tmp), wp_spa_json_get_data (tmp));
return FALSE;
}
section.name = wp_spa_json_parse_string (tmp);
g_clear_pointer (&tmp, wp_spa_json_unref);
/* parse the section contents */
tmp = wp_spa_json_parser_get_json (parser);
if (!tmp) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"section '%s' has no value", section.name);
return FALSE;
}
section.value = g_steal_pointer (&tmp);
section.location = g_strdup (path);
g_array_append_val (sections, section);
memset (&section, 0, sizeof (section));
/* parse the next section name */
tmp = wp_spa_json_parser_get_json (parser);
}
}
return 0;
/* store the mapped file and the sections; note that the stored WpSpaJson
still point to the data in the GMappedFile, so this is why we keep the
GMappedFile alive */
g_ptr_array_add (self->files, g_steal_pointer (&file));
g_array_append_vals (self->conf_sections, sections->data, sections->len);
g_array_set_clear_func (sections, NULL);
return TRUE;
}
static void
ensure_section_loaded (WpConf *self, const gchar *section)
/*!
* \brief Opens the configuration file and its fragments and keeps them
* mapped in memory for further access.
*
* \ingroup wpconf
* \param self the configuration
* \param error (out)(nullable): return location for a GError, or NULL
* \returns TRUE on success, FALSE on error
*/
gboolean
wp_conf_open (WpConf * self, GError ** error)
{
g_autoptr (WpCore) core = NULL;
struct pw_context *pw_ctx = NULL;
g_autoptr (WpSpaJson) json_section = NULL;
g_autofree gchar *override_section = NULL;
const gchar *no_frags = NULL;
if (g_hash_table_contains (self->sections, section))
return;
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
core = g_weak_ref_get (&self->core);
g_return_if_fail (core);
pw_ctx = wp_core_get_pw_context (core);
g_return_if_fail (pw_ctx);
g_autofree gchar *path = NULL;
g_autoptr (WpIterator) iterator = NULL;
g_auto (GValue) value = G_VALUE_INIT;
pw_context_conf_section_for_each (pw_ctx, section, merge_section_cb,
&json_section);
override_section = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", section);
pw_context_conf_section_for_each (pw_ctx, override_section, merge_section_cb,
&json_section);
if (self->properties) {
no_frags = wp_properties_get (self->properties, "no-fragments");
}
if (json_section)
g_hash_table_insert (self->sections, g_strdup (section),
g_steal_pointer (&json_section));
/*
* open the config file - if the path supplied is absolute,
* wp_base_dirs_find_file will ignore WP_BASE_DIRS_CONFIGURATION
*/
path = wp_base_dirs_find_file (WP_BASE_DIRS_CONFIGURATION, NULL, self->name);
if (path) {
wp_info_object (self, "opening config file: %s", path);
if (!open_and_load_sections (self, path, error))
return FALSE;
}
g_clear_pointer (&path, g_free);
/* open the .conf.d/ fragments */
if (!no_frags) {
path = g_strdup_printf ("%s.d", self->name);
iterator = wp_base_dirs_new_files_iterator (WP_BASE_DIRS_CONFIGURATION, path,
".conf");
for (; wp_iterator_next (iterator, &value); g_value_unset (&value)) {
const gchar *filename = g_value_get_string (&value);
wp_info_object (self, "opening fragment file: %s", filename);
g_autoptr (GError) e = NULL;
if (!open_and_load_sections (self, filename, &e)) {
wp_warning_object (self, "failed to open '%s': %s", filename, e->message);
continue;
}
}
}
if (self->files->len == 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
"Could not locate configuration file '%s'", self->name);
return FALSE;
}
return TRUE;
}
/*!
* \brief Closes the configuration file and its fragments
*
* \ingroup wpconf
* \param self the configuration
*/
void
wp_conf_close (WpConf * self)
{
g_return_if_fail (WP_IS_CONF (self));
g_array_set_size (self->conf_sections, 0);
g_ptr_array_set_size (self->files, 0);
}
/*!
* \brief Tests if the configuration files are open
*
* \ingroup wpconf
* \param self the configuration
* \returns TRUE if the configuration files are open, FALSE otherwise
*/
gboolean
wp_conf_is_open (WpConf * self)
{
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
return self->files->len > 0;
}
/*!
* \brief Gets the name of the configuration file
*
* \ingroup wpconf
* \param self the configuration
* \returns the name of the configuration file
*/
const gchar *
wp_conf_get_name (WpConf * self)
{
g_return_val_if_fail (WP_IS_CONF (self), NULL);
return self->name;
}
static WpSpaJson *
ensure_merged_section (WpConf * self, const gchar *section)
{
g_autoptr (WpSpaJson) merged = NULL;
WpConfSection *merged_section = NULL;
/* check if the section is already merged */
for (guint i = 0; i < self->conf_sections->len; i++) {
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
if (g_str_equal (s->name, section)) {
if (!s->location) {
wp_debug_object (self, "section %s is already merged", section);
return wp_spa_json_ref (s->value);
}
}
}
/* Iterate over the sections and merge them */
for (guint i = 0; i < self->conf_sections->len; i++) {
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
const gchar *s_name = s->name;
/* skip the "override." prefix and take a note */
gboolean override = g_str_has_prefix (s_name, OVERRIDE_SECTION_PREFIX);
if (override)
s_name += strlen (OVERRIDE_SECTION_PREFIX);
if (g_str_equal (s_name, section)) {
/* Merge sections if a previous value exists and
the 'override.' prefix is not present */
if (!override && merged) {
g_autoptr (WpSpaJson) new_merged =
wp_json_utils_merge_containers (merged, s->value);
if (!merged) {
wp_warning_object (self,
"skipping merge of '%s' from '%s' as JSON containers are not compatible",
section, s->location);
continue;
}
g_clear_pointer (&merged, wp_spa_json_unref);
merged = g_steal_pointer (&new_merged);
merged_section = NULL;
}
/* Otherwise always replace */
else {
g_clear_pointer (&merged, wp_spa_json_unref);
merged = wp_spa_json_ref (s->value);
merged_section = s;
}
}
}
/* cache the result */
if (merged_section) {
/* if the merged json came from a single location, just clear
the location from that WpConfSection to mark it as the result */
wp_info_object (self, "section '%s' is used as-is from '%s'", section,
merged_section->location);
g_clear_pointer (&merged_section->location, g_free);
} else if (merged) {
/* if the merged json came from multiple locations, create a new
WpConfSection to store it */
WpConfSection s = { g_strdup (section), wp_spa_json_ref (merged), NULL };
g_array_append_val (self->conf_sections, s);
wp_info_object (self, "section '%s' is merged from multiple locations",
section);
} else {
wp_info_object (self, "section '%s' is not defined", section);
}
return g_steal_pointer (&merged);
}
/*!
* This method will get the JSON value of a specific section from the
* configuration. If the same section is defined in multiple locations, the
* sections with the same name will be either merged in case of arrays and
* objects, or overridden in case of boolean, int, double and strings. The
* passed fallback value will be returned if the section does not exist.
* objects, or overridden in case of boolean, int, double and strings.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param fallback (transfer full)(nullable): the fallback value
* \returns (transfer full): the JSON value of the section
* \returns (transfer full) (nullable): the JSON value of the section or NULL
* if the section does not exist
*/
WpSpaJson *
wp_conf_get_section (WpConf *self, const gchar *section, WpSpaJson *fallback)
wp_conf_get_section (WpConf *self, const gchar *section)
{
WpSpaJson *s;
g_autoptr (WpSpaJson) fb = fallback;
g_return_val_if_fail (WP_IS_CONF (self), NULL);
ensure_section_loaded (self, section);
s = g_hash_table_lookup (self->sections, section);
if (!s)
return fb ? g_steal_pointer (&fb) : NULL;
return wp_spa_json_ref (s);
}
/*!
* This is a convenient function to access a JSON value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback (transfer full)(nullable): the fallback value
* \returns (transfer full): the JSON value of the section's key if it exists,
* or the passed fallback value otherwise
*/
WpSpaJson *
wp_conf_get_value (WpConf *self, const gchar *section, const gchar *key,
WpSpaJson *fallback)
{
g_autoptr (WpSpaJson) s = NULL;
g_autoptr (WpSpaJson) fb = fallback;
WpSpaJson *v;
g_return_val_if_fail (WP_IS_CONF (self), NULL);
g_return_val_if_fail (section, NULL);
g_return_val_if_fail (key, NULL);
s = wp_conf_get_section (self, section, NULL);
if (!s)
goto return_fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get JSON key %s from %s as section is not an JSON object",
key, section);
goto return_fallback;
}
if (wp_spa_json_object_get (s, key, "J", &v, NULL))
return v;
return_fallback:
return fb ? g_steal_pointer (&fb) : NULL;
return ensure_merged_section (self, section);
}
/*!
* This is a convenient function to access a boolean value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
* \brief Updates the given properties with the values of a specific section
* from the configuration.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback the fallback value
* \returns the boolean value of the section's key if it exists and could be
* parsed, or the passed fallback value otherwise
*/
gboolean
wp_conf_get_value_boolean (WpConf *self, const gchar *section,
const gchar *key, gboolean fallback)
{
g_autoptr (WpSpaJson) s = NULL;
gboolean v;
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
g_return_val_if_fail (section, FALSE);
g_return_val_if_fail (key, FALSE);
s = wp_conf_get_section (self, section, NULL);
if (!s)
return fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get boolean key %s from %s as section is not an JSON object",
key, section);
return fallback;
}
return wp_spa_json_object_get (s, key, "b", &v, NULL) ? v : fallback;
}
/*!
* This is a convenient function to access a int value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback the fallback value
* \returns the int value of the section's key if it exists and could be
* parsed, or the passed fallback value otherwise
* \param props the properties to update
* \returns the number of properties updated
*/
gint
wp_conf_get_value_int (WpConf *self, const gchar *section,
const gchar *key, gint fallback)
wp_conf_section_update_props (WpConf *self, const gchar *section,
WpProperties *props)
{
g_autoptr (WpSpaJson) s = NULL;
gint v;
g_autoptr (WpSpaJson) json = NULL;
g_return_val_if_fail (WP_IS_CONF (self), 0);
g_return_val_if_fail (section, 0);
g_return_val_if_fail (key, 0);
g_return_val_if_fail (WP_IS_CONF (self), -1);
g_return_val_if_fail (section, -1);
g_return_val_if_fail (props, -1);
s = wp_conf_get_section (self, section, NULL);
if (!s)
return fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get int key %s from %s as section is not an JSON object",
key, section);
return fallback;
}
return wp_spa_json_object_get (s, key, "i", &v, NULL) ? v : fallback;
json = wp_conf_get_section (self, section);
if (!json)
return 0;
return wp_properties_update_from_json (props, json);
}
#include "private/parse-conf-section.c"
/*!
* This is a convenient function to access a float value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
* \brief Parses standard pw_context sections from \a conf
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback the fallback value
* \returns the float value of the section's key if it exists and could be
* parsed, or the passed fallback value otherwise
* \param context the associated pw_context
*/
float
wp_conf_get_value_float (WpConf *self, const gchar *section,
const gchar *key, float fallback)
void
wp_conf_parse_pw_context_sections (WpConf * self, struct pw_context * context)
{
g_autoptr (WpSpaJson) s = NULL;
float v;
gint res;
WpProperties *conf_wp;
struct pw_properties *conf_pw;
g_return_val_if_fail (WP_IS_CONF (self), 0);
g_return_val_if_fail (section, 0);
g_return_val_if_fail (key, 0);
g_return_if_fail (WP_IS_CONF (self));
g_return_if_fail (context);
s = wp_conf_get_section (self, section, NULL);
if (!s)
return fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get float key %s from %s as section is not an JSON object",
key, section);
return fallback;
/* convert needed sections into a pipewire-style conf dictionary */
conf_wp = wp_properties_new ("config.path", "wpconf", NULL);
{
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.spa-libs");
if (j) {
g_autofree gchar *js = wp_spa_json_parse_string (j);
wp_properties_set (conf_wp, "context.spa-libs", js);
}
}
return wp_spa_json_object_get (s, key, "f", &v, NULL) ? v : fallback;
}
/*!
* This is a convenient function to access a string value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback (nullable): the fallback value
* \returns (transfer full): the string value of the section's key if it exists
* and could be parsed, or the passed fallback value otherwise
*/
gchar *
wp_conf_get_value_string (WpConf *self, const gchar *section,
const gchar *key, const gchar *fallback)
{
g_autoptr (WpSpaJson) s = NULL;
gchar *v;
g_return_val_if_fail (WP_IS_CONF (self), NULL);
g_return_val_if_fail (section, NULL);
g_return_val_if_fail (key, NULL);
s = wp_conf_get_section (self, section, NULL);
if (!s)
goto return_fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get string key %s from %s as section is not an JSON object",
key, section);
goto return_fallback;
{
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.modules");
if (j) {
g_autofree gchar *js = wp_spa_json_parse_string (j);
wp_properties_set (conf_wp, "context.modules", js);
}
}
conf_pw = wp_properties_unref_and_take_pw_properties (conf_wp);
if (wp_spa_json_object_get (s, key, "s", &v, NULL))
return v;
/* parse sections */
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.spa-libs")) < 0)
goto error;
wp_info_object (self, "parsed %d context.spa-libs items", res);
return_fallback:
return fallback ? g_strdup (fallback) : NULL;
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.modules")) < 0)
goto error;
if (res > 0)
wp_info_object (self, "parsed %d context.modules items", res);
else
wp_warning_object (self, "no modules loaded from context.modules");
out:
pw_properties_free (conf_pw);
return;
error:
wp_critical_object (self, "failed to parse pw_context sections: %s",
spa_strerror (res));
goto out;
}

View file

@ -14,6 +14,8 @@
G_BEGIN_DECLS
struct pw_context;
/*!
* \brief The WpConf GType
* \ingroup wpconf
@ -24,31 +26,34 @@ WP_API
G_DECLARE_FINAL_TYPE (WpConf, wp_conf, WP, CONF, GObject)
WP_API
WpConf * wp_conf_get_instance (WpCore * core);
WpConf * wp_conf_new (const gchar * name, WpProperties * properties);
WP_API
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section,
WpSpaJson *fallback);
WpConf * wp_conf_new_open (const gchar * name, WpProperties * properties,
GError ** error);
WP_API
WpSpaJson *wp_conf_get_value (WpConf *self,
const gchar *section, const gchar *key, WpSpaJson *fallback);
gboolean wp_conf_open (WpConf * self, GError ** error);
WP_API
gboolean wp_conf_get_value_boolean (WpConf *self,
const gchar *section, const gchar *key, gboolean fallback);
void wp_conf_close (WpConf * self);
WP_API
gint wp_conf_get_value_int (WpConf *self,
const gchar *section, const gchar *key, gint fallback);
gboolean wp_conf_is_open (WpConf * self);
WP_API
float wp_conf_get_value_float (WpConf *self,
const gchar *section, const gchar *key, float fallback);
const gchar * wp_conf_get_name (WpConf * self);
WP_API
gchar *wp_conf_get_value_string (WpConf *self,
const gchar *section, const gchar *key, const gchar *fallback);
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section);
WP_API
gint wp_conf_section_update_props (WpConf * self, const gchar * section,
WpProperties * props);
WP_API
void wp_conf_parse_pw_context_sections (WpConf * self,
struct pw_context * context);
G_END_DECLS

View file

@ -30,20 +30,23 @@ struct _WpLoopSource
{
GSource parent;
struct pw_loop *loop;
gboolean entered;
};
static gboolean
wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
{
WpLoopSource *ls = WP_LOOP_SOURCE (s);
int result;
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
if (!ls->entered) {
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
pw_loop_enter (ls->loop);
ls->entered = TRUE;
g_source_set_ready_time (s, -1);
}
pw_loop_enter (WP_LOOP_SOURCE(s)->loop);
result = pw_loop_iterate (WP_LOOP_SOURCE(s)->loop, 0);
pw_loop_leave (WP_LOOP_SOURCE(s)->loop);
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
result = pw_loop_iterate (ls->loop, 0);
if (G_UNLIKELY (result < 0))
wp_warning_boxed (G_TYPE_SOURCE, s,
@ -55,7 +58,21 @@ wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
static void
wp_loop_source_finalize (GSource * s)
{
pw_loop_destroy (WP_LOOP_SOURCE(s)->loop);
WpLoopSource *ls = WP_LOOP_SOURCE (s);
wp_trace_boxed (G_TYPE_SOURCE, s, "finalize loop source");
/* Source should be left from the thread it was entered from.
*
* This puts additional restrictions to upper layers on how WpLoopSource (and
* WpCore) can be used: they must be finalized from the GMainContext thread.
*/
if (ls->entered) {
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
pw_loop_leave (ls->loop);
}
pw_loop_destroy (ls->loop);
}
static GSourceFuncs source_funcs = {
@ -75,6 +92,9 @@ wp_loop_source_new (void)
pw_loop_get_fd (WP_LOOP_SOURCE(s)->loop),
G_IO_IN | G_IO_ERR | G_IO_HUP);
/* dispatch immediately to enter the loop */
g_source_set_ready_time (s, 0);
return (GSource *) s;
}
@ -96,6 +116,25 @@ wp_loop_source_new (void)
* objects that appear in the registry, making them accessible through
* the WpObjectManager API.
*
* The core is also responsible for loading components, which are defined in
* the main configuration file. Components are loaded when
* WP_CORE_FEATURE_COMPONENTS is activated.
*
* \b Configuration
*
* The main configuration file needs to be created and opened before the core
* is created, using the WpConf API. It is then passed to the core as an
* argument in the constructor.
*
* If a configuration file is not provided, the core will let the underlying
* `pw_context` load its own configuration, based on the rules that apply to
* all pipewire clients (e.g. it respects the `PIPEWIRE_CONFIG_NAME` environment
* variable and loads "client.conf" as a last resort).
*
* If a configuration file is provided, the core does not let the underlying
* `pw_context` load any configuration and instead uses the provided WpConf
* object.
*
* \gproperties
*
* \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
@ -110,6 +149,9 @@ wp_loop_source_new (void)
* \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE,
* The pipewire core}
*
* \gproperty{conf, WpConf *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
* The main configuration file}
*
* \gsignals
*
* \par connected
@ -151,16 +193,25 @@ struct _WpCore
struct spa_hook core_listener;
struct spa_hook proxy_core_listener;
/* the main configuration file */
WpConf *conf;
WpRegistry registry;
GHashTable *async_tasks; // <int seq, GTask*>
};
struct context_data {
grefcount rc;
GSource *loop_source;
};
enum {
PROP_0,
PROP_G_MAIN_CONTEXT,
PROP_PROPERTIES,
PROP_PW_CONTEXT,
PROP_PW_CORE,
PROP_CONF,
};
enum {
@ -276,16 +327,32 @@ static void
wp_core_constructed (GObject *object)
{
WpCore *self = WP_CORE (object);
g_autoptr (GSource) source = NULL;
/* loop */
source = wp_loop_source_new ();
g_source_attach (source, self->g_main_context);
/* context */
if (!self->pw_context) {
struct pw_properties *p = NULL;
const gchar *str = NULL;
g_autoptr (GSource) source = wp_loop_source_new ();
/* use our own configuration file, if specified */
if (self->conf) {
wp_info_object (self, "using configuration file: %s",
wp_conf_get_name (self->conf));
/* ensure we have our very own properties set,
since we are going to modify it */
self->properties = self->properties ?
wp_properties_ensure_unique_owner (self->properties) :
wp_properties_new_empty ();
/* load context.properties */
wp_conf_section_update_props (self->conf, "context.properties",
self->properties);
/* disable loading of a configuration file in pw_context */
wp_properties_set (self->properties, PW_KEY_CONFIG_NAME, "null");
wp_properties_set (self->properties, "context.modules.allow-empty", "true");
}
/* properties are fully stored in the pw_context, no need to keep a copy */
p = self->properties ?
@ -293,26 +360,34 @@ wp_core_constructed (GObject *object)
self->properties = NULL;
self->pw_context = pw_context_new (WP_LOOP_SOURCE(source)->loop, p,
sizeof (grefcount));
sizeof (struct context_data));
g_return_if_fail (self->pw_context);
/* use the same config option as pipewire to set the log level */
p = (struct pw_properties *) pw_context_get_properties (self->pw_context);
if (!g_getenv("WIREPLUMBER_DEBUG") &&
(str = pw_properties_get(p, "log.level")) != NULL) {
if (!wp_log_set_global_level (str))
if (!wp_log_set_level (str))
wp_warning ("ignoring invalid log.level in config file: %s", str);
}
/* parse pw_context specific configuration sections */
if (self->conf)
wp_conf_parse_pw_context_sections (self->conf, self->pw_context);
/* Init refcount */
grefcount *rc = pw_context_get_user_data (self->pw_context);
g_return_if_fail (rc);
g_ref_count_init (rc);
struct context_data *cd = pw_context_get_user_data (self->pw_context);
g_return_if_fail (cd);
g_ref_count_init (&cd->rc);
cd->loop_source = g_source_ref (source);
/* Start source */
g_source_attach (source, self->g_main_context);
} else {
/* Increase refcount */
grefcount *rc = pw_context_get_user_data (self->pw_context);
g_return_if_fail (rc);
g_ref_count_inc (rc);
struct context_data *cd = pw_context_get_user_data (self->pw_context);
g_return_if_fail (cd);
g_ref_count_inc (&cd->rc);
}
G_OBJECT_CLASS (wp_core_parent_class)->constructed (object);
@ -333,18 +408,24 @@ static void
wp_core_finalize (GObject * obj)
{
WpCore *self = WP_CORE (obj);
grefcount *rc = pw_context_get_user_data (self->pw_context);
g_return_if_fail (rc);
struct context_data *cd = pw_context_get_user_data (self->pw_context);
g_return_if_fail (cd);
wp_core_disconnect (self);
/* Clear pw-context if refcount reaches 0 */
if (g_ref_count_dec (rc))
if (g_ref_count_dec (&cd->rc)) {
GSource *source = cd->loop_source;
g_clear_pointer (&self->pw_context, pw_context_destroy);
g_source_destroy (source);
g_source_unref (source);
}
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_pointer (&self->g_main_context, g_main_context_unref);
g_clear_pointer (&self->async_tasks, g_hash_table_unref);
g_clear_object (&self->conf);
wp_debug_object (self, "WpCore destroyed");
@ -370,6 +451,9 @@ wp_core_get_property (GObject * object, guint property_id,
case PROP_PW_CORE:
g_value_set_pointer (value, self->pw_core);
break;
case PROP_CONF:
g_value_set_object (value, self->conf);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
@ -392,6 +476,9 @@ wp_core_set_property (GObject * object, guint property_id,
case PROP_PW_CONTEXT:
self->pw_context = g_value_get_pointer (value);
break;
case PROP_CONF:
self->conf = g_value_dup_object (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
@ -447,6 +534,11 @@ on_components_loaded (WpCore * self, GAsyncResult *res,
return;
}
if (self->conf) {
wp_info_object (self, "done loading components, closing conf file...");
wp_conf_close (self->conf);
}
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
}
@ -541,6 +633,11 @@ wp_core_class_init (WpCoreClass * klass)
g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core",
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_CONF,
g_param_spec_object ("conf", "conf", "The main configuration file",
WP_TYPE_CONF,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
signals[SIGNAL_CONNECTED] = g_signal_new ("connected",
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
@ -555,16 +652,20 @@ wp_core_class_init (WpCoreClass * klass)
*
* \ingroup wpcore
* \param context (transfer none) (nullable): the GMainContext to use for events
* \param properties (transfer full) (nullable): additional properties, which are
* passed to pw_context_new() and pw_context_connect()
* \param conf (transfer full) (nullable): the main configuration file
* \param properties (transfer full) (nullable): additional properties, which
* are also passed to pw_context_new() and pw_context_connect()
* \returns (transfer full): a new WpCore
*/
WpCore *
wp_core_new (GMainContext *context, WpProperties * properties)
wp_core_new (GMainContext * context, WpConf * conf, WpProperties * properties)
{
g_autoptr (WpConf) c = conf;
g_autoptr (WpProperties) props = properties;
return g_object_new (WP_TYPE_CORE,
"g-main-context", context,
"conf", conf,
"properties", properties,
"pw-context", NULL,
NULL);
@ -583,6 +684,7 @@ wp_core_clone (WpCore * self)
return g_object_new (WP_TYPE_CORE,
"core", self,
"g-main-context", self->g_main_context,
"conf", self->conf,
"properties", self->properties,
"pw-context", self->pw_context,
NULL);
@ -618,6 +720,20 @@ wp_core_get_export_core (WpCore * self)
return wp_core_find_object (self, find_export_core, NULL);
}
/*!
* \brief Gets the main configuration file of the core
*
* \ingroup wpcore
* \param self the core
* \returns (transfer full) (nullable): the main configuration file
*/
WpConf *
wp_core_get_conf (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
return self->conf ? g_object_ref (self->conf) : NULL;
}
/*!
* \brief Gets the GMainContext of the core
*
@ -735,6 +851,37 @@ wp_core_get_vm_type (WpCore *self)
return res;
}
static gboolean
wp_core_connect_internal (WpCore *self, int fd)
{
struct pw_properties *p = NULL;
/* Don't do anything if core is already connected */
if (self->pw_core)
return TRUE;
/* Connect */
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
if (fd == -1)
self->pw_core = pw_context_connect (self->pw_context, p, 0);
else
self->pw_core = pw_context_connect_fd (self->pw_context, fd, p, 0);
if (!self->pw_core)
return FALSE;
/* Add the core listeners */
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
&self->proxy_core_listener, &proxy_core_events, self);
/* Add the registry listener */
wp_registry_attach (&self->registry, self->pw_core);
return TRUE;
}
/*!
* \brief Connects this core to the PipeWire server.
*
@ -748,29 +895,31 @@ wp_core_get_vm_type (WpCore *self)
gboolean
wp_core_connect (WpCore *self)
{
struct pw_properties *p = NULL;
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
/* Don't do anything if core is already connected */
if (self->pw_core)
return TRUE;
return wp_core_connect_internal (self, -1);
}
/* Connect */
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
self->pw_core = pw_context_connect (self->pw_context, p, 0);
if (!self->pw_core)
return FALSE;
/*!
* \brief Connects this core to the PipeWire server on the given socket.
*
* When connection succeeds, the WpCore \c "connected" signal is emitted.
*
* \ingroup wpcore
* \param self the core
* \param fd the connected socket to use, the socket will be closed
* automatically on disconnect or error
* \returns TRUE if the core is effectively connected or FALSE if
* connection failed
* \since 0.5.6
*/
gboolean
wp_core_connect_fd (WpCore *self, int fd)
{
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
g_return_val_if_fail (fd > -1, FALSE);
/* Add the core listeners */
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
&self->proxy_core_listener, &proxy_core_events, self);
/* Add the registry listener */
wp_registry_attach (&self->registry, self->pw_core);
return TRUE;
return wp_core_connect_internal (self, fd);
}
/*!
@ -989,8 +1138,8 @@ wp_core_update_properties (WpCore * self, WpProperties * updates)
* \ingroup wpcore
* \param self the core
* \param source (out) (optional): the source
* \param function (scope notified): the function to call
* \param data (closure): data to pass to \a function
* \param function (scope notified)(closure data)(destroy destroy): the function to call
* \param data data to pass to \a function
* \param destroy (nullable): a function to destroy \a data
*/
void
@ -1054,8 +1203,8 @@ wp_core_idle_add_closure (WpCore * self, GSource **source, GClosure * closure)
* \param self the core
* \param source (out) (optional): the source
* \param timeout_ms the timeout in milliseconds
* \param function (scope notified): the function to call
* \param data (closure): data to pass to \a function
* \param function (scope notified)(closure data)(destroy destroy): the function to call
* \param data data to pass to \a function
* \param destroy (nullable): a function to destroy \a data
*/
void
@ -1118,8 +1267,8 @@ wp_core_timeout_add_closure (WpCore * self, GSource **source, guint timeout_ms,
* \ingroup wpcore
* \param self the core
* \param cancellable (nullable): a GCancellable to cancel the operation
* \param callback (scope async): a function to call when the operation is done
* \param user_data (closure): data to pass to \a callback
* \param callback (scope async)(closure user_data): a function to call when the operation is done
* \param user_data data to pass to \a callback
* \returns TRUE if the sync operation was started, FALSE if an error
* occurred before returning from this function
*/
@ -1229,6 +1378,7 @@ wp_core_sync_finish (WpCore * self, GAsyncResult * res, GError ** error)
/*!
* \brief Finds a registered object
*
* \ingroup wpcore
* \param self the core
* \param func (scope call): a function that takes the object being searched
* as the first argument and \a data as the second. it should return TRUE if

View file

@ -12,6 +12,7 @@
#include "object.h"
#include "properties.h"
#include "spa-json.h"
#include "conf.h"
G_BEGIN_DECLS
@ -41,7 +42,8 @@ G_DECLARE_FINAL_TYPE (WpCore, wp_core, WP, CORE, WpObject)
/* Basic */
WP_API
WpCore * wp_core_new (GMainContext *context, WpProperties * properties);
WpCore * wp_core_new (GMainContext * context, WpConf * conf,
WpProperties * properties);
WP_API
WpCore * wp_core_clone (WpCore * self);
@ -49,6 +51,9 @@ WpCore * wp_core_clone (WpCore * self);
WP_API
WpCore * wp_core_get_export_core (WpCore * self);
WP_API
WpConf * wp_core_get_conf (WpCore * self);
WP_API
GMainContext * wp_core_get_g_main_context (WpCore * self);
@ -66,6 +71,9 @@ gchar *wp_core_get_vm_type (WpCore *self);
WP_API
gboolean wp_core_connect (WpCore *self);
WP_API
gboolean wp_core_connect_fd (WpCore *self, int fd);
WP_API
void wp_core_disconnect (WpCore *self);

View file

@ -199,6 +199,7 @@ struct _WpSpaDevice
struct spa_hook listener;
WpProperties *properties;
GPtrArray *managed_objs;
GPtrArray *pending_obj_config;
};
enum {
@ -225,11 +226,19 @@ object_unref_safe (gpointer object)
g_object_unref (object);
}
static void
pod_unref_safe (gpointer object)
{
if (object)
wp_spa_pod_unref (object);
}
static void
wp_spa_device_init (WpSpaDevice * self)
{
self->properties = wp_properties_new_empty ();
self->managed_objs = g_ptr_array_new_with_free_func (object_unref_safe);
self->pending_obj_config = g_ptr_array_new_with_free_func (pod_unref_safe);
}
static void
@ -262,6 +271,7 @@ wp_spa_device_finalize (GObject * object)
g_clear_pointer (&self->handle, pw_unload_spa_handle);
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_pointer (&self->managed_objs, g_ptr_array_unref);
g_clear_pointer (&self->pending_obj_config, g_ptr_array_unref);
G_OBJECT_CLASS (wp_spa_device_parent_class)->finalize (object);
}
@ -313,8 +323,8 @@ spa_device_event_info (void *data, const struct spa_device_info *info)
WpSpaDevice *self = WP_SPA_DEVICE (data);
/*
* This is emited syncrhonously at the time we add the listener and
* before object_info is emited. It gives us additional properties
* This is emitted synchronously at the time we add the listener and
* before object_info is emitted. It gives us additional properties
* about the device, like the "api.alsa.card.*" ones that are not
* set by the monitor
*/
@ -322,6 +332,67 @@ spa_device_event_info (void *data, const struct spa_device_info *info)
wp_properties_update_from_dict (self->properties, info->props);
}
static WpSpaPod *
pending_obj_config_pop (WpSpaDevice *self, guint32 id)
{
if (id < self->pending_obj_config->len)
return g_steal_pointer (&g_ptr_array_index (self->pending_obj_config, id));
return NULL;
}
static void
pending_obj_config_set (WpSpaDevice *self, guint32 id, WpSpaPod *props)
{
if (id >= self->pending_obj_config->len)
g_ptr_array_set_size (self->pending_obj_config, id + 1);
gpointer *ptr = &g_ptr_array_index (self->pending_obj_config, id);
pod_unref_safe (*ptr);
*ptr = props;
}
static void
append_props (WpSpaPodBuilder *b, WpSpaPod *props, GHashTable *used)
{
g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (props);
GValue next = G_VALUE_INIT;
for (; wp_iterator_next (it, &next); g_value_unset (&next)) {
WpSpaPod *p = g_value_get_boxed (&next);
const char *key;
g_autoptr (WpSpaPod) value = NULL;
if (!wp_spa_pod_get_property (p, &key, &value))
continue;
if (g_hash_table_contains(used, key))
continue;
wp_spa_pod_builder_add_property (b, key);
wp_spa_pod_builder_add_pod (b, value);
g_hash_table_add (used, (gpointer) g_strdup (key));
}
}
static WpSpaPod *
merge_props (WpSpaPod *old_props, WpSpaPod *new_props)
{
g_autoptr (GHashTable) used = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_object (
"Spa:Pod:Object:Param:Props", "Props");
if (new_props) {
append_props (b, new_props, used);
wp_spa_pod_unref (new_props);
}
if (old_props) {
append_props (b, old_props, used);
wp_spa_pod_unref (old_props);
}
return wp_spa_pod_builder_end (b);
}
static void
spa_device_event_event (void *data, const struct spa_event *event)
{
@ -341,10 +412,18 @@ spa_device_event_event (void *data, const struct spa_event *event)
NULL))
child = wp_spa_device_get_managed_object (self, id);
if (child && !g_strcmp0 (type, "ObjectConfig") &&
WP_IS_PIPEWIRE_OBJECT (child) && props) {
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
g_steal_pointer (&props));
if (!g_strcmp0 (type, "ObjectConfig") && props) {
if (child && WP_IS_PIPEWIRE_OBJECT (child)) {
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
g_steal_pointer (&props));
} else if (!child) {
/* Save Props set on ids pending for a managed object */
WpSpaPod *pending_props = pending_obj_config_pop (self, id);
if (pending_props) {
pending_props = merge_props (pending_props, g_steal_pointer(&props));
pending_obj_config_set (self, id, pending_props);
}
}
}
}
@ -359,12 +438,23 @@ spa_device_event_object_info (void *data, uint32_t id,
g_autoptr (WpProperties) props = NULL;
type = spa_debug_type_short_name (info->type);
props = wp_properties_new_wrap_dict (info->props);
props = wp_properties_new_copy_dict (info->props);
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
id, type, info->factory_name);
if (id < self->managed_objs->len &&
g_ptr_array_index (self->managed_objs, id) != NULL) {
wp_debug_object (self, "object already exists, removing");
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
wp_spa_device_store_managed_object (self, id, NULL);
}
g_signal_emit (self, spa_device_signals[SIGNAL_CREATE_OBJECT], 0,
id, type, info->factory_name, props);
}
else {
wp_debug_object (self, "object removed: id:%u", id);
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
wp_spa_device_store_managed_object (self, id, NULL);
}
@ -447,6 +537,7 @@ wp_spa_device_deactivate (WpObject * object, WpObjectFeatures features)
WpSpaDevice *self = WP_SPA_DEVICE (object);
spa_hook_remove (&self->listener);
g_ptr_array_set_size (self->managed_objs, 0);
g_ptr_array_set_size (self->pending_obj_config, 0);
wp_object_update_features (object, 0, WP_SPA_DEVICE_FEATURE_ENABLED);
}
}
@ -697,4 +788,40 @@ wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
if (*ptr)
g_object_unref (*ptr);
*ptr = object;
/* Clear pending status, and set pending props if any */
g_autoptr(WpSpaPod) props = pending_obj_config_pop (self, id);
if (props && object && WP_IS_PIPEWIRE_OBJECT (object)) {
wp_trace_boxed (WP_TYPE_SPA_POD, props, "pending ObjectConfig, object %d", id);
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (object), "Props", 0,
g_steal_pointer (&props));
}
}
/*!
* \brief Marks a managed object id pending.
*
* When an object id is pending, Props from received ObjectConfig events
* for the id are saved. When \ref wp_spa_device_store_managed_object later sets
* an object for the id, the saved Props are immediately set on the object and
* pending status is cleared.
*
* If an object is already set for the id, this has no effect.
*
* \ingroup wpspadevice
* \param self the spa device
* \param id the (device-internal) id of the object
*/
void
wp_spa_device_set_managed_pending (WpSpaDevice * self, guint id)
{
g_return_if_fail (WP_IS_SPA_DEVICE (self));
g_autoptr (GObject) obj = wp_spa_device_get_managed_object (self, id);
if (obj)
return;
pending_obj_config_set (self, id,
wp_spa_pod_new_object ("Spa:Pod:Object:Param:Props", "Props", NULL));
}

View file

@ -67,6 +67,9 @@ WP_API
void wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
GObject * object);
WP_API
void wp_spa_device_set_managed_pending (WpSpaDevice * self, guint id);
G_END_DECLS
#endif

View file

@ -15,6 +15,161 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
typedef struct _HookData HookData;
struct _HookData
{
struct spa_list link;
WpEventHook *hook;
GPtrArray *dependencies;
};
static inline HookData *
hook_data_new (WpEventHook * hook)
{
HookData *hook_data = g_new0 (HookData, 1);
spa_list_init (&hook_data->link);
hook_data->hook = g_object_ref (hook);
hook_data->dependencies = g_ptr_array_new ();
return hook_data;
}
static void
hook_data_free (HookData *self)
{
g_clear_object (&self->hook);
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
g_free (self);
}
static inline void
record_dependency (struct spa_list *list, const gchar *target,
const gchar *dependency)
{
HookData *hook_data;
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
break;
}
}
}
static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
return TRUE;
}
}
}
return FALSE;
}
static gboolean
sort_hooks (GPtrArray *hooks)
{
struct spa_list collected, result, remaining;
HookData *sorted_hook_data = NULL;
spa_list_init (&collected);
spa_list_init (&result);
spa_list_init (&remaining);
for (guint i = 0; i < hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (hooks, i);
HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
}
if (!spa_list_is_empty (&collected)) {
HookData *hook_data;
/* convert "before" dependencies into "after" dependencies */
spa_list_for_each (hook_data, &collected, link) {
const gchar * const * strv =
wp_event_hook_get_runs_before_hooks (hook_data->hook);
while (strv && *strv) {
/* record hook_data->hook as a dependency of the *strv hook */
record_dependency (&collected, *strv,
wp_event_hook_get_name (hook_data->hook));
strv++;
}
}
/* sort */
while (!spa_list_is_empty (&collected)) {
gboolean made_progress = FALSE;
/* examine each hook to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
spa_list_consume (hook_data, &collected, link) {
guint deps_satisfied = 0;
spa_list_remove (&hook_data->link);
for (guint i = 0; i < hook_data->dependencies->len; i++) {
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
/* if the dependency is already in the sorted result list or if
it doesn't exist at all, we consider it satisfied */
if (hook_exists_in (dep, &result) ||
!(hook_exists_in (dep, &collected) ||
hook_exists_in (dep, &remaining))) {
deps_satisfied++;
}
}
if (deps_satisfied == hook_data->dependencies->len) {
spa_list_append (&result, &hook_data->link);
made_progress = TRUE;
} else {
spa_list_append (&remaining, &hook_data->link);
}
}
if (made_progress) {
/* run again with the remaining hooks */
spa_list_insert_list (&collected, &remaining);
spa_list_init (&remaining);
}
else if (!spa_list_is_empty (&remaining)) {
/* if we did not make any progress towards growing the result list,
it means the dependencies cannot be satisfied because of circles */
spa_list_consume (hook_data, &result, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
spa_list_consume (hook_data, &remaining, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
return FALSE;
}
}
}
/* clear hooks and add the sorted ones */
g_ptr_array_set_size (hooks, 0);
spa_list_consume (sorted_hook_data, &result, link) {
spa_list_remove (&sorted_hook_data->link);
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
hook_data_free (sorted_hook_data);
}
return TRUE;
}
typedef struct _EventData EventData;
struct _EventData
{
@ -49,7 +204,8 @@ struct _WpEventDispatcher
GObject parent;
GWeakRef core;
GPtrArray *hooks; /* registered hooks */
GHashTable *defined_hooks; /* registered hooks for defined events */
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
GSource *source; /* the event loop source */
GList *events; /* the events stack */
struct spa_system *system;
@ -104,7 +260,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
/* get the highest priority event */
GList *levent = g_list_first (d->events);
while (levent) {
if (levent) {
EventData *event_data = (EventData *) (levent->data);
WpEvent *event = event_data->event;
GCancellable *cancellable = wp_event_get_cancellable (event);
@ -144,6 +300,8 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
/* get the next event */
levent = g_list_first (d->events);
if (levent && !((EventData *) levent->data)->current_hook_in_async)
spa_system_eventfd_write (d->system, d->eventfd, 1);
}
return G_SOURCE_CONTINUE;
@ -160,7 +318,9 @@ static void
wp_event_dispatcher_init (WpEventDispatcher * self)
{
g_weak_ref_init (&self->core, NULL);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
(GDestroyNotify)g_ptr_array_unref);
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
((WpEventSource *) self->source)->dispatcher = self;
@ -184,7 +344,8 @@ wp_event_dispatcher_finalize (GObject * object)
close (self->eventfd);
g_clear_pointer (&self->hooks, g_ptr_array_unref);
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
g_weak_ref_clear (&self->core);
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
@ -264,7 +425,7 @@ wp_event_dispatcher_push_event (WpEventDispatcher * self, WpEvent * event)
self->events = g_list_insert_sorted (self->events, event_data,
(GCompareFunc) event_cmp_func);
wp_trace_object (self, "pushed event (%s)", wp_event_get_name (event));
wp_debug_object (self, "pushed event (%s)", wp_event_get_name (event));
/* wakeup the GSource */
spa_system_eventfd_write (self->system, self->eventfd, 1);
@ -284,6 +445,10 @@ void
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
WpEventHook * hook)
{
g_autoptr (GPtrArray) event_types = NULL;
gboolean is_defined = FALSE;
const gchar *hook_name;
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
@ -292,12 +457,79 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == NULL);
wp_event_hook_set_dispatcher (hook, self);
g_ptr_array_add (self->hooks, g_object_ref (hook));
/* Register the event hook in the defined hooks table if it is defined */
hook_name = wp_event_hook_get_name (hook);
event_types = wp_event_hook_get_matching_event_types (hook);
if (event_types) {
for (guint i = 0; i < event_types->len; i++) {
const gchar *event_type = g_ptr_array_index (event_types, i);
GPtrArray *hooks;
wp_debug_object (self, "Registering hook %s for defined event type %s",
hook_name, event_type);
/* Check if the event type was registered in the hash table */
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
if (hooks) {
g_ptr_array_add (hooks, g_object_ref (hook));
if (!sort_hooks (hooks))
goto sort_error;
} else {
GPtrArray *new_hooks = g_ptr_array_new_with_free_func (g_object_unref);
/* Add undefined hooks */
for (guint i = 0; i < self->undefined_hooks->len; i++) {
WpEventHook *uh = g_ptr_array_index (self->undefined_hooks, i);
g_ptr_array_add (new_hooks, g_object_ref (uh));
}
/* Add current hook */
g_ptr_array_add (new_hooks, g_object_ref (hook));
g_hash_table_insert (self->defined_hooks, g_strdup (event_type),
new_hooks);
if (!sort_hooks (new_hooks))
goto sort_error;
}
is_defined = TRUE;
}
}
/* Otherwise just register it as undefined hook */
if (!is_defined) {
GHashTableIter iter;
gpointer value;
wp_debug_object (self, "Registering hook %s for undefined event types",
hook_name);
/* Add it to the defined hooks table */
g_hash_table_iter_init (&iter, self->defined_hooks);
while (g_hash_table_iter_next (&iter, NULL, &value)) {
GPtrArray *defined_hooks = value;
g_ptr_array_add (defined_hooks, g_object_ref (hook));
if (!sort_hooks (defined_hooks))
goto sort_error;
}
/* Add it to the undefined hooks */
g_ptr_array_add (self->undefined_hooks, g_object_ref (hook));
if (!sort_hooks (self->undefined_hooks))
goto sort_error;
}
wp_info_object (self, "Registered hook %s successfully", hook_name);
return;
sort_error:
/* Unregister hook */
wp_event_dispatcher_unregister_hook (self, hook);
wp_warning_object (self,
"Could not register hook %s because of circular dependencies", hook_name);
}
/*!
* \brief Unregisters an event hook
* \ingroup wpeventdispacher
* \ingroup wpeventdispatcher
*
* \param self the event dispatcher
* \param hook (transfer none): the hook to unregister
@ -306,6 +538,9 @@ void
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpEventHook * hook)
{
GHashTableIter iter;
gpointer value;
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
@ -314,11 +549,29 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == self);
wp_event_hook_set_dispatcher (hook, NULL);
g_ptr_array_remove_fast (self->hooks, hook);
/* Remove hook from defined table and undefined list */
g_hash_table_iter_init (&iter, self->defined_hooks);
while (g_hash_table_iter_next (&iter, NULL, &value)) {
GPtrArray *defined_hooks = value;
g_ptr_array_remove (defined_hooks, hook);
}
g_ptr_array_remove (self->undefined_hooks, hook);
}
static void
add_unique (GPtrArray *array, WpEventHook * hook)
{
for (guint i = 0; i < array->len; i++)
if (g_ptr_array_index (array, i) == hook)
return;
g_ptr_array_add (array, g_object_ref (hook));
}
/*!
* \brief Returns an iterator to iterate over all the registered hooks
* \deprecated Use \ref wp_event_dispatcher_new_hooks_for_event_type_iterator
* instead.
* \ingroup wpeventdispatcher
*
* \param self the event dispatcher
@ -327,7 +580,56 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpIterator *
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
{
GPtrArray *items =
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
GHashTableIter iter;
gpointer value;
/* Add all defined hooks */
g_hash_table_iter_init (&iter, self->defined_hooks);
while (g_hash_table_iter_next (&iter, NULL, &value)) {
GPtrArray *hooks = value;
for (guint i = 0; i < hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (hooks, i);
add_unique (items, hook);
}
}
/* Add all undefined hooks */
for (guint i = 0; i < self->undefined_hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (self->undefined_hooks, i);
add_unique (items, hook);
}
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
}
/*!
* \brief Returns an iterator to iterate over the registered hooks for a
* particular event type.
* \ingroup wpeventdispatcher
*
* \param self the event dispatcher
* \param event_type the event type
* \return (transfer full): a new iterator
* \since 0.5.13
*/
WpIterator *
wp_event_dispatcher_new_hooks_for_event_type_iterator (
WpEventDispatcher * self, const gchar *event_type)
{
GPtrArray *items;
GPtrArray *hooks;
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
if (hooks) {
wp_debug_object (self, "Using %d defined hooks for event type %s",
hooks->len, event_type);
} else {
hooks = self->undefined_hooks;
wp_debug_object (self, "Using %d undefined hooks for event type %s",
hooks->len, event_type);
}
items = g_ptr_array_copy (hooks, (GCopyFunc) g_object_ref, NULL);
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
}

View file

@ -41,7 +41,12 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpEventHook * hook);
WP_API
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
G_GNUC_DEPRECATED_FOR (wp_event_dispatcher_new_hooks_for_event_type_iterator);
WP_API
WpIterator * wp_event_dispatcher_new_hooks_for_event_type_iterator (
WpEventDispatcher * self, const gchar *event_type);
G_END_DECLS

View file

@ -184,6 +184,7 @@ wp_event_hook_get_runs_after_hooks (WpEventHook * self)
/*!
* \brief Returns the associated event dispatcher
*
* \private
* \ingroup wpeventhook
* \param self the event hook
* \return (transfer full)(nullable): the event dispatcher on which this hook
@ -197,6 +198,15 @@ wp_event_hook_get_dispatcher (WpEventHook * self)
return g_weak_ref_get (&priv->dispatcher);
}
/*!
* \brief Sets the associated event dispatcher
*
* \private
* \ingroup wpeventhook
* \param self the event hook
* \param dispatcher (transfer none): the event dispatcher on which
* this hook is registered
*/
void
wp_event_hook_set_dispatcher (WpEventHook * self, WpEventDispatcher * dispatcher)
{
@ -229,9 +239,9 @@ wp_event_hook_runs_for_event (WpEventHook * self, WpEvent * event)
* \param self the event hook
* \param event the event that triggered the hook
* \param cancellable (nullable): a GCancellable to cancel the async operation
* \param callback (scope async): a callback to fire after execution of the hook
* \param callback (scope async)(closure callback_data): a callback to fire after execution of the hook
* has completed
* \param callback_data (closure): data for the callback
* \param callback_data data for the callback
*/
void
wp_event_hook_run (WpEventHook * self,
@ -244,6 +254,24 @@ wp_event_hook_run (WpEventHook * self,
callback_data);
}
/*!
* \brief Gets all the matching event types for this hook if any.
*
* \ingroup wpeventhook
* \param self the event hook
* \returns (element-type gchar*) (transfer full) (nullable): the matching
* event types for this hook if any.
* \since 0.5.13
*/
GPtrArray *
wp_event_hook_get_matching_event_types (WpEventHook * self)
{
g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
g_return_val_if_fail (
WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
}
/*!
* \brief Finishes the async operation that was started by wp_event_hook_run()
*
@ -311,36 +339,63 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
wp_interest_event_hook_get_instance_private (self);
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
g_autoptr (GObject) subject = wp_event_get_subject (event);
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
guint i;
WpObjectInterest *interest = NULL;
WpInterestMatch match;
const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
WP_INTEREST_MATCH_PW_PROPERTIES |
WP_INTEREST_MATCH_G_PROPERTIES);
for (i = 0; i < priv->interests->len; i++) {
interest = g_ptr_array_index (priv->interests, i);
match = wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
gtype, subject, properties, properties);
/* the interest may have a GType that matches the GType of the subject
or it may have WP_TYPE_EVENT as its GType, in which case it will
match any type of subject */
if (match == WP_INTEREST_MATCH_ALL)
return TRUE;
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
match = wp_object_interest_matches_full (interest, 0,
WP_TYPE_EVENT, NULL, NULL, NULL);
if (match & WP_INTEREST_MATCH_GTYPE)
if (wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_NONE,
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
return TRUE;
}
}
return FALSE;
}
static void
add_unique (GPtrArray *array, const gchar * lookup)
{
for (guint i = 0; i < array->len; i++)
if (g_str_equal (g_ptr_array_index (array, i), lookup))
return;
g_ptr_array_add (array, g_strdup (lookup));
}
static GPtrArray *
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
{
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
WpInterestEventHookPrivate *priv =
wp_interest_event_hook_get_instance_private (self);
g_autoptr (GPtrArray) res = g_ptr_array_new_with_free_func (g_free);
guint i;
for (i = 0; i < priv->interests->len; i++) {
WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
g_autoptr (GPtrArray) values =
wp_object_interest_find_defined_constraint_values (interest,
WP_CONSTRAINT_TYPE_NONE, "event.type");
if (!values || values->len == 0) {
/* We always consider the hook undefined if it has at least one interest
* without a defined 'event.type' constraint */
return NULL;
} else {
for (guint j = 0; j < values->len; j++) {
GVariant *v = g_ptr_array_index (values, j);
if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
const gchar *v_str = g_variant_get_string (v, NULL);
add_unique (res, v_str);
}
}
}
}
}
return g_steal_pointer (&res);
}
static void
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
{
@ -349,6 +404,8 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
object_class->finalize = wp_interest_event_hook_finalize;
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
hook_class->get_matching_event_types =
wp_interest_event_hook_get_matching_event_types;
}
/*!

View file

@ -39,8 +39,10 @@ struct _WpEventHookClass
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
/*< private >*/
WP_PADDING(5)
WP_PADDING(4)
};
WP_API
@ -67,6 +69,9 @@ void wp_event_hook_run (WpEventHook * self,
WpEvent * event, GCancellable * cancellable,
GAsyncReadyCallback callback, gpointer callback_data);
WP_API
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
WP_API
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
GError ** error);

View file

@ -17,37 +17,11 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
typedef struct _HookData HookData;
struct _HookData
{
struct spa_list link;
WpEventHook *hook;
GPtrArray *dependencies;
};
static inline HookData *
hook_data_new (WpEventHook * hook)
{
HookData *hook_data = g_new0 (HookData, 1);
spa_list_init (&hook_data->link);
hook_data->hook = g_object_ref (hook);
hook_data->dependencies = g_ptr_array_new ();
return hook_data;
}
static void
hook_data_free (HookData *self)
{
g_clear_object (&self->hook);
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
g_free (self);
}
struct _WpEvent
{
grefcount ref;
GData *datalist;
struct spa_list hooks;
GPtrArray *hooks;
/* immutable fields */
gint priority;
@ -96,7 +70,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
WpEvent * self = g_new0 (WpEvent, 1);
g_ref_count_init (&self->ref);
g_datalist_init (&self->datalist);
spa_list_init (&self->hooks);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->priority = priority;
self->properties = properties ?
@ -155,11 +129,7 @@ wp_event_get_name(WpEvent *self)
static void
wp_event_free (WpEvent * self)
{
HookData *hook_data;
spa_list_consume (hook_data, &self->hooks, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
g_clear_pointer (&self->hooks, g_ptr_array_unref);
g_datalist_clear (&self->datalist);
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_object (&self->source);
@ -316,33 +286,6 @@ wp_event_get_data (WpEvent * self, const gchar * key)
return g_datalist_get_data (&self->datalist, key);
}
static inline void
record_dependency (struct spa_list *list, const gchar *target,
const gchar *dependency)
{
HookData *hook_data;
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
break;
}
}
}
static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
return TRUE;
}
}
}
return FALSE;
}
/*!
* \brief Collects all the hooks registered in the \a dispatcher that run for
* this \a event
@ -355,188 +298,37 @@ hook_exists_in (const gchar *hook_name, struct spa_list *list)
gboolean
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
{
struct spa_list collected, result, remaining;
g_autoptr (WpIterator) all_hooks = NULL;
g_auto (GValue) value = G_VALUE_INIT;
const gchar *event_type = NULL;
g_return_val_if_fail (event != NULL, FALSE);
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
/* hooks already collected */
if (!spa_list_is_empty (&event->hooks))
return TRUE;
/* Clear all current hooks */
g_ptr_array_set_size (event->hooks, 0);
spa_list_init (&collected);
spa_list_init (&result);
spa_list_init (&remaining);
/* Get the event type */
event_type = wp_properties_get (event->properties, "event.type");
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
event->name, event_type);
/* collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
/* Collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
event_type);
while (wp_iterator_next (all_hooks, &value)) {
WpEventHook *hook = g_value_get_object (&value);
if (wp_event_hook_runs_for_event (hook, event)) {
HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
wp_trace_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
g_ptr_array_add (event->hooks, g_object_ref (hook));
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
}
g_value_unset (&value);
}
if (!spa_list_is_empty (&collected)) {
HookData *hook_data;
/* convert "before" dependencies into "after" dependencies */
spa_list_for_each (hook_data, &collected, link) {
const gchar * const * strv =
wp_event_hook_get_runs_before_hooks (hook_data->hook);
while (strv && *strv) {
/* record hook_data->hook as a dependency of the *strv hook */
record_dependency (&collected, *strv,
wp_event_hook_get_name (hook_data->hook));
strv++;
}
}
/* sort */
while (!spa_list_is_empty (&collected)) {
gboolean made_progress = FALSE;
/* examine each hook to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
spa_list_consume (hook_data, &collected, link) {
guint deps_satisfied = 0;
spa_list_remove (&hook_data->link);
for (guint i = 0; i < hook_data->dependencies->len; i++) {
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
/* if the dependency is already in the sorted result list or if
it doesn't exist at all, we consider it satisfied */
if (hook_exists_in (dep, &result) ||
!(hook_exists_in (dep, &collected) ||
hook_exists_in (dep, &remaining))) {
deps_satisfied++;
}
}
if (deps_satisfied == hook_data->dependencies->len) {
spa_list_append (&result, &hook_data->link);
made_progress = TRUE;
} else {
spa_list_append (&remaining, &hook_data->link);
}
}
if (made_progress) {
/* run again with the remaining hooks */
spa_list_insert_list (&collected, &remaining);
spa_list_init (&remaining);
}
else if (!spa_list_is_empty (&remaining)) {
/* if we did not make any progress towards growing the result list,
it means the dependencies cannot be satisfied because of circles */
wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
"dependencies in the collected hooks!");
/* clean up */
spa_list_consume (hook_data, &result, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
spa_list_consume (hook_data, &remaining, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
return FALSE;
}
}
}
spa_list_insert_list (&event->hooks, &result);
return !spa_list_is_empty (&event->hooks);
return event->hooks->len > 0;
}
struct event_hooks_iterator_data
{
WpEvent *event;
HookData *cur;
};
static void
event_hooks_iterator_reset (WpIterator *it)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
struct spa_list *list = &it_data->event->hooks;
if (!spa_list_is_empty (list))
it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
}
static gboolean
event_hooks_iterator_next (WpIterator *it, GValue *item)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
struct spa_list *list = &it_data->event->hooks;
if (!spa_list_is_empty (list) &&
!spa_list_is_end (it_data->cur, list, link)) {
g_value_init (item, WP_TYPE_EVENT_HOOK);
g_value_set_object (item, it_data->cur->hook);
it_data->cur = spa_list_next (it_data->cur, link);
return TRUE;
}
return FALSE;
}
static gboolean
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
gpointer data)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
struct spa_list *list = &it_data->event->hooks;
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
g_auto (GValue) item = G_VALUE_INIT;
g_value_init (&item, WP_TYPE_EVENT_HOOK);
g_value_set_object (&item, hook_data->hook);
if (!func (&item, ret, data))
return FALSE;
}
}
return TRUE;
}
static void
event_hooks_iterator_finalize (WpIterator *it)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
wp_event_unref (it_data->event);
}
static const WpIteratorMethods event_hooks_iterator_methods = {
.version = WP_ITERATOR_METHODS_VERSION,
.reset = event_hooks_iterator_reset,
.next = event_hooks_iterator_next,
.fold = event_hooks_iterator_fold,
.finalize = event_hooks_iterator_finalize,
};
/*!
* \brief Returns an iterator that iterates over all the hooks that were
* collected by wp_event_collect_hooks()
@ -547,15 +339,8 @@ static const WpIteratorMethods event_hooks_iterator_methods = {
WpIterator *
wp_event_new_hooks_iterator (WpEvent * event)
{
WpIterator *it = NULL;
struct event_hooks_iterator_data *it_data;
GPtrArray *hooks;
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL);
return wp_iterator_new_ptr_array (hooks, WP_TYPE_EVENT_HOOK);
g_return_val_if_fail (event != NULL, NULL);
it = wp_iterator_new (&event_hooks_iterator_methods,
sizeof (struct event_hooks_iterator_data));
it_data = wp_iterator_get_user_data (it);
it_data->event = wp_event_ref (event);
event_hooks_iterator_reset (it);
return it;
}

View file

@ -250,6 +250,20 @@ wp_global_proxy_destroyed (WpProxy * proxy)
WpGlobalProxyPrivate *priv =
wp_global_proxy_get_instance_private (self);
if (priv->global && priv->global->proxy &&
(priv->global->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY)) {
/* We can end up here as a result of _request_destroy() followed by
* _deactivate(FEATURE_BOUND), where the latter triggers this callback
* before _remove_global is processed.
* If proxy is owned, it is gone now, so not much owned left.
* self might be cleaned up soon, so this is a good time to remove
* the non-refcounted backreference in global. If not done now, _dispose()
* does not have a chance to cleanup (as the reference to global is gone).
* If remove_global then comes in later, there is no more real work to
* do when WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY is removed
*/
wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
}
g_clear_pointer (&priv->global, wp_global_unref);
}

View file

@ -178,9 +178,9 @@ wp_iterator_next (WpIterator *self, GValue *item)
*
* \ingroup wpiterator
* \param self the iterator
* \param func (scope call): the fold function
* \param func (scope call)(closure data): the fold function
* \param ret (inout): the accumulator data
* \param data (closure): the user data
* \param data the user data
* \returns TRUE if all the items were processed, FALSE otherwise.
*/
gboolean
@ -200,8 +200,8 @@ wp_iterator_fold (WpIterator *self, WpIteratorFoldFunc func, GValue *ret,
*
* \ingroup wpiterator
* \param self the iterator
* \param func (scope call): the foreach function
* \param data (closure): the user data
* \param func (scope call)(closure data): the foreach function
* \param data the user data
* \returns TRUE if all the items were processed, FALSE otherwise.
*/
gboolean

View file

@ -34,7 +34,7 @@ match_rules_cb (void *data, const char *location, const char *action,
}
/*!
* \brief Matches the given properties against a set of rules descriped in JSON
* \brief Matches the given properties against a set of rules described in JSON
* and calls the given callback to perform actions on a successful match.
*
* The given JSON should be an array of objects, where each object has a
@ -53,7 +53,7 @@ match_rules_cb (void *data, const char *location, const char *action,
* {
* matches = [
* # any of the items in matches needs to match, if one does,
* # actions are emited.
* # actions are emitted.
* {
* # all keys must match the value. ! negates. ~ starts regex.
* <key> = <value>
@ -72,8 +72,8 @@ match_rules_cb (void *data, const char *location, const char *action,
* \ingroup wpjsonutils
* \param json a JSON array containing rules in the described format
* \param match_props (transfer none): the properties to match against the rules
* \param callback (scope call): a function to call for each action on a successful match
* \param data (closure callback): data to be passed to \a callback
* \param callback (scope call)(closure data): a function to call for each action on a successful match
* \param data data to be passed to \a callback
* \param error (out)(optional): the error that occurred, if any
* \returns FALSE if an error occurred, TRUE otherwise
*/
@ -117,7 +117,7 @@ update_props_cb (gpointer data, const gchar * action, WpSpaJson * value,
}
/*!
* \brief Matches the given properties against a set of rules descriped in JSON
* \brief Matches the given properties against a set of rules described in JSON
* and updates the properties if the rule actions include the "update-props"
* action.
*
@ -211,12 +211,12 @@ merge_json_objects (WpSpaJson *a, WpSpaJson *b)
g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
val = g_value_dup_boxed (&item);
if (!override &&
if (!override && wp_spa_json_is_container (val) &&
(wp_spa_json_object_get (a, key_str, "J", &j, NULL) ||
wp_spa_json_object_get (a, override_key_str, "J", &j, NULL))) {
g_autoptr (WpSpaJson) merged = wp_json_utils_merge_containers (j, val);
if (!merged) {
wp_warning ("skipping merge of %s as JSON values are not compatible",
wp_warning ("skipping merge of %s as JSON values are not compatible containers",
key_str);
continue;
}

View file

@ -37,8 +37,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-link")
* \code
* void
* state_changed_callback (WpLink * self,
* WpLinkState * old_state,
* WpLinkState * new_state,
* WpLinkState old_state,
* WpLinkState new_state,
* gpointer user_data)
* \endcode
*

View file

@ -18,8 +18,39 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-log")
* \{
*/
/*!
* \def WP_LOG_LEVEL_TRACE
* \brief A custom GLib log level for trace messages (see GLogLevelFlags)
* \def WP_DEFINE_LOCAL_LOG_TOPIC(name)
* \brief Defines a static \em WpLogTopic* variable called \em WP_LOCAL_LOG_TOPIC
*
* The log topic is automatically initialized to the given topic \a name when
* it is first used. The default logging macros expect this variable to be
* defined, so it is a good coding practice in the WirePlumber codebase to
* start all files at the top with:
* \code
* WP_DEFINE_LOCAL_LOG_TOPIC ("some-topic")
* \endcode
*
* \param name The name of the log topic
*/
/*!
* \def WP_LOG_TOPIC_STATIC(var, name)
* \brief Defines a static \em WpLogTopic* variable called \a var with the given
* topic \a name
* \param var The name of the variable to define
* \param name The name of the log topic
*/
/*!
* \def WP_LOG_TOPIC(var, name)
* \brief Defines a \em WpLogTopic* variable called \a var with the given
* topic \a name. Unlike WP_LOG_TOPIC_STATIC(), the variable defined here is
* not static, so it can be linked to by other object files.
* \param var The name of the variable to define
* \param name The name of the log topic
*/
/*!
* \def WP_LOG_TOPIC_EXTERN(var)
* \brief Declares an extern \em WpLogTopic* variable called \a var.
* This variable is meant to be defined in a .c file with WP_LOG_TOPIC()
* \param var The name of the variable to declare
*/
/*!
* \def WP_OBJECT_FORMAT
@ -176,24 +207,33 @@ static GString *spa_dbg_str = NULL;
#include <spa/debug/pod.h>
#define DEFAULT_LOG_LEVEL 4 /* MESSAGE */
#define DEFAULT_LOG_LEVEL_FLAGS (G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR)
struct log_topic_pattern
{
GPatternSpec *spec;
gchar *spec_str;
gint log_level;
};
static struct {
gboolean use_color;
gboolean output_is_journal;
gboolean set_pw_log;
gint global_log_level;
GLogLevelFlags global_log_level_flags;
struct log_topic_pattern *patterns;
GPtrArray *log_topics;
GMutex log_topics_lock;
} log_state = {
.use_color = FALSE,
.output_is_journal = FALSE,
.global_log_level = 4 /* MESSAGE */,
.global_log_level_flags = G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR,
.set_pw_log = FALSE,
.global_log_level = DEFAULT_LOG_LEVEL,
.global_log_level_flags = DEFAULT_LOG_LEVEL_FLAGS,
.patterns = NULL,
.log_topics = NULL,
};
/* reference: https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit */
@ -260,6 +300,8 @@ level_index_from_flags (GLogLevelFlags log_level)
static G_GNUC_CONST inline GLogLevelFlags
level_index_to_flag (gint lvl_index)
{
if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
return 0;
return log_level_info [lvl_index].log_level_flags;
}
@ -300,6 +342,8 @@ level_index_from_spa (gint spa_lvl, gboolean warn_to_notice)
static G_GNUC_CONST inline gint
level_index_to_spa (gint lvl_index)
{
if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
return 0;
return log_level_info [lvl_index].spa_level;
}
@ -325,106 +369,6 @@ level_index_from_string (const char *str, gint *lvl)
return FALSE;
}
/* private, called from wp_init() */
void
wp_log_init (gint flags)
{
const gchar *level_str;
gint global_log_level = log_state.global_log_level;
struct log_topic_pattern *patterns = NULL, *pttrn;
gint n_tokens = 0;
gchar **tokens = NULL;
level_str = g_getenv ("WIREPLUMBER_DEBUG");
log_state.use_color = g_log_writer_supports_color (fileno (stderr));
log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
if (level_str && level_str[0] != '\0') {
/* [<glob>:]<level>,..., */
tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
}
/* allocate enough space to hold all pattern specs */
patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
pttrn = patterns;
if (!patterns)
g_error ("unable to allocate space for %d log patterns", n_tokens + 2);
for (gint i = 0; i < n_tokens; i++) {
gint n_tok;
gchar **tok;
gint lvl;
tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
pttrn->spec = g_pattern_spec_new (tok[0]);
pttrn->log_level = lvl;
pttrn++;
} else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
global_log_level = lvl;
} else {
/* note that this is going to initialize the wp-log topic here */
wp_warning ("Ignoring invalid format in WIREPLUMBER_DEBUG: '%s'",
tokens[i]);
}
pw_free_strv (tok);
}
/* disable pipewire connection trace by default */
pttrn->spec = g_pattern_spec_new ("conn.*");
pttrn->log_level = 0;
pttrn++;
/* terminate with NULL */
pttrn->spec = NULL;
pttrn->log_level = 0;
pw_free_strv (tokens);
log_state.patterns = patterns;
log_state.global_log_level = global_log_level;
log_state.global_log_level_flags =
level_index_to_full_flags (global_log_level);
/* set the log level also on the spa_log */
wp_spa_log_get_instance()->level = level_index_to_spa (global_log_level);
if (flags & WP_INIT_SET_GLIB_LOG)
g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
/* set PIPEWIRE_DEBUG and the spa_log interface that pipewire will use */
if (flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG")) {
/* always set PIPEWIRE_DEBUG for 2 reasons:
* 1. to overwrite it from the environment, in case the user has set it
* 2. to prevent pw_context from parsing "log.level" from the config file;
* we do this ourselves here and allows us to have more control over
* the whole process.
*/
gchar lvl_str[2];
g_snprintf (lvl_str, 2, "%d", wp_spa_log_get_instance ()->level);
g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
pw_log_set_level (wp_spa_log_get_instance ()->level);
pw_log_set (wp_spa_log_get_instance ());
}
}
gboolean
wp_log_set_global_level (const gchar *log_level)
{
gint level;
if (level_index_from_string (log_level, &level)) {
log_state.global_log_level = level;
log_state.global_log_level_flags = level_index_to_full_flags (level);
wp_spa_log_get_instance()->level = level_index_to_spa (level);
pw_log_set_level (level_index_to_spa (level));
return TRUE;
} else {
return FALSE;
}
}
static gint
find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
{
@ -452,6 +396,255 @@ find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
return log_level;
}
static void
log_topic_update_level (WpLogTopic *topic)
{
gint log_level = find_topic_log_level (topic->topic_name, NULL);
gint flags = topic->flags & ~WP_LOG_TOPIC_LEVEL_MASK;
flags |= level_index_to_full_flags (log_level);
topic->flags = flags;
}
static void
update_log_topic_levels (void)
{
guint i;
g_mutex_lock (&log_state.log_topics_lock);
if (log_state.log_topics)
for (i = 0; i < log_state.log_topics->len; ++i)
log_topic_update_level (g_ptr_array_index (log_state.log_topics, i));
g_mutex_unlock (&log_state.log_topics_lock);
}
static void
free_patterns (struct log_topic_pattern *patterns)
{
struct log_topic_pattern *p = patterns;
while (p && p->spec) {
g_clear_pointer (&p->spec, g_pattern_spec_free);
g_clear_pointer (&p->spec_str, g_free);
++p;
}
g_free (patterns);
}
/* Parse value to log level and patterns. If no global level in string,
global_log_level is not modified. */
static gboolean
parse_log_level (const gchar *level_str, struct log_topic_pattern **global_patterns, gint *global_log_level)
{
struct log_topic_pattern *patterns = NULL, *pttrn;
gint n_tokens = 0;
gchar **tokens = NULL;
int level = *global_log_level;
*global_patterns = NULL;
if (level_str && level_str[0] != '\0') {
/* [<glob>:]<level>,..., */
tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
}
/* allocate enough space to hold all pattern specs */
patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
pttrn = patterns;
if (!patterns)
g_error ("unable to allocate space for %d log patterns", n_tokens + 2);
for (gint i = 0; i < n_tokens; i++) {
gint n_tok;
gchar **tok;
gint lvl;
tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
pttrn->spec = g_pattern_spec_new (tok[0]);
pttrn->spec_str = g_strdup (tok[0]);
pttrn->log_level = lvl;
pttrn++;
} else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
level = lvl;
} else {
pttrn->spec = NULL;
pw_free_strv (tok);
free_patterns (patterns);
return FALSE;
}
pw_free_strv (tok);
}
/* disable pipewire connection trace by default */
pttrn->spec = g_pattern_spec_new ("conn.*");
pttrn->spec_str = g_strdup ("conn.*");
pttrn->log_level = 0;
pttrn++;
/* terminate with NULL */
pttrn->spec = NULL;
pttrn->spec_str = NULL;
pttrn->log_level = 0;
pw_free_strv (tokens);
*global_patterns = patterns;
*global_log_level = level;
return TRUE;
}
static gchar *
format_pw_log_level_string (gint level, const struct log_topic_pattern *patterns)
{
GString *str = g_string_new (NULL);
const struct log_topic_pattern *p;
g_string_printf (str, "%d", level_index_to_spa (level));
for (p = patterns; p && p->spec; ++p)
g_string_append_printf (str, ",%s:%d", p->spec_str, level_index_to_spa (p->log_level));
return g_string_free (str, FALSE);
}
gboolean
wp_log_set_level (const gchar *level_str)
{
gint level;
GLogLevelFlags flags;
struct log_topic_pattern *patterns;
level = DEFAULT_LOG_LEVEL;
if (!parse_log_level (level_str, &patterns, &level))
return FALSE;
flags = level_index_to_full_flags (level);
g_mutex_lock (&log_state.log_topics_lock);
log_state.global_log_level = level;
log_state.global_log_level_flags = flags;
SPA_SWAP (log_state.patterns, patterns);
g_mutex_unlock (&log_state.log_topics_lock);
free_patterns (patterns);
update_log_topic_levels ();
wp_spa_log_get_instance()->level = level_index_to_spa (level);
if (log_state.set_pw_log) {
#if PW_CHECK_VERSION(1,1,0)
g_autofree gchar *pw_pattern = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
pw_log_set_level_string (pw_pattern);
#else
pw_log_set_level (level_index_to_spa (level));
#endif
}
return TRUE;
}
/*!
* \brief private, called from wp_init()
* \ingroup wplog
* \private
*/
void
wp_log_init (gint flags)
{
log_state.use_color = g_log_writer_supports_color (fileno (stderr));
log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
log_state.set_pw_log = flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG");
if (flags & WP_INIT_SET_GLIB_LOG)
g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
/* set the spa_log interface that pipewire will use */
if (log_state.set_pw_log)
pw_log_set (wp_spa_log_get_instance ());
if (!wp_log_set_level (g_getenv ("WIREPLUMBER_DEBUG"))) {
wp_warning ("Ignoring invalid value in WIREPLUMBER_DEBUG");
wp_log_set_level (NULL);
}
if (log_state.set_pw_log) {
/* always set PIPEWIRE_DEBUG for 2 reasons:
* 1. to overwrite it from the environment, in case the user has set it
* 2. to prevent pw_context from parsing "log.level" from the config file;
* we do this ourselves here and allows us to have more control over
* the whole process.
*/
g_autofree gchar *lvl_str = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
}
}
static void
log_topic_register (WpLogTopic *topic)
{
if (!log_state.log_topics)
log_state.log_topics = g_ptr_array_new ();
g_ptr_array_add (log_state.log_topics, topic);
log_topic_update_level (topic);
topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
}
static void
log_topic_unregister (WpLogTopic *topic)
{
if (!log_state.log_topics)
return;
g_ptr_array_remove_fast (log_state.log_topics, topic);
if (log_state.log_topics->len == 0) {
g_ptr_array_free (log_state.log_topics, TRUE);
log_state.log_topics = NULL;
}
}
/*!
* \brief Registers a log topic.
*
* The log topic must be unregistered using \ref wp_log_topic_unregister
* before its lifetime ends.
*
* This function is threadsafe.
*
* \ingroup wplog
*/
void
wp_log_topic_register (WpLogTopic *topic)
{
g_mutex_lock (&log_state.log_topics_lock);
log_topic_register (topic);
g_mutex_unlock (&log_state.log_topics_lock);
}
/*!
* \brief Unregisters a log topic.
*
* This function is threadsafe.
*
* \ingroup wplog
*/
void
wp_log_topic_unregister (WpLogTopic *topic)
{
g_mutex_lock (&log_state.log_topics_lock);
log_topic_unregister (topic);
g_mutex_unlock (&log_state.log_topics_lock);
}
/*!
* \brief Initializes a log topic. Internal function, don't use it directly
* \ingroup wplog
@ -459,21 +652,17 @@ find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
void
wp_log_topic_init (WpLogTopic *topic)
{
g_bit_lock (&topic->flags, 30);
if ((topic->flags & (1u << 31)) == 0) {
bool has_custom_level;
gint log_level = find_topic_log_level (topic->topic_name, &has_custom_level);
gint flags = topic->flags;
flags |= level_index_to_full_flags (log_level);
flags |= (1u << 31); /* initialized = true */
if (has_custom_level)
flags |= (1u << 29); /* has_custom_level = true */
topic->global_flags = &log_state.global_log_level_flags;
topic->flags = flags;
g_mutex_lock (&log_state.log_topics_lock);
if ((topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) == 0) {
if (topic->flags & WP_LOG_TOPIC_FLAG_STATIC) {
/* Auto-register log topics that have infinite lifetime */
log_topic_register (topic);
} else {
log_topic_update_level (topic);
topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
}
}
g_bit_unlock (&topic->flags, 30);
g_mutex_unlock (&log_state.log_topics_lock);
}
typedef struct _WpLogFields WpLogFields;
@ -485,6 +674,7 @@ struct _WpLogFields
const gchar *func;
const gchar *message;
gint log_level;
gboolean debug;
GType object_type;
gconstpointer object;
};
@ -493,6 +683,7 @@ static void
wp_log_fields_init (WpLogFields *lf,
const gchar *log_topic,
gint log_level,
gboolean debug,
const gchar *file,
const gchar *line,
const gchar *func,
@ -502,6 +693,7 @@ wp_log_fields_init (WpLogFields *lf,
{
lf->log_topic = log_topic ? log_topic : "default";
lf->log_level = log_level;
lf->debug = debug;
lf->file = file;
lf->line = line;
lf->func = func;
@ -510,11 +702,18 @@ wp_log_fields_init (WpLogFields *lf,
lf->message = message ? message : "(null)";
}
static gboolean
wp_want_debug_log (const struct spa_log_topic *topic)
{
return spa_log_level_topic_enabled (wp_spa_log_get_instance(), topic, SPA_LOG_LEVEL_DEBUG);
}
static void
wp_log_fields_init_from_glib (WpLogFields *lf, GLogLevelFlags log_level_flags,
const GLogField *fields, gsize n_fields)
{
wp_log_fields_init (lf, NULL, level_index_from_flags (log_level_flags),
wp_want_debug_log (NULL),
NULL, NULL, NULL, 0, NULL, NULL);
for (guint i = 0; i < n_fields; i++) {
@ -573,15 +772,52 @@ wp_log_fields_write_to_stream (WpLogFields *lf, FILE *s)
static gboolean
wp_log_fields_write_to_journal (WpLogFields *lf)
{
gsize n_fields = 6;
GLogField fields[6] = {
{ "PRIORITY", log_level_info[lf->log_level].priority, -1 },
{ "CODE_FILE", lf->file ? lf->file : "", -1 },
{ "CODE_LINE", lf->line ? lf->line : "", -1 },
{ "CODE_FUNC", lf->func ? lf->func : "", -1 },
{ "TOPIC", lf->log_topic ? lf->log_topic : "", -1 },
{ "MESSAGE", lf->message ? lf->message : "", -1 },
};
GLogField fields[10];
gsize n_fields = 0;
g_autofree gchar *full_message = NULL;
const gchar *message = lf->message ? lf->message : "";
g_autofree gchar *pid = g_strdup_printf("%d", getpid());
g_autofree gchar *tid = g_strdup_printf("%d", gettid());
#ifdef HAS_SHORT_NAME
const gchar *syslog_identifier = program_invocation_short_name;
#else
const gchar *syslog_identifier = g_get_prgname();
#endif
if (lf->debug) {
if (lf->file && lf->line && lf->func) {
g_autofree gchar *file = g_path_get_basename(lf->file);
message = full_message = g_strdup_printf("%c %s%s[%s:%s:%s]: %s",
log_level_info[lf->log_level].name,
lf->log_topic ? lf->log_topic : "",
lf->log_topic ? " " : "",
file, lf->line, lf->func, message);
} else {
message = full_message = g_strdup_printf("%c %s%s%s",
log_level_info[lf->log_level].name,
lf->log_topic ? lf->log_topic : "",
lf->log_topic ? ": " : "",
message);
}
} else if (lf->log_topic) {
message = full_message = g_strdup_printf("%s: %s", lf->log_topic, message);
}
fields[n_fields++] = (GLogField) { "SYSLOG_PID", pid, -1 };
fields[n_fields++] = (GLogField) { "TID", tid, -1 };
fields[n_fields++] = (GLogField) { "SYSLOG_IDENTIFIER", syslog_identifier, -1 };
fields[n_fields++] = (GLogField) { "SYSLOG_FACILITY", "3", -1 };
fields[n_fields++] = (GLogField) { "PRIORITY", log_level_info[lf->log_level].priority, -1 };
if (lf->file)
fields[n_fields++] = (GLogField) { "CODE_FILE", lf->file, -1 };
if (lf->line)
fields[n_fields++] = (GLogField) { "CODE_LINE", lf->line, -1 };
if (lf->func)
fields[n_fields++] = (GLogField) { "CODE_FUNC", lf->func, -1 };
if (lf->log_topic)
fields[n_fields++] = (GLogField) { "TOPIC", lf->log_topic, -1 };
fields[n_fields++] = (GLogField) { "MESSAGE", message, -1 };
/* the log level flags are not used in this function, so we can pass 0 */
return (g_log_writer_journald (0, fields, n_fields, NULL) == G_LOG_WRITER_HANDLED);
@ -671,6 +907,8 @@ wp_log_writer_default (GLogLevelFlags log_level_flags,
/*!
* \brief Used internally by the debug logging macros. Avoid using it directly.
*
* \deprecated Use \ref wp_logt_checked instead.
*
* This assumes that the arguments are correct and that the log_topic is
* enabled for the given log_level. No additional checks are performed.
* \ingroup wplog
@ -696,6 +934,46 @@ wp_log_checked (
va_end (args);
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags),
wp_want_debug_log (NULL),
file, line, func, object_type, object, message);
wp_log_fields_log (&lf);
}
/*!
* \brief Used internally by the debug logging macros. Avoid using it directly.
*
* This assumes that the arguments are correct and that the log_topic is
* enabled for the given log_level. No additional checks are performed.
* \ingroup wplog
*/
void
wp_logt_checked (
const WpLogTopic *topic,
GLogLevelFlags log_level_flags,
const gchar *file,
const gchar *line,
const gchar *func,
GType object_type,
gconstpointer object,
const gchar *message_format,
...)
{
WpLogFields lf = {0};
g_autofree gchar *message = NULL;
va_list args;
const gchar *log_topic = topic ? topic->topic_name : NULL;
gboolean debug;
if (topic)
debug = (topic->flags & WP_LOG_TOPIC_LEVEL_MASK & G_LOG_LEVEL_DEBUG);
else
debug = wp_want_debug_log (NULL);
va_start (args, message_format);
message = g_strdup_vprintf (message_format, args);
va_end (args);
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags), debug,
file, line, func, object_type, object, message);
wp_log_fields_log (&lf);
}
@ -719,6 +997,7 @@ wp_spa_log_logtv (void *object,
message = g_strdup_vprintf (fmt, args);
wp_log_fields_init (&lf, topic ? topic->topic : NULL, log_level,
wp_want_debug_log (topic),
file, line_str, func, 0, NULL, message);
wp_log_fields_log (&lf);
}

View file

@ -14,7 +14,21 @@
G_BEGIN_DECLS
#define WP_LOG_LEVEL_TRACE (1 << G_LOG_LEVEL_USER_SHIFT)
/*!
* \brief A custom GLib log level for trace messages (extension of GLogLevelFlags)
* \ingroup wplog
*/
static const guint WP_LOG_LEVEL_TRACE = (1 << 8);
/*
The above WP_LOG_LEVEL_TRACE constant is intended to be defined as
(1 << G_LOG_LEVEL_USER_SHIFT), but due to a gobject-introspection bug
we define it with the value of G_LOG_LEVEL_USER_SHIFT, which is 8, so
that it ends up correctly in the bindings. To avoid value mismatches,
we statically verify here that G_LOG_LEVEL_USER_SHIFT is indeed 8.
See https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/540
*/
G_STATIC_ASSERT (G_LOG_LEVEL_USER_SHIFT == 8);
#define WP_OBJECT_FORMAT "<%s:%p>"
#define WP_OBJECT_ARGS(object) \
@ -24,57 +38,68 @@ WP_PRIVATE_API
void wp_log_init (gint flags);
WP_API
gboolean wp_log_set_global_level (const gchar *log_level);
gboolean wp_log_set_level (const gchar *log_level);
typedef struct _WpLogTopic WpLogTopic;
struct _WpLogTopic {
/*!
* \brief WpLogTopic flags
* \ingroup wplog
*/
typedef enum { /*< flags >*/
/*! the lower 16 bits of the flags are GLogLevelFlags */
WP_LOG_TOPIC_LEVEL_MASK = 0xFFFF,
/*! the log topic has infinite lifetime (lives on static storage) */
WP_LOG_TOPIC_FLAG_STATIC = 1u << 30,
/*! the log topic has been initialized */
WP_LOG_TOPIC_FLAG_INITIALIZED = 1u << 31,
} WpLogTopicFlags;
/*!
* \brief A structure representing a log topic
* \ingroup wplog
*/
typedef struct {
const char *topic_name;
WpLogTopicFlags flags;
/*< private >*/
/*
* lower 16 bits: GLogLevelFlags
* bit 29: has_custom_level
* bit 30: a g_bit_lock
* bit 31: 1 - initialized, 0 - not initialized
*/
gint flags;
gint *global_flags;
WP_PADDING(2)
};
WP_PADDING(3)
} WpLogTopic;
#define WP_LOG_TOPIC_EXTERN(var) \
extern WpLogTopic * var;
#define WP_LOG_TOPIC(var, t) \
WpLogTopic var##_struct = { .topic_name = t, .flags = 0 }; \
#define WP_LOG_TOPIC(var, name) \
WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
WpLogTopic * var = &(var##_struct);
#define WP_LOG_TOPIC_STATIC(var, t) \
static WpLogTopic var##_struct = { .topic_name = t, .flags = 0 }; \
#define WP_LOG_TOPIC_STATIC(var, name) \
static WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
static G_GNUC_UNUSED WpLogTopic * var = &(var##_struct);
#define WP_DEFINE_LOCAL_LOG_TOPIC(t) \
WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, t)
#define WP_DEFINE_LOCAL_LOG_TOPIC(name) \
WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, name)
/* make glib log functions also use the local log topic */
#ifdef G_LOG_DOMAIN
# undef G_LOG_DOMAIN
#ifdef WP_USE_LOCAL_LOG_TOPIC_IN_G_LOG
# ifdef G_LOG_DOMAIN
# undef G_LOG_DOMAIN
# endif
# define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
#endif
#define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
WP_API
void wp_log_topic_init (WpLogTopic *topic);
WP_API
void wp_log_topic_register (WpLogTopic *topic);
WP_API
void wp_log_topic_unregister (WpLogTopic *topic);
static inline gboolean
wp_log_topic_is_initialized (WpLogTopic *topic)
{
return (topic->flags & (1u << 31)) != 0;
}
static inline gboolean
wp_log_topic_has_custom_level (WpLogTopic *topic)
{
return (topic->flags & (1u << 29)) != 0;
return (topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) != 0;
}
static inline gboolean
@ -84,10 +109,7 @@ wp_log_topic_is_enabled (WpLogTopic *topic, GLogLevelFlags log_level)
if (G_UNLIKELY (!wp_log_topic_is_initialized (topic)))
wp_log_topic_init (topic);
if (wp_log_topic_has_custom_level (topic))
return (topic->flags & (log_level & 0xFFFF)) != 0;
else
return (*topic->global_flags & (log_level & 0xFFFF)) != 0;
return (topic->flags & log_level & WP_LOG_TOPIC_LEVEL_MASK) != 0;
}
#define wp_local_log_topic_is_enabled(log_level) \
@ -99,6 +121,12 @@ GLogWriterOutput wp_log_writer_default (GLogLevelFlags log_level,
WP_API
void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
const gchar *file, const gchar *line, const gchar *func,
GType object_type, gconstpointer object,
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9) G_GNUC_DEPRECATED_FOR (wp_logt_checked);
WP_API
void wp_logt_checked (const WpLogTopic *topic, GLogLevelFlags log_level,
const gchar *file, const gchar *line, const gchar *func,
GType object_type, gconstpointer object,
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9);
@ -106,7 +134,7 @@ void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
#define wp_log(topic, level, type, object, ...) \
({ \
if (G_UNLIKELY (wp_log_topic_is_enabled (topic, level))) \
wp_log_checked (topic->topic_name, level, __FILE__, G_STRINGIFY (__LINE__), \
wp_logt_checked (topic, level, __FILE__, G_STRINGIFY (__LINE__), \
G_STRFUNC, type, object, __VA_ARGS__); \
})

View file

@ -1,4 +1,5 @@
wp_lib_sources = files(
'base-dirs.c',
'client.c',
'component-loader.c',
'conf.c',
@ -21,8 +22,10 @@ wp_lib_sources = files(
'object.c',
'object-interest.c',
'object-manager.c',
'permission-manager.c',
'plugin.c',
'port.c',
'proc-utils.c',
'properties.c',
'proxy.c',
'proxy-interfaces.c',
@ -40,9 +43,11 @@ wp_lib_sources = files(
wp_lib_priv_sources = files(
'private/pipewire-object-mixin.c',
'private/internal-comp-loader.c',
'private/registry.c',
)
wp_lib_headers = files(
'base-dirs.h',
'client.h',
'component-loader.h',
'conf.h',
@ -65,8 +70,10 @@ wp_lib_headers = files(
'object.h',
'object-interest.h',
'object-manager.h',
'permission-manager.h',
'plugin.h',
'port.h',
'proc-utils.h',
'properties.h',
'proxy.h',
'proxy-interfaces.h',
@ -109,22 +116,26 @@ wpversion = configure_file(
)
wp_gen_sources += [wpversion]
wpbuildbasedirs_data = configuration_data()
wpbuildbasedirs_data.set('BUILD_SYSCONFDIR', '"@0@"'.format(get_option('prefix') / get_option('sysconfdir')))
wpbuildbasedirs_data.set('BUILD_DATADIR', '"@0@"'.format(get_option('prefix') / get_option('datadir')))
wpbuildbasedirs_data.set('BUILD_LIBDIR', '"@0@"'.format(get_option('prefix') / get_option('libdir')))
wpbuildbasedirs_data.set('BUILD_LOCALEDIR', '"@0@"'.format(get_option('prefix') / get_option('localedir')))
wpbuildbasedirs = configure_file (
output : 'wpbuildbasedirs.h',
configuration : wpbuildbasedirs_data,
)
wp_lib = library('wireplumber-' + wireplumber_api_version,
wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion,
wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion, wpbuildbasedirs,
c_args : [
'-D_GNU_SOURCE',
'-DG_LOG_USE_STRUCTURED',
'-DWIREPLUMBER_DEFAULT_MODULE_DIR="@0@"'.format(wireplumber_module_dir),
'-DWIREPLUMBER_DEFAULT_CONFIG_DIR="@0@"'.format(wireplumber_config_dir),
'-DWIREPLUMBER_DEFAULT_DATA_DIR="@0@"'.format(wireplumber_data_dir),
'-DLOCALE_DIR="@0@"'.format(wireplumber_locale_dir),
'-DBUILDING_WP',
],
install: true,
include_directories: wp_lib_include_dir,
dependencies : [gobject_dep, gmodule_dep, gio_dep, pipewire_dep, libintl_dep],
soversion: wireplumber_so_version,
version: meson.project_version(),
version: wireplumber_libversion,
)
wp_dep = declare_dependency(

View file

@ -41,7 +41,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-metadata")
* gchar * value,
* gpointer user_data)
* \endcode
* Emited when metadata change
* Emitted when metadata change
*
* Parameters:
* - `subject` - the metadata subject id
@ -319,6 +319,129 @@ wp_metadata_class_init (WpMetadataClass * klass)
G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
}
/*!
* \struct WpMetadataItem
*
* WpMetadataItem holds the subject, key, type and value of a metadata entry.
*/
struct _WpMetadataItem
{
WpMetadata *metadata;
guint32 subject;
const gchar *key;
const gchar *type;
const gchar *value;
};
G_DEFINE_BOXED_TYPE (WpMetadataItem, wp_metadata_item,
wp_metadata_item_ref, wp_metadata_item_unref)
static WpMetadataItem *
wp_metadata_item_new (WpMetadata *metadata, guint32 subject, const gchar *key,
const gchar *type, const gchar *value)
{
WpMetadataItem *self = g_rc_box_new0 (WpMetadataItem);
self->metadata = g_object_ref (metadata);
self->subject = subject;
self->key = key;
self->type = type;
self->value = value;
return self;
}
static void
wp_metadata_item_free (gpointer p)
{
WpMetadataItem *self = p;
g_clear_object (&self->metadata);
}
/*!
* \brief Increases the reference count of a metadata item object
* \ingroup wpmetadata
* \param self a metadata item object
* \returns (transfer full): \a self with an additional reference count on it
* \since 0.5.0
*/
WpMetadataItem *
wp_metadata_item_ref (WpMetadataItem *self)
{
return g_rc_box_acquire (self);
}
/*!
* \brief Decreases the reference count on \a self and frees it when the ref
* count reaches zero.
* \ingroup wpmetadata
* \param self (transfer full): a metadata item object
* \since 0.5.0
*/
void
wp_metadata_item_unref (WpMetadataItem *self)
{
g_rc_box_release_full (self, wp_metadata_item_free);
}
/*!
* \brief Gets the subject from a metadata item
*
* \ingroup wpmetadata
* \param self the item held by the GValue that was returned from the WpIterator
* of wp_metadata_new_iterator()
* \returns the metadata subject of the \a item
* \since 0.5.0
*/
guint32
wp_metadata_item_get_subject (WpMetadataItem * self)
{
return self->subject;
}
/*!
* \brief Gets the key from a metadata item
*
* \ingroup wpmetadata
* \param self the item held by the GValue that was returned from the WpIterator
* of wp_metadata_new_iterator()
* \returns (transfer none): the metadata key of the \a item
* \since 0.5.0
*/
const gchar *
wp_metadata_item_get_key (WpMetadataItem * self)
{
return self->key;
}
/*!
* \brief Gets the value type from a metadata item
*
* \ingroup wpmetadata
* \param self the item held by the GValue that was returned from the WpIterator
* of wp_metadata_new_iterator()
* \returns (transfer none): the metadata value type of the \a item
* \since 0.5.0
*/
const gchar *
wp_metadata_item_get_value_type (WpMetadataItem * self)
{
return self->type;
}
/*!
* \brief Gets the value from a metadata item
*
* \ingroup wpmetadata
* \param self the item held by the GValue that was returned from the WpIterator
* of wp_metadata_new_iterator()
* \returns (transfer none): the metadata value of the \a item
* \since 0.5.0
*/
const gchar *
wp_metadata_item_get_value (WpMetadataItem * self)
{
return self->value;
}
struct metadata_iterator_data
{
WpMetadata *metadata;
@ -346,8 +469,11 @@ metadata_iterator_next (WpIterator *it, GValue *item)
while (pw_array_check (&priv->metadata, it_data->item)) {
if ((it_data->subject == PW_ID_ANY ||
it_data->subject == it_data->item->subject)) {
g_value_init (item, G_TYPE_POINTER);
g_value_set_pointer (item, (gpointer) it_data->item);
g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
it_data->item->subject, it_data->item->key, it_data->item->type,
it_data->item->value);
g_value_init (item, WP_TYPE_METADATA_ITEM);
g_value_take_boxed (item, g_steal_pointer (&mi));
it_data->item++;
return TRUE;
}
@ -369,8 +495,11 @@ metadata_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
if ((it_data->subject == PW_ID_ANY ||
it_data->subject == it_data->item->subject)) {
g_auto (GValue) item = G_VALUE_INIT;
g_value_init (&item, G_TYPE_POINTER);
g_value_set_pointer (&item, (gpointer) i);
g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
it_data->item->subject, it_data->item->key, it_data->item->type,
it_data->item->value);
g_value_init (&item, WP_TYPE_METADATA_ITEM);
g_value_take_boxed (&item, g_steal_pointer (&mi));
if (!func (&item, ret, data))
return FALSE;
}
@ -407,8 +536,7 @@ static const WpIteratorMethods metadata_iterator_methods = {
* \param self a metadata object
* \param subject the metadata subject id, or -1 (PW_ID_ANY)
* \returns (transfer full): an iterator that iterates over the found metadata.
* Use wp_metadata_iterator_item_extract() to parse the items returned by
* this iterator.
* The type of the iterator item is WpMetadataItem.
*/
WpIterator *
wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
@ -429,33 +557,6 @@ wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
return g_steal_pointer (&it);
}
/*!
* \brief Extracts the metadata subject, key, type and value out of a
* GValue that was returned from the WpIterator of wp_metadata_find()
*
* \ingroup wpmetadata
* \param item a GValue that was returned from the WpIterator of wp_metadata_find()
* \param subject (out)(optional): the subject id of the current item
* \param key (out)(optional)(transfer none): the key of the current item
* \param type (out)(optional)(transfer none): the type of the current item
* \param value (out)(optional)(transfer none): the value of the current item
*/
void
wp_metadata_iterator_item_extract (const GValue * item, guint32 * subject,
const gchar ** key, const gchar ** type, const gchar ** value)
{
const struct item *i = g_value_get_pointer (item);
g_return_if_fail (i != NULL);
if (subject)
*subject = i->subject;
if (key)
*key = i->key;
if (type)
*type = i->type;
if (value)
*value = i->value;
}
/*!
* \brief Finds the metadata value given its \a subject and \a key.
*
@ -474,8 +575,10 @@ wp_metadata_find (WpMetadata * self, guint32 subject, const gchar * key,
g_auto (GValue) val = G_VALUE_INIT;
it = wp_metadata_new_iterator (self, subject);
for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
const gchar *k = NULL, *t = NULL, *v = NULL;
wp_metadata_iterator_item_extract (&val, NULL, &k, &t, &v);
WpMetadataItem *mi = g_value_get_boxed (&val);
const gchar *k = wp_metadata_item_get_key (mi);
const gchar *t = wp_metadata_item_get_value_type (mi);
const gchar *v = wp_metadata_item_get_value (mi);
if (g_strcmp0 (k, key) == 0) {
if (type)
*type = t;

View file

@ -13,6 +13,36 @@
G_BEGIN_DECLS
/*!
* \brief The WpMetadataItem GType
* \ingroup wpmetadata
*/
#define WP_TYPE_METADATA_ITEM (wp_metadata_item_get_type ())
WP_API
GType wp_metadata_item_get_type (void);
typedef struct _WpMetadataItem WpMetadataItem;
WP_API
WpMetadataItem *wp_metadata_item_ref (WpMetadataItem *self);
WP_API
void wp_metadata_item_unref (WpMetadataItem *self);
WP_API
guint32 wp_metadata_item_get_subject (WpMetadataItem * self);
WP_API
const gchar * wp_metadata_item_get_key (WpMetadataItem * self);
WP_API
const gchar * wp_metadata_item_get_value_type (WpMetadataItem * self);
WP_API
const gchar * wp_metadata_item_get_value (WpMetadataItem * self);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpMetadataItem, wp_metadata_item_unref)
/*!
* \brief An extension of WpProxyFeatures for WpMetadata objects
* \ingroup wpmetadata
@ -42,10 +72,6 @@ struct _WpMetadataClass
WP_API
WpIterator * wp_metadata_new_iterator (WpMetadata * self, guint32 subject);
WP_API
void wp_metadata_iterator_item_extract (const GValue * item, guint32 * subject,
const gchar ** key, const gchar ** type, const gchar ** value);
WP_API
const gchar * wp_metadata_find (WpMetadata * self, guint32 subject,
const gchar * key, const gchar ** type);

View file

@ -33,6 +33,7 @@ struct _WpImplModule
WpProperties *props; /* only used during module load */
struct pw_impl_module *pw_impl_module;
struct spa_hook impl_module_listener;
};
G_DEFINE_TYPE (WpImplModule, wp_impl_module, G_TYPE_OBJECT);
@ -46,6 +47,17 @@ enum {
PROP_PW_IMPL_MODULE,
};
static void impl_module_free (void *data)
{
WpImplModule *self = WP_IMPL_MODULE (data);
self->pw_impl_module = NULL;
}
static const struct pw_impl_module_events impl_module_events = {
PW_VERSION_IMPL_MODULE_EVENTS,
.free = impl_module_free,
};
static void
wp_impl_module_init (WpImplModule * self)
{
@ -80,10 +92,15 @@ wp_impl_module_constructed (GObject * object)
self->pw_impl_module =
pw_context_load_module (context, self->name, self->args, props);
if (self->pw_impl_module && self->props) {
/* With the module loaded, properties are just passthrough now */
wp_properties_unref (self->props);
self->props = NULL;
if (self->pw_impl_module) {
if (self->props) {
/* With the module loaded, properties are just passthrough now */
wp_properties_unref (self->props);
self->props = NULL;
}
pw_impl_module_add_listener (self->pw_impl_module,
&self->impl_module_listener, &impl_module_events, self);
}
G_OBJECT_CLASS (wp_impl_module_parent_class)->constructed (object);
@ -104,6 +121,8 @@ wp_impl_module_finalize (GObject * object)
if (self->props)
wp_properties_unref (self->props);
G_OBJECT_CLASS (wp_impl_module_parent_class)->finalize (object);
}
static void

View file

@ -63,8 +63,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-node")
* \code
* void
* state_changed_callback (WpNode * self,
* WpNodeState * old_state,
* WpNodeState * new_state,
* WpNodeState old_state,
* WpNodeState new_state,
* gpointer user_data)
* \endcode
*
@ -631,7 +631,7 @@ wp_node_send_command (WpNode * self, const gchar * command)
struct spa_command cmd =
SPA_NODE_COMMAND_INIT(wp_spa_id_value_number (command_value));
pw_node_send_command (wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
pw_node_send_command ((struct pw_node*)wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
}
/*! \defgroup wpimplnode WpImplNode */

View file

@ -81,7 +81,7 @@ G_DEFINE_BOXED_TYPE (WpObjectInterest, wp_object_interest,
* For further reading on the constraint's arguments, see
* wp_object_interest_add_constraint()
*
* For example, this interest matches objects that are descendands of WpProxy
* For example, this interest matches objects that are descendants of WpProxy
* with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
* called "format.dsp" that contains the string "audio" somewhere in the value
* and with a pipewire property "port.name" being present (with any value):
@ -770,6 +770,8 @@ wp_object_interest_matches_full (WpObjectInterest * self,
if (!pw_global_props && WP_IS_SESSION_ITEM (object)) {
WpSessionItem *si = (WpSessionItem *) object;
pw_global_props = props = wp_session_item_get_properties (si);
if (!pw_props)
pw_props = props;
}
}
@ -879,3 +881,51 @@ wp_object_interest_matches_full (WpObjectInterest * self,
}
return result;
}
/*!
* \brief Finds all the defined constraint values for a subject in \a self.
*
* A defined constraint value is the value of a constraint with the 'equal' or
* 'in-list' verb, because the full value must be defined with those verbs. This
* can be useful for cases where we want to enumerate interests that are
* interested in specific subjects.
*
* \ingroup wpobjectinterest
* \param self the object interest
* \param type the constraint type
* \param subject the subject that the constraint applies to
* \returns (element-type GVariant) (transfer full) (nullable): the defined
* constraint values for this object interest.
* \since 0.5.13
*/
GPtrArray *
wp_object_interest_find_defined_constraint_values (WpObjectInterest * self,
WpConstraintType type, const gchar * subject)
{
GPtrArray *res = g_ptr_array_new_with_free_func (
(GDestroyNotify)g_variant_unref);
struct constraint *c;
pw_array_for_each (c, &self->constraints) {
if ((c->type == type || WP_CONSTRAINT_TYPE_NONE == type) &&
g_str_equal (c->subject, subject)) {
switch (c->verb) {
case WP_CONSTRAINT_VERB_EQUALS:
g_ptr_array_add (res, g_variant_ref (c->value));
break;
case WP_CONSTRAINT_VERB_IN_LIST: {
GVariantIter iter;
GVariant *child;
g_variant_iter_init (&iter, c->value);
while ((child = g_variant_iter_next_value (&iter)))
g_ptr_array_add (res, child);
break;
}
default:
break;
}
}
}
return res;
}

View file

@ -67,18 +67,17 @@ typedef enum { /*< flags >*/
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES = (1 << 1),
WP_INTEREST_MATCH_PW_PROPERTIES = (1 << 2),
WP_INTEREST_MATCH_G_PROPERTIES = (1 << 3),
} WpInterestMatch;
/*!
* \brief Special WpInterestMatch value that indicates that all constraints
* have been matched
* \ingroup wpobjectinterest
*/
#define WP_INTEREST_MATCH_ALL \
(WP_INTEREST_MATCH_GTYPE | \
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES | \
WP_INTEREST_MATCH_PW_PROPERTIES | \
WP_INTEREST_MATCH_G_PROPERTIES)
/*!
* Special WpInterestMatch value that indicates that all constraints
* have been matched
*/
WP_INTEREST_MATCH_ALL =
(WP_INTEREST_MATCH_GTYPE |
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
WP_INTEREST_MATCH_PW_PROPERTIES |
WP_INTEREST_MATCH_G_PROPERTIES),
} WpInterestMatch;
/*!
* \brief Flags to alter the behaviour of wp_object_interest_matches_full()
@ -86,7 +85,7 @@ typedef enum { /*< flags >*/
*/
typedef enum { /*< flags >*/
WP_INTEREST_MATCH_FLAGS_NONE = 0,
/*! check all the constraints instead of returning after the first mis-match */
/*! check all the constraints instead of returning after the first mismatch */
WP_INTEREST_MATCH_FLAGS_CHECK_ALL = (1 << 0),
} WpInterestMatchFlags;
@ -131,6 +130,10 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
WpInterestMatchFlags flags, GType object_type, gpointer object,
WpProperties * pw_props, WpProperties * pw_global_props);
WP_API
GPtrArray * wp_object_interest_find_defined_constraint_values (
WpObjectInterest * self, WpConstraintType type, const gchar * subject);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
G_END_DECLS

View file

@ -587,7 +587,7 @@ wp_object_manager_lookup (WpObjectManager * self, GType gtype, ...)
*
* \ingroup wpobjectmanager
* \param self the object manager
* \param interest (transfer full): the interst
* \param interest (transfer full): the interest
* \returns (type GObject)(transfer full)(nullable): the first managed object
* that matches the lookup interest, or NULL if no object matches
*/
@ -672,7 +672,13 @@ idle_emit_objects_changed (WpObjectManager * self)
return G_SOURCE_REMOVE;
}
static void
/*!
* \brief Checks if the object manager should emit the 'objects-changed' signal
* \private
* \ingroup wpobjectmanager
* \param self the object manager
*/
void
wp_object_manager_maybe_objects_changed (WpObjectManager * self)
{
wp_trace_object (self, "pending:%u changed:%d idle_source:%p installed:%d",
@ -724,8 +730,15 @@ wp_object_manager_maybe_objects_changed (WpObjectManager * self)
}
}
/* caller must also call wp_object_manager_maybe_objects_changed() after */
static void
/*!
* \brief Adds an object to the object manager.
* \private
* \ingroup wpobjectmanager
* \param self the object manager
* \param object (transfer none): the object to add
* \note caller must also call wp_object_manager_maybe_objects_changed() after
*/
void
wp_object_manager_add_object (WpObjectManager * self, gpointer object)
{
if (wp_object_manager_is_interested_in_object (self, object)) {
@ -736,8 +749,15 @@ wp_object_manager_add_object (WpObjectManager * self, gpointer object)
}
}
/* caller must also call wp_object_manager_maybe_objects_changed() after */
static void
/*!
* \brief Removes an object from the object manager.
* \private
* \ingroup wpobjectmanager
* \param self the object manager
* \param object the object to remove
* \note caller must also call wp_object_manager_maybe_objects_changed() after
*/
void
wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
{
guint index;
@ -765,8 +785,15 @@ on_proxy_ready (GObject * proxy, GAsyncResult * res, gpointer data)
wp_object_manager_maybe_objects_changed (self);
}
/* caller must also call wp_object_manager_maybe_objects_changed() after */
static void
/*!
* \brief Adds a global object to the object manager.
* \private
* \ingroup wpobjectmanager
* \param self the object manager
* \param global the global object to add
* \note caller must also call wp_object_manager_maybe_objects_changed() after
*/
void
wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
{
WpProxyFeatures features = 0;
@ -795,405 +822,6 @@ wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
}
}
/*
* WpRegistry:
*
* The registry keeps track of registered objects on the wireplumber core.
* There are 3 kinds of registered objects:
*
* 1) PipeWire global objects, which live in another process.
*
* These objects are represented by a WpGlobal with the
* WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
* the registry_global() event is fired and are removed by
* registry_global_remove(). These objects do not have an associated
* WpProxy, unless there is at least one WpObjectManager that is interested
* in them. In this case, a WpProxy is constructed and it is owned by the
* WpGlobal until the global is removed by the registry_global_remove() event.
*
* 2) PipeWire global objects, which were constructed by this process, either
* by calling into a remove factory (see wp_node_new_from_factory()) or
* by exporting a local object (WpImplSession etc...).
*
* These objects are also represented by a WpGlobal, which may however be
* constructed before they appear on the registry. The associated WpProxy
* calls into wp_registry_prepare_new_global() at the time it receives
* the 'bound' event and creates a global that has the
* WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
* these globals are "owned" by the WpProxy and the WpGlobal has no ref
* on the WpProxy itself. This allows destroying the proxy in client code
* by dropping its last reference.
*
* Normally, these global objects also appear on the pipewire registry. When
* this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
* and that keeps an additional reference on the global (both flags must
* be dropped before the WpGlobal is destroyed).
*
* In some cases, such an object might appear first on the registry and
* then receive the 'bound' event. In order to handle this situation, globals
* are not advertised immediately when they appear on the registry, but
* they are added on a tmp_globals list instead, which is emptied on the
* next core sync. In all cases, the proxy 'bound' and the registry 'global'
* events will be fired in the same sync cycle, so we can catch a late
* 'bound' event and still associate the proxy with the WpGlobal before
* object managers are notified about the existence of this global.
*
* 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
*
* These are local objects that have nothing to do with PipeWire. They do not
* have a global id and they are also not subclasses of WpProxy. The registry
* always owns a reference on them, so that they are kept alive for as long
* as the WpCore is alive.
*/
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "wp-registry"
void
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
{
for (guint i = 0; i < self->object_managers->len; i++) {
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
wp_object_manager_add_object (om, object);
wp_object_manager_maybe_objects_changed (om);
}
}
void
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
{
for (guint i = 0; i < self->object_managers->len; i++) {
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
wp_object_manager_rm_object (om, object);
wp_object_manager_maybe_objects_changed (om);
}
}
static void
object_manager_destroyed (gpointer data, GObject * om)
{
WpRegistry *self = data;
g_ptr_array_remove_fast (self->object_managers, om);
}
/* find the subclass of WpPipewireGloabl that can handle
the given pipewire interface type of the given version */
static inline GType
find_proxy_instance_type (const char * type, guint32 version)
{
g_autofree GType *children;
guint n_children;
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
for (guint i = 0; i < n_children; i++) {
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
klass->pw_iface_version == version) {
g_type_class_unref (klass);
return children[i];
}
g_type_class_unref (klass);
}
return WP_TYPE_GLOBAL_PROXY;
}
/* called by the registry when a global appears */
static void
registry_global (void *data, uint32_t id, uint32_t permissions,
const char *type, uint32_t version, const struct spa_dict *props)
{
WpRegistry *self = data;
GType gtype = find_proxy_instance_type (type, version);
wp_debug_object (wp_registry_get_core (self),
"global:%u perm:0x%x type:%s/%u -> %s",
id, permissions, type, version, g_type_name (gtype));
wp_registry_prepare_new_global (self, id, permissions,
WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
}
/* called by the registry when a global is removed */
static void
registry_global_remove (void *data, uint32_t id)
{
WpRegistry *self = data;
WpGlobal *global = NULL;
if (id < self->globals->len)
global = g_ptr_array_index (self->globals, id);
/* if not found, look in the tmp_globals, as it may still not be exposed */
if (!global) {
for (guint i = 0; i < self->tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
if (g->id == id) {
global = g;
break;
}
}
}
g_return_if_fail (global &&
global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
wp_debug_object (wp_registry_get_core (self),
"global removed:%u type:%s", id, g_type_name (global->type));
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = registry_global,
.global_remove = registry_global_remove,
};
void
wp_registry_init (WpRegistry *self)
{
self->globals =
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
self->tmp_globals =
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
self->objects = g_ptr_array_new_with_free_func (g_object_unref);
self->object_managers = g_ptr_array_new ();
self->features = g_ptr_array_new_with_free_func (g_free);
}
void
wp_registry_clear (WpRegistry *self)
{
wp_registry_detach (self);
g_clear_pointer (&self->globals, g_ptr_array_unref);
g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
g_clear_pointer (&self->features, g_ptr_array_unref);
/* remove all the registered objects
this will normally also destroy the object managers, eventually, since
they are normally ref'ed by modules, which are registered objects */
{
g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);
while (objlist->len > 0) {
g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
objlist->len - 1);
wp_registry_notify_rm_object (self, object);
}
}
/* in case there are any object managers left,
remove the weak ref on them and let them be... */
{
g_autoptr (GPtrArray) object_mgrs;
GObject *om;
object_mgrs = g_steal_pointer (&self->object_managers);
while (object_mgrs->len > 0) {
om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
g_object_weak_unref (om, object_manager_destroyed, self);
}
}
}
void
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
{
self->pw_registry = pw_core_get_registry (pw_core,
PW_VERSION_REGISTRY, 0);
pw_registry_add_listener (self->pw_registry, &self->listener,
&registry_events, self);
}
void
wp_registry_detach (WpRegistry *self)
{
if (self->pw_registry) {
spa_hook_remove (&self->listener);
pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
self->pw_registry = NULL;
}
/* remove pipewire globals */
GPtrArray *objlist = self->globals;
while (objlist && objlist->len > 0) {
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
objlist->len - 1);
if (!global)
continue;
if (global->proxy)
wp_registry_notify_rm_object (self, global->proxy);
/* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
by the registry; set registry to NULL to avoid further interference */
global->registry = NULL;
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
/* the registry's ref on global is dropped here; it may still live if
there is a proxy that owns a ref on it, but global->registry is set
to NULL, so there is no further interference */
}
/* drop tmp globals as well */
objlist = self->tmp_globals;
while (objlist && objlist->len > 0) {
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
objlist->len - 1);
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
}
}
static gboolean
expose_tmp_globals (WpCore *core)
{
WpRegistry *self = wp_core_get_registry (core);
g_autoptr (GPtrArray) tmp_globals = NULL;
g_autoptr (GPtrArray) object_managers = NULL;
/* in case the registry was cleared in the meantime... */
if (G_UNLIKELY (!self->tmp_globals))
return G_SOURCE_REMOVE;
/* steal the tmp_globals list and replace it with an empty one */
tmp_globals = self->tmp_globals;
self->tmp_globals =
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
wp_debug_object (core, "exposing %u new globals", tmp_globals->len);
/* traverse in the order that the globals appeared on the registry */
for (guint i = 0; i < tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
/* if global was already removed, drop it */
if (g->flags == 0 || g->id == SPA_ID_INVALID)
continue;
/* if old global is owned by proxy, remove it */
if (self->globals->len > g->id) {
WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
}
g_return_val_if_fail (self->globals->len <= g->id ||
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
/* set the registry, so that wp_global_rm_flag() can work full-scale */
g->registry = self;
/* store it in the globals list */
if (self->globals->len <= g->id)
g_ptr_array_set_size (self->globals, g->id + 1);
g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
}
object_managers = g_ptr_array_copy (self->object_managers,
(GCopyFunc) g_object_ref, NULL);
g_ptr_array_set_free_func (object_managers, g_object_unref);
/* notify object managers */
for (guint i = 0; i < object_managers->len; i++) {
WpObjectManager *om = g_ptr_array_index (object_managers, i);
for (guint i = 0; i < tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
/* if global was already removed, drop it */
if (g->flags == 0 || g->id == SPA_ID_INVALID)
continue;
wp_object_manager_add_global (om, g);
}
wp_object_manager_maybe_objects_changed (om);
}
return G_SOURCE_REMOVE;
}
/*
* \param new_global (out) (transfer full) (optional): the new global
*
* This is normally called up to 2 times in the same sync cycle:
* one from registry_global(), another from the proxy bound event
* Unfortunately the order in which those 2 events happen is specific
* to the implementation of the object, which is why this is implemented
* with a temporary globals list that get exposed later to the object managers
*/
void
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
guint32 permissions, guint32 flag, GType type,
WpGlobalProxy *proxy, const struct spa_dict *props,
WpGlobal ** new_global)
{
g_autoptr (WpGlobal) global = NULL;
WpCore *core = wp_registry_get_core (self);
g_return_if_fail (flag != 0);
for (guint i = 0; i < self->tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
if (g->id == id) {
global = wp_global_ref (g);
break;
}
}
wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
global ? "reuse" : "new", id, g_type_name (type),
(global && global->proxy) ? global->proxy : proxy);
if (!global) {
global = g_rc_box_new0 (WpGlobal);
global->flags = flag;
global->id = id;
global->type = type;
global->permissions = permissions;
global->properties = props ?
wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
global->proxy = proxy;
g_ptr_array_add (self->tmp_globals, wp_global_ref (global));
/* ensure we have 'object.id' so that we can filter by id on object managers */
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);
/* schedule exposing when adding the first global */
if (self->tmp_globals->len == 1) {
wp_core_idle_add_closure (core, NULL,
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
}
} else {
/* store the most permissive permissions */
if (permissions > global->permissions)
global->permissions = permissions;
global->flags |= flag;
/* store the most deep type (i.e. WpImplNode instead of WpNode),
so that object-manager interests can work more accurately
if the interest is on a specific subclass */
if (g_type_depth (type) > g_type_depth (global->type))
global->type = type;
if (proxy) {
g_return_if_fail (global->proxy == NULL);
global->proxy = proxy;
}
if (props)
wp_properties_update_from_dict (global->properties, props);
}
if (new_global)
*new_global = g_steal_pointer (&global);
}
/*!
* \brief Installs the object manager on this core, activating its internal
* management engine.
@ -1209,111 +837,12 @@ void
wp_core_install_object_manager (WpCore * self, WpObjectManager * om)
{
WpRegistry *reg;
guint i;
g_return_if_fail (WP_IS_CORE (self));
g_return_if_fail (WP_IS_OBJECT_MANAGER (om));
reg = wp_core_get_registry (self);
g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, reg);
g_ptr_array_add (reg->object_managers, om);
g_weak_ref_set (&om->core, self);
/* add pre-existing objects to the object manager,
in case it's interested in them */
for (i = 0; i < reg->globals->len; i++) {
WpGlobal *g = g_ptr_array_index (reg->globals, i);
/* check if null because the globals array can have gaps */
if (g)
wp_object_manager_add_global (om, g);
}
for (i = 0; i < reg->objects->len; i++) {
GObject *o = g_ptr_array_index (reg->objects, i);
wp_object_manager_add_object (om, o);
}
wp_object_manager_maybe_objects_changed (om);
}
/* WpGlobal */
G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)
void
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
{
WpRegistry *reg = global->registry;
guint32 id = global->id;
/* no flag to remove */
if (!(global->flags & rm_flag))
return;
wp_trace_boxed (WP_TYPE_GLOBAL, global,
"remove global %u flag 0x%x [flags:0x%x, reg:%p]",
id, rm_flag, global->flags, reg);
/* global was owned by the proxy; by removing the flag, we clear out
also the proxy pointer, which is presumably no longer valid and we
notify all listeners that the proxy is gone */
if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
if (reg && global->proxy) {
wp_registry_notify_rm_object (reg, global->proxy);
}
global->proxy = NULL;
}
/* registry removed the global */
else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;
/* destroy the proxy if it exists */
if (global->proxy) {
/* steal the proxy to avoid calling wp_registry_notify_rm_object()
again while removing OWNED_BY_PROXY;
keep a temporary ref so that _deactivate() doesn't crash in case the
pw-proxy-destroyed signal causes external references to be dropped */
g_autoptr (WpGlobalProxy) proxy =
g_object_ref (g_steal_pointer (&global->proxy));
/* notify all listeners that the proxy is gone */
if (reg)
wp_registry_notify_rm_object (reg, proxy);
/* remove FEATURE_BOUND to destroy the underlying pw_proxy */
wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);
/* stop all in-progress activations */
wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");
/* if the proxy is not owning the global, unref it */
if (global->flags == 0)
g_object_unref (proxy);
}
/* It's possible to receive consecutive {add, remove, add} events for the
* same id. Since the WpGlobal might not be destroyed immediately below,
* (e.g. it's in tmp_globals list), we must invalidate the id now, so that
* this WpGlobal is not used in reference to objects added later.
*/
global->id = SPA_ID_INVALID;
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
}
/* drop the registry's ref on global when it does not appear on the registry anymore */
if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
}
}
struct pw_proxy *
wp_global_bind (WpGlobal * global)
{
g_return_val_if_fail (global->proxy, NULL);
g_return_val_if_fail (global->registry, NULL);
WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
return pw_registry_bind (global->registry->pw_registry, global->id,
klass->pw_iface_type, klass->pw_iface_version, 0);
reg = wp_core_get_registry (self);
wp_registry_install_object_manager (reg, om);
}

View file

@ -72,6 +72,22 @@ WP_API
gpointer wp_object_manager_lookup_full (WpObjectManager * self,
WpObjectInterest * interest);
/* private */
typedef struct _WpGlobal WpGlobal;
WP_PRIVATE_API
void wp_object_manager_maybe_objects_changed (WpObjectManager * self);
WP_PRIVATE_API
void wp_object_manager_add_object (WpObjectManager * self, gpointer object);
WP_PRIVATE_API
void wp_object_manager_rm_object (WpObjectManager * self, gpointer object);
WP_PRIVATE_API
void wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global);
G_END_DECLS
#endif

View file

@ -441,8 +441,8 @@ on_transition_completed (WpTransition * transition, GParamSpec * param,
* \param self the object
* \param features the features to enable
* \param cancellable (nullable): a cancellable for the async operation
* \param callback (scope async): a function to call when activation is complete
* \param user_data (closure): data for \a callback
* \param callback (scope async)(closure user_data): a function to call when activation is complete
* \param user_data data for \a callback
*/
void
wp_object_activate (WpObject * self,

707
lib/wp/permission-manager.c Normal file
View file

@ -0,0 +1,707 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <pipewire/permission.h>
#include <pipewire/pipewire.h>
#include "private/permission-manager.h"
#include "permission-manager.h"
#include "proxy-interfaces.h"
#include "object-manager.h"
#include "json-utils.h"
#include "error.h"
#include "core.h"
#include "log.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-permission-manager")
/*! \defgroup wppermissionmanager WpPermissionManager */
/*!
* \struct WpPermissionManager
*
* The WpPermissionManager class is in charge of updating automatically
* permissions on interested objects every time they are added or removed for
* a particular client.
*
* WpPermissionManager API.
*/
typedef struct _PermissionMatch PermissionMatch;
struct _PermissionMatch
{
guint32 id;
guint32 permissions;
GClosure *closure;
WpObjectInterest *interest;
WpSpaJson *rules;
};
static guint
get_next_id ()
{
static guint32 next_id = 0;
g_atomic_int_inc (&next_id);
return next_id;
}
static PermissionMatch *
permission_match_new (guint32 perms, GClosure *closure,
WpObjectInterest * interest, WpSpaJson * rules)
{
PermissionMatch *match = g_new0 (PermissionMatch, 1);
match->id = get_next_id ();
match->permissions = perms;
match->closure = closure ? g_closure_ref (closure) : NULL;
match->interest = interest ? wp_object_interest_ref (interest) : NULL;
match->rules = rules ? wp_spa_json_ref (rules) : NULL;
return match;
}
static void
permission_interest_free (PermissionMatch *self)
{
g_clear_pointer (&self->closure, g_closure_unref);
g_clear_pointer (&self->interest, wp_object_interest_unref);
g_clear_pointer (&self->rules, wp_spa_json_unref);
g_free (self);
}
struct _WpPermissionManager
{
WpObject parent;
guint32 default_perms;
guint32 core_perms;
GPtrArray *clients;
GHashTable *matches;
WpObjectManager *om;
};
G_DEFINE_TYPE (WpPermissionManager, wp_permission_manager, WP_TYPE_OBJECT)
static void
wp_permission_manager_init (WpPermissionManager * self)
{
/* Init default permissions to all */
self->default_perms = PW_PERM_R | PW_PERM_W | PW_PERM_X;
/* Core permissions not set by default (inherit from default_perms) */
self->core_perms = PW_PERM_INVALID;
/* Init permission interests table */
self->matches = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
(GDestroyNotify)permission_interest_free);
/* Init clients list */
self->clients = g_ptr_array_new_with_free_func (
(GDestroyNotify) g_object_unref);
}
enum {
STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
};
static WpObjectFeatures
wp_permission_manager_get_supported_features (WpObject * self)
{
return WP_PERMISSION_MANAGER_LOADED;
}
static guint
wp_permission_manager_activate_get_next_step (WpObject * self,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
g_return_val_if_fail (missing == WP_PERMISSION_MANAGER_LOADED,
WP_TRANSITION_STEP_ERROR);
return STEP_LOAD;
}
static guint32
invoke_permissions_closure (WpPermissionManager *self, WpClient *client,
WpGlobalProxy *object, GClosure *closure)
{
GValue args[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };
GValue ret = G_VALUE_INIT;
guint32 perms;
g_value_init (&args[0], WP_TYPE_PERMISSION_MANAGER);
g_value_set_object (&args[0], self);
g_value_init (&args[1], WP_TYPE_CLIENT);
g_value_set_object (&args[1], client);
g_value_init (&args[2], WP_TYPE_GLOBAL_PROXY);
g_value_set_object (&args[2], object);
g_value_init (&ret, G_TYPE_UINT);
g_closure_invoke (closure, &ret, 3, args, NULL);
perms = g_value_get_uint (&ret);
g_value_unset (&args[0]);
g_value_unset (&args[1]);
g_value_unset (&args[2]);
g_value_unset (&ret);
return perms;
}
typedef struct _MatchRulesCallbackData MatchRulesCallbackData;
struct _MatchRulesCallbackData {
gboolean matched;
guint32 perms;
};
static gboolean
match_rules_cb (gpointer data, const gchar * action, WpSpaJson * value,
GError ** e)
{
MatchRulesCallbackData *cb_data = (MatchRulesCallbackData *)data;
g_autofree gchar *perms_str = NULL;
guint32 perms = 0;
if (!g_str_equal (action, "set-permissions")) {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Action name '%s' is not valid", action);
return FALSE;
}
if (!wp_spa_json_is_string (value)) {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Action '%s' must be a string", action);
return FALSE;
}
/* Parse permissions */
perms_str = wp_spa_json_parse_string (value);
if (g_strcmp0 (perms_str, "all") == 0) {
perms = PW_PERM_ALL;
} else if (perms_str) {
for (guint i = 0; i < strlen (perms_str); i++) {
switch (perms_str[i]) {
case 'r': perms |= PW_PERM_R; break;
case 'w': perms |= PW_PERM_W; break;
case 'x': perms |= PW_PERM_X; break;
case 'm': perms |= PW_PERM_M; break;
case 'l': perms |= PW_PERM_L; break;
case '-': break;
default: {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Permissions '%s' are not valid", perms_str);
return FALSE;
}
}
}
}
if (cb_data) {
cb_data->matched = TRUE;
cb_data->perms |= perms;
}
return TRUE;
}
static gboolean
get_rules_matched_object_permissions (WpPermissionManager *self,
WpSpaJson *rules, WpGlobalProxy *object, guint32 *perms)
{
g_autoptr (GError) e = NULL;
g_autoptr (WpProperties) gp_props = NULL;
g_autoptr (WpProperties) po_props = NULL;
MatchRulesCallbackData data = { FALSE, 0 };
/* Check global proxy properties */
gp_props = wp_global_proxy_get_global_properties (object);
if (gp_props && !wp_json_utils_match_rules (rules, gp_props, match_rules_cb,
&data, &e))
goto error;
/* Also check pipewire object properties if it is a pipewire object */
if (WP_IS_PIPEWIRE_OBJECT (object)) {
po_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object));
if (po_props && !wp_json_utils_match_rules (rules, po_props, match_rules_cb,
&data, &e))
goto error;
}
/* Set permissions if there was a match */
if (data.matched && perms)
*perms = data.perms;
return data.matched;
error:
wp_warning_object (self, "Malformed JSON match rules: %s", e->message);
return FALSE;
}
static gboolean
get_matched_object_permissions (WpPermissionManager *self, PermissionMatch *m,
WpClient *client, WpGlobalProxy *object, guint32 *perms)
{
/* Check interest */
if (m->interest && wp_object_interest_matches (m->interest, object)) {
if (!perms)
return TRUE;
*perms = m->closure ? invoke_permissions_closure (self, client, object,
m->closure) : m->permissions;
return TRUE;
}
/* Check rules */
if (m->rules)
return get_rules_matched_object_permissions (self, m->rules, object, perms);
return FALSE;
}
static GArray *
build_permissions_array (WpPermissionManager *self, WpClient *client)
{
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) value = G_VALUE_INIT;
struct pw_permission def_perm = { PW_ID_ANY, self->default_perms };
GArray *arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));
/* Add default permissions */
g_array_append_val (arr, def_perm);
/* Add core permissions if explicitly set (core is not in the OM since it is
* implicit in the PipeWire connection and not sent through the registry) */
if (self->core_perms != PW_PERM_INVALID) {
struct pw_permission core_perm = { PW_ID_CORE, self->core_perms };
g_array_append_val (arr, core_perm);
}
/* Add object specific permissions in the array */
it = wp_object_manager_new_iterator (self->om);
for (; wp_iterator_next (it, &value); g_value_unset (&value)) {
WpGlobalProxy *object = g_value_get_object (&value);
GHashTableIter iter;
PermissionMatch *match = NULL;
g_hash_table_iter_init (&iter, self->matches);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&match)) {
guint32 perms = PW_PERM_INVALID;
if (get_matched_object_permissions (self, match, client, object, &perms)
&& perms != PW_PERM_INVALID) {
struct pw_permission obj_perm = { 0, };
obj_perm.id = wp_proxy_get_bound_id (WP_PROXY (object));
obj_perm.permissions = perms;
g_array_append_val (arr, obj_perm);;
}
}
}
/* Merge permissions with same object ID */
for (guint i = 0; i < arr->len; i++) {
for (guint j = i + 1; j < arr->len; ) {
struct pw_permission *a = &g_array_index (arr, struct pw_permission, i);
struct pw_permission *b = &g_array_index (arr, struct pw_permission, j);
if (a->id == b->id) {
a->permissions |= b->permissions;
g_array_remove_index (arr, j);
} else {
j++;
}
}
}
return arr;
}
static void
update_client_permissions (WpPermissionManager *self, WpClient *client)
{
guint32 bound_id = 0;
g_autoptr (GArray) perms = NULL;
/* Dont do anything if the permission manager is not activated */
if (!(wp_object_get_active_features (WP_OBJECT (self)) &
WP_PERMISSION_MANAGER_LOADED))
return;
/* Make sure the client proxy is still valid */
if (!wp_proxy_get_pw_proxy (WP_PROXY (client)))
return;
bound_id = wp_proxy_get_bound_id (WP_PROXY (client));
perms = build_permissions_array (self, client);
wp_info_object (self,
"Updating permissions on client %u: any=%c%c%c%c%c len=%u",
bound_id,
!!(self->default_perms & PW_PERM_R) ? 'r' : '-',
!!(self->default_perms & PW_PERM_W) ? 'w' : '-',
!!(self->default_perms & PW_PERM_X) ? 'x' : '-',
!!(self->default_perms & PW_PERM_M) ? 'm' : '-',
!!(self->default_perms & PW_PERM_L) ? 'l' : '-',
perms->len);
wp_client_update_permissions_array (client, perms->len,
(const struct pw_permission *) perms->data);
}
static gboolean
has_object_match (WpPermissionManager *self, WpGlobalProxy *object)
{
GHashTableIter iter;
PermissionMatch *m = NULL;
g_hash_table_iter_init (&iter, self->matches);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&m)) {
if (m->interest && wp_object_interest_matches (m->interest, object))
return TRUE;
if (m->rules && get_rules_matched_object_permissions (self, m->rules,
object, NULL))
return TRUE;
}
return FALSE;
}
static void
update_permissions (WpPermissionManager *self)
{
for (guint i = 0; i < self->clients->len; i++) {
WpClient *client = g_ptr_array_index (self->clients, i);
update_client_permissions (self, client);
}
}
static void
on_object_added_or_removed (WpObjectManager *om, WpGlobalProxy *object,
gpointer d)
{
WpPermissionManager * self = WP_PERMISSION_MANAGER (d);
if (has_object_match (self, object))
update_permissions (self);
}
static void
on_object_manager_installed (WpObjectManager *om, gpointer d)
{
WpTransition * transition = WP_TRANSITION (d);
WpPermissionManager * self = wp_transition_get_source_object (transition);
wp_object_update_features (WP_OBJECT (self), WP_PERMISSION_MANAGER_LOADED, 0);
}
static void
wp_permission_manager_activate_execute_step (WpObject * object,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_autoptr (WpCore) core = wp_object_get_core (object);
switch (step) {
case STEP_LOAD: {
/* Install object manager */
g_clear_object (&self->om);
self->om = wp_object_manager_new ();
wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
wp_object_manager_request_object_features (self->om,
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
g_signal_connect_object (self->om, "object-added",
G_CALLBACK (on_object_added_or_removed), self, 0);
g_signal_connect_object (self->om, "object-removed",
G_CALLBACK (on_object_added_or_removed), self, 0);
g_signal_connect_object (self->om, "installed",
G_CALLBACK (on_object_manager_installed), transition, 0);
wp_core_install_object_manager (core, self->om);
break;
}
case WP_TRANSITION_STEP_ERROR:
break;
default:
g_assert_not_reached ();
}
}
static void
wp_permission_manager_deactivate (WpObject * object, WpObjectFeatures features)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_clear_object (&self->om);
wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
}
static void
wp_permission_manager_finalize (GObject * object)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_clear_pointer (&self->clients, g_ptr_array_unref);
g_clear_pointer (&self->matches, g_hash_table_unref);
g_clear_object (&self->om);
G_OBJECT_CLASS (wp_permission_manager_parent_class)->finalize (object);
}
static void
wp_permission_manager_class_init (WpPermissionManagerClass * klass)
{
GObjectClass * object_class = (GObjectClass *) klass;
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
object_class->finalize = wp_permission_manager_finalize;
wpobject_class->get_supported_features =
wp_permission_manager_get_supported_features;
wpobject_class->activate_get_next_step =
wp_permission_manager_activate_get_next_step;
wpobject_class->activate_execute_step =
wp_permission_manager_activate_execute_step;
wpobject_class->deactivate = wp_permission_manager_deactivate;
}
void
wp_permission_manager_add_client (WpPermissionManager *self, WpClient *client)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_ptr_array_add (self->clients, g_object_ref (client));
update_client_permissions (self, client);
}
void
wp_permission_manager_remove_client (WpPermissionManager *self,
WpClient *client)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_ptr_array_remove_fast (self->clients, client);
update_client_permissions (self, client);
}
/*!
* \brief Creates a new WpPermissionManager object
*
* \ingroup wppermissionmanager
* \param core the WpCore
* \returns (transfer full): a new WpPermissionManager object
*/
WpPermissionManager *
wp_permission_manager_new (WpCore * core)
{
g_return_val_if_fail (core, NULL);
return g_object_new (WP_TYPE_PERMISSION_MANAGER, "core", core, NULL);
}
/*!
* \brief Sets the default permissions that will be applied to all objects that
* don't match any interest
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the default permissions to apply
*/
void
wp_permission_manager_set_default_permissions (WpPermissionManager *self,
guint32 permissions)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
if (self->default_perms != permissions) {
self->default_perms = permissions;
update_permissions (self);
}
}
/*!
* \brief Sets the permissions that will be applied to the core object (ID 0).
*
* The core object is not visible to the permission manager's object manager
* because it is implicit in the PipeWire connection and not sent through the
* registry. This method allows setting explicit permissions on it, independent
* of the default permissions.
*
* If not set (or set to PW_PERM_INVALID), the core inherits default_permissions.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the permissions to apply to the core object
*/
void
wp_permission_manager_set_core_permissions (WpPermissionManager *self,
guint32 permissions)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
if (self->core_perms != permissions) {
self->core_perms = permissions;
update_permissions (self);
}
}
static guint32
wp_permission_manager_add_match (WpPermissionManager *self,
PermissionMatch *match)
{
guint id = match->id;
g_hash_table_insert (self->matches, GUINT_TO_POINTER (id), match);
update_permissions (self);
return id;
}
/*!
* \brief Adds an interest match to apply permissions with callback in matched
* objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param callback (scope async): the permissions match callback
* \param user_data data to pass to \a callback
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match (WpPermissionManager *self,
WpPermissionMatchCallback callback, gpointer user_data,
WpObjectInterest * interest)
{
GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);
return wp_permission_manager_add_interest_match_closure (self, closure,
interest);
}
/*!
* \brief Adds an interest match to apply permissions with closure in matched
* objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param closure (transfer full): the closure to apply permissions
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match_closure (WpPermissionManager *self,
GClosure *closure, WpObjectInterest * interest)
{
g_autoptr (WpObjectInterest) i = interest;
g_autoptr (GClosure) c = closure;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (closure, SPA_ID_INVALID);
g_return_val_if_fail (i, SPA_ID_INVALID);
if (G_CLOSURE_NEEDS_MARSHAL (closure))
g_closure_set_marshal (closure, g_cclosure_marshal_generic);
match = permission_match_new (PW_PERM_INVALID, c, i, NULL);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Adds an interest match to apply same permissions in matched objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the permissions to apply
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match_simple (WpPermissionManager *self,
guint32 permissions, WpObjectInterest * interest)
{
g_autoptr (WpObjectInterest) i = interest;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (i, SPA_ID_INVALID);
match = permission_match_new (permissions, NULL, i, NULL);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Adds a rules match to apply permissions in matched objects.
*
* The rules must be defined in a JSON object using the same format as all
* the wireplumber/pipewire rules.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param rules (transfer full): the JSON rules
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_rules_match (WpPermissionManager *self,
WpSpaJson *rules)
{
g_autoptr (WpSpaJson) r = rules;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (r, SPA_ID_INVALID);
match = permission_match_new (PW_PERM_INVALID, NULL, NULL, rules);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Removes the previously added match so that the associated permissions
* are not applied anymore.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param match_id the match ID to remove
*/
void
wp_permission_manager_remove_match (WpPermissionManager *self, guint32 match_id)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_return_if_fail (match_id != SPA_ID_INVALID);
g_hash_table_remove (self->matches, GUINT_TO_POINTER (match_id));
update_permissions (self);
}
/*!
* \brief Updates permissions on all clients the permission manager has.
*
* The permission manager already updates permissions on all clients
* automatically when a new client or object is added, however, this might be
* needed if interests with closures or callbacks were added and something
* changed externally.
*
* \ingroup wppermissionmanager
* \param self the permission manager
*/
void
wp_permission_manager_update_permissions (WpPermissionManager *self)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
update_permissions (self);
}

View file

@ -0,0 +1,88 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@ollabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PERMISSION_MANAGER_H__
#define __WIREPLUMBER_PERMISSION_MANAGER_H__
#include "object-interest.h"
#include "global-proxy.h"
G_BEGIN_DECLS
/*!
* \brief Flags to be used as WpObjectFeatures for WpPermissionManager.
* \ingroup wppermissionmanager
*/
typedef enum { /*< flags >*/
/*! Loads the permission manager */
WP_PERMISSION_MANAGER_LOADED = (1 << 0),
} WpPermissionManagerFeatures;
/*!
* \brief The WpPermissionManager GType
* \ingroup wppermissionmanager
*/
#define WP_TYPE_PERMISSION_MANAGER (wp_permission_manager_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpPermissionManager, wp_permission_manager, WP,
PERMISSION_MANAGER, WpObject)
typedef struct _WpClient WpClient;
/*!
* \brief callback to set permissions on the matched global object
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param client the client that will have its permissions updated
* \param object the matched global
* \param user_data the passed data
*/
typedef guint32 (*WpPermissionMatchCallback) (WpPermissionManager *self,
WpClient *client, WpGlobalProxy *object, gpointer user_data);
WP_API
WpPermissionManager * wp_permission_manager_new (WpCore * core);
WP_API
void wp_permission_manager_set_default_permissions (
WpPermissionManager *self, guint32 permissions);
WP_API
void wp_permission_manager_set_core_permissions (
WpPermissionManager *self, guint32 permissions);
WP_API
guint32 wp_permission_manager_add_interest_match (WpPermissionManager *self,
WpPermissionMatchCallback callback, gpointer user_data,
WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_interest_match_closure (
WpPermissionManager *self, GClosure *closure, WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_interest_match_simple (
WpPermissionManager *self, guint32 permissions,
WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_rules_match (WpPermissionManager *self,
WpSpaJson *rules);
WP_API
void wp_permission_manager_remove_match (WpPermissionManager *self,
guint32 match_id);
WP_API
void wp_permission_manager_update_permissions (WpPermissionManager *self);
G_END_DECLS
#endif

View file

@ -198,7 +198,7 @@ wp_plugin_find (WpCore * core, const gchar * plugin_name)
}
/*!
* \brief Retreives the name of a plugin.
* \brief Retrieves the name of a plugin.
*
* \ingroup wpplugin
* \param self the plugin

View file

@ -28,7 +28,7 @@ struct _ComponentData
grefcount ref;
/* an identifier for this component that is understandable by the end user */
gchar *printable_id;
/* the provided feature name (points to same storage as the id) or NULL */
/* the provided feature name */
gchar *provides;
/* the original state of the feature (required / optional / disabled) */
FeatureState state;
@ -39,6 +39,8 @@ struct _ComponentData
WpSpaJson *arguments;
GPtrArray *requires; /* value-type: string (owned) */
GPtrArray *wants; /* value-type: string (owned) */
GPtrArray *before; /* value-type: string (owned) */
GPtrArray *after; /* value-type: string (owned) */
/* TRUE when the component is in the final sorted list */
gboolean visited;
@ -174,6 +176,8 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
g_ref_count_init (&comp->ref);
comp->requires = g_ptr_array_new_with_free_func (g_free);
comp->wants = g_ptr_array_new_with_free_func (g_free);
comp->before = g_ptr_array_new_with_free_func (g_free);
comp->after = g_ptr_array_new_with_free_func (g_free);
props = wp_properties_new_json (json);
if (rules && !wp_json_utils_match_rules (rules, props, component_rule_match_cb,
@ -201,7 +205,7 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
comp->printable_id = g_strdup_printf ("%s [%s]", comp->provides, comp->type);
}
} else {
comp->provides = NULL;
comp->provides = g_strdup_printf ("__anonymous_%p", comp);
comp->state = FEATURE_STATE_REQUIRED;
comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
}
@ -228,6 +232,28 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
}
}
if ((str = wp_properties_get (props, "before"))) {
g_autoptr (WpSpaJson) comp_before = wp_spa_json_new_wrap_string (str);
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_before);
g_auto (GValue) item = G_VALUE_INIT;
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *dep = g_value_get_boxed (&item);
g_ptr_array_add (comp->before, wp_spa_json_to_string (dep));
}
}
if ((str = wp_properties_get (props, "after"))) {
g_autoptr (WpSpaJson) comp_after = wp_spa_json_new_wrap_string (str);
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_after);
g_auto (GValue) item = G_VALUE_INIT;
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *dep = g_value_get_boxed (&item);
g_ptr_array_add (comp->after, wp_spa_json_to_string (dep));
}
}
return g_steal_pointer (&comp);
}
@ -241,6 +267,8 @@ component_data_free (ComponentData * self)
g_clear_pointer (&self->arguments, wp_spa_json_unref);
g_clear_pointer (&self->requires, g_ptr_array_unref);
g_clear_pointer (&self->wants, g_ptr_array_unref);
g_clear_pointer (&self->before, g_ptr_array_unref);
g_clear_pointer (&self->after, g_ptr_array_unref);
g_free (self);
}
@ -297,6 +325,100 @@ wp_component_array_load_task_get_next_step (WpTransition * transition, guint ste
}
}
static gboolean
component_equals (const ComponentData * comp, const gchar * provides)
{
return g_str_equal (provides, comp->provides);
}
static inline gboolean
component_exists_in (const gchar *comp_provides, GPtrArray *list)
{
return g_ptr_array_find_with_equal_func (list, comp_provides,
(GEqualFunc) component_equals, NULL);
}
static gboolean
sort_components_before_after (WpComponentArrayLoadTask * self, GError ** error)
{
g_autoptr (GPtrArray) remaining = g_ptr_array_new_with_free_func (
(GDestroyNotify) component_data_unref);
g_autoptr (GPtrArray) result = g_ptr_array_new_with_free_func (
(GDestroyNotify) component_data_unref);
for (guint i = 0; i < self->components->len; i++) {
ComponentData *comp = g_ptr_array_index (self->components, i);
/* implicitly add all "requires" and "wants" as "after" dependencies */
g_ptr_array_extend (comp->after, comp->requires, (GCopyFunc) g_strdup, NULL);
g_ptr_array_extend (comp->after, comp->wants, (GCopyFunc) g_strdup, NULL);
/* convert "before" dependencies into "after" dependencies */
for (guint j = 0; j < comp->before->len; j++) {
gchar *target_provides = g_ptr_array_index (comp->before, j);
for (guint k = 0; k < self->components->len; k++) {
ComponentData *target = g_ptr_array_index (self->components, k);
if (g_str_equal (target_provides, target->provides)) {
g_ptr_array_insert (target->after, -1, g_strdup (comp->provides));
}
}
}
}
/* sort */
while (self->components->len > 0) {
gboolean made_progress = FALSE;
/* examine each component to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
while (self->components->len > 0) {
ComponentData *comp = g_ptr_array_steal_index (self->components, 0);
guint deps_satisfied = 0;
wp_trace_object (self, "examining: %s", comp->printable_id);
for (guint i = 0; i < comp->after->len; i++) {
const gchar *dep = g_ptr_array_index (comp->after, i);
/* if the dependency is already in the sorted result list or if
it doesn't exist at all, we consider it satisfied */
if (component_exists_in (dep, result) ||
!(component_exists_in (dep, self->components) ||
component_exists_in (dep, remaining))) {
deps_satisfied++;
}
wp_trace_object (self, "depends: %s, satisfied: %u/%u",
dep, deps_satisfied, comp->after->len);
}
if (deps_satisfied == comp->after->len) {
wp_trace_object (self, "sorted: %s", comp->printable_id);
g_ptr_array_add (result, comp);
made_progress = TRUE;
} else {
g_ptr_array_add (remaining, comp);
}
}
if (made_progress) {
/* run again with the remaining components */
g_ptr_array_extend_and_steal (self->components, g_ptr_array_ref (remaining));
}
else if (remaining->len > 0) {
/* if we did not make any progress towards growing the result list,
it means the dependencies cannot be satisfied because of circles */
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"detected circular before/after dependencies in the components!");
return FALSE;
}
}
/* transfer the result array back to self->components */
g_ptr_array_extend_and_steal (self->components, g_steal_pointer (&result));
return TRUE;
}
static gchar *
print_dep_chain (ComponentData *comp)
{
@ -409,9 +531,8 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
if (comp->state == FEATURE_STATE_REQUIRED)
g_ptr_array_add (required_components, component_data_ref (comp));
if (comp->provides)
g_hash_table_insert (self->feat_components, comp->provides,
component_data_ref (comp));
g_hash_table_insert (self->feat_components, comp->provides,
component_data_ref (comp));
}
/* topological sorting based on depth-first search */
@ -424,6 +545,10 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
}
}
/* sort again, taking into account before/after dependencies */
if (!sort_components_before_after (self, error))
return FALSE;
/* terminate the array with NULL */
g_ptr_array_add (self->components, NULL);
@ -598,7 +723,7 @@ ensure_no_media_session_task_idle (GTask * task)
}
static void
ensure_no_media_session (GTask * task, WpCore * core)
ensure_no_media_session (GTask * task, WpCore * core, WpSpaJson * args)
{
WpObjectManager *om = wp_object_manager_new ();
@ -619,7 +744,7 @@ ensure_no_media_session (GTask * task, WpCore * core)
}
static void
load_export_core (GTask * task, WpCore * core)
load_export_core (GTask * task, WpCore * core, WpSpaJson * args)
{
g_autofree gchar *export_core_name = NULL;
g_autoptr (WpCore) export_core = NULL;
@ -641,12 +766,27 @@ load_export_core (GTask * task, WpCore * core)
g_task_return_pointer (task, g_steal_pointer (&export_core), g_object_unref);
}
static void
load_settings_instance (GTask * task, WpCore * core, WpSpaJson * args)
{
g_autofree gchar *metadata_name = NULL;
if (args)
wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL);
wp_info_object (core, "loading settings instance '%s'...",
metadata_name ? metadata_name : "(default: sm-settings)");
WpSettings *settings = wp_settings_new (core, metadata_name);
g_task_return_pointer (task, settings, g_object_unref);
}
static const struct {
const gchar * name;
void (*load) (GTask *, WpCore *);
void (*load) (GTask *, WpCore *, WpSpaJson *);
} builtin_components[] = {
{ "ensure-no-media-session", ensure_no_media_session },
{ "export-core", load_export_core },
{ "settings-instance", load_settings_instance },
};
/*** WpInternalCompLoader ***/
@ -683,10 +823,12 @@ load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
GModule *gmodule;
gpointer module_init;
if (!g_file_test (module_name, G_FILE_TEST_EXISTS))
module_path = g_module_build_path (wp_get_module_dir (), module_name);
else
module_path = g_strdup (module_name);
module_path = wp_base_dirs_find_file (WP_BASE_DIRS_MODULE, NULL, module_name);
if (!module_path) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"Failed to locate module %s", module_name);
return NULL;
}
wp_trace_object (core, "loading %s from %s", module_name, module_path);
@ -708,6 +850,65 @@ load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
return ((WpModuleInitFunc) module_init) (core, args, error);
}
static gboolean
parse_profile_description (WpProperties * profile, WpSpaJson * all_profiles_j,
const gchar * profile_name, GPtrArray * inherited_set, GError ** error)
{
g_autoptr (WpSpaJson) profile_j = NULL;
g_autoptr (WpSpaJson) inherits_j = NULL;
if (!all_profiles_j) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"wireplumber.profiles section does not exist in the configuration");
return FALSE;
}
if (!wp_spa_json_is_object (all_profiles_j)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"wireplumber.profiles section is not an object");
return FALSE;
}
if (!wp_spa_json_object_get (all_profiles_j, profile_name, "J", &profile_j, NULL)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"profile '%s' not found in the configuration", profile_name);
return FALSE;
}
if (!wp_spa_json_is_object (profile_j)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"profile description of '%s' is not an object", profile_name);
return FALSE;
}
/* mark as inherited */
g_ptr_array_add (inherited_set, g_strdup (profile_name));
if (wp_spa_json_object_get (profile_j, "inherits", "J", &inherits_j, NULL) &&
wp_spa_json_is_array (inherits_j)) {
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (inherits_j);
g_auto (GValue) item = G_VALUE_INIT;
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *inherited_j = g_value_get_boxed (&item);
g_autofree gchar *inherited_profile = wp_spa_json_to_string (inherited_j);
/* skip if already inherited - avoid loops */
if (g_ptr_array_find_with_equal_func (inherited_set, inherited_profile,
g_str_equal, NULL))
continue;
if (!parse_profile_description (profile, all_profiles_j, inherited_profile,
inherited_set, error))
return FALSE;
}
}
wp_properties_update_from_json (profile, profile_j);
wp_properties_set (profile, "inherits", NULL);
return TRUE;
}
static gboolean
wp_internal_comp_loader_supports_type (WpComponentLoader * cl,
const gchar * type)
@ -734,25 +935,26 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
if (g_str_equal (type, "profile")) {
/* component name is the profile name;
component list and profile features are loaded from config */
g_autoptr (WpConf) conf = wp_conf_get_instance (core);
g_autoptr (WpSpaJson) profile_json = NULL;
g_autoptr (WpConf) conf = wp_core_get_conf (core);
g_autoptr (GPtrArray) inherited_set = g_ptr_array_new_with_free_func (g_free);
g_autoptr (WpSpaJson) all_profiles_j = NULL;
g_autoptr (GError) error = NULL;
const gchar *profile_name = component;
profile_json =
wp_conf_get_value (conf, "wireplumber.profiles", component, NULL);
if (!profile_json) {
wp_info ("Loading profile '%s'", profile_name);
all_profiles_j = wp_conf_get_section (conf, "wireplumber.profiles");
if (!parse_profile_description (profile, all_profiles_j, profile_name,
inherited_set, &error)) {
g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
g_task_set_source_tag (task, wp_internal_comp_loader_load);
g_task_return_new_error (G_TASK (task), WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"profile '%s' not found in configuration", component);
g_task_return_error (G_TASK (task), g_steal_pointer (&error));
return;
}
wp_properties_update_from_json (profile, profile_json);
components = wp_conf_get_section (conf, "wireplumber.components", NULL);
rules = wp_conf_get_section (conf, "wireplumber.components.rules", NULL);
components = wp_conf_get_section (conf, "wireplumber.components");
rules = wp_conf_get_section (conf, "wireplumber.components.rules");
}
else {
/* component list is retrieved from args; profile features are empty */
@ -796,7 +998,7 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
else if (g_str_equal (type, "built-in")) {
for (guint i = 0; i < G_N_ELEMENTS (builtin_components); i++) {
if (g_str_equal (component, builtin_components[i].name)) {
builtin_components[i].load (task, core);
builtin_components[i].load (task, core, args);
return;
}
}

View file

@ -0,0 +1,158 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
/* SPDX-License-Identifier: MIT */
/*
This is a partial copy of functions from libpipewire's conf.c that is meant to
live here temporarily until pw_context_parse_conf_section() is fixed upstream.
See https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1925
*/
#include <string.h>
#include <spa/utils/result.h>
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <spa/utils/cleanup.h>
#include <pipewire/impl.h>
struct data {
struct pw_context *context;
struct pw_properties *props;
int count;
};
/* context.spa-libs = {
* <factory-name regex> = <library-name>
* }
*/
static int parse_spa_libs(void *user_data, const char *location,
const char *section, const char *str, size_t len)
{
struct data *d = user_data;
struct pw_context *context = d->context;
struct spa_json it[2];
char key[512], value[512];
spa_json_init(&it[0], str, len);
if (spa_json_enter_object(&it[0], &it[1]) < 0) {
pw_log_error("config file error: context.spa-libs is not an object");
return -EINVAL;
}
while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
pw_context_add_spa_lib(context, key, value);
d->count++;
}
}
return 0;
}
static int load_module(struct pw_context *context, const char *key, const char *args, const char *flags)
{
if (pw_context_load_module(context, key, args, NULL) == NULL) {
if (errno == ENOENT && flags && strstr(flags, "ifexists") != NULL) {
pw_log_info("%p: skipping unavailable module %s",
context, key);
} else if (flags == NULL || strstr(flags, "nofail") == NULL) {
pw_log_error("%p: could not load mandatory module \"%s\": %m",
context, key);
return -errno;
} else {
pw_log_info("%p: could not load optional module \"%s\": %m",
context, key);
}
} else {
pw_log_info("%p: loaded module %s", context, key);
}
return 0;
}
/*
* context.modules = [
* { name = <module-name>
* ( args = { <key> = <value> ... } )
* ( flags = [ ( ifexists ) ( nofail ) ]
* ( condition = [ { key = value, .. } .. ] )
* }
* ]
*/
static int parse_modules(void *user_data, const char *location,
const char *section, const char *str, size_t len)
{
struct data *d = user_data;
struct pw_context *context = d->context;
struct spa_json it[4];
char key[512];
int res = 0;
spa_autofree char *s = strndup(str, len);
spa_json_init(&it[0], s, len);
if (spa_json_enter_array(&it[0], &it[1]) < 0) {
pw_log_error("config file error: context.modules is not an array");
return -EINVAL;
}
while (spa_json_enter_object(&it[1], &it[2]) > 0) {
char *name = NULL, *args = NULL, *flags = NULL;
bool have_match = true;
while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
const char *val;
int len;
if ((len = spa_json_next(&it[2], &val)) <= 0)
break;
if (spa_streq(key, "name")) {
name = (char*)val;
spa_json_parse_stringn(val, len, name, len+1);
} else if (spa_streq(key, "args")) {
if (spa_json_is_container(val, len))
len = spa_json_container_len(&it[2], val, len);
args = (char*)val;
spa_json_parse_stringn(val, len, args, len+1);
} else if (spa_streq(key, "flags")) {
if (spa_json_is_container(val, len))
len = spa_json_container_len(&it[2], val, len);
flags = (char*)val;
spa_json_parse_stringn(val, len, flags, len+1);
}
}
if (!have_match)
continue;
if (name != NULL)
res = load_module(context, name, args, flags);
if (res < 0)
break;
d->count++;
}
return res;
}
static int _pw_context_parse_conf_section(struct pw_context *context,
struct pw_properties *conf, const char *section)
{
struct data data = { .context = context };
int res;
if (spa_streq(section, "context.spa-libs"))
res = pw_conf_section_for_each(&conf->dict, section,
parse_spa_libs, &data);
else if (spa_streq(section, "context.modules"))
res = pw_conf_section_for_each(&conf->dict, section,
parse_modules, &data);
else
res = -EINVAL;
return res == 0 ? data.count : res;
}

View file

@ -0,0 +1,26 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
#define __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
#include "client.h"
G_BEGIN_DECLS
typedef struct _WpPermissionManager WpPermissionManager;
void wp_permission_manager_add_client (WpPermissionManager *self,
WpClient *client);
void wp_permission_manager_remove_client (WpPermissionManager *self,
WpClient *client);
G_END_DECLS
#endif

View file

@ -783,7 +783,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
g_clear_pointer (&d->properties, wp_properties_unref);
d->properties = wp_properties_new_wrap_dict (props);
d->properties = wp_properties_new_copy_dict (props);
g_object_notify (G_OBJECT (instance), "properties");
}

515
lib/wp/private/registry.c Normal file
View file

@ -0,0 +1,515 @@
/* WirePlumber
*
* Copyright © 2019-2024 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include "registry.h"
#include "object-manager.h"
#include "log.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-registry")
/*
* WpRegistry:
*
* The registry keeps track of registered objects on the wireplumber core.
* There are 3 kinds of registered objects:
*
* 1) PipeWire global objects, which live in another process.
*
* These objects are represented by a WpGlobal with the
* WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
* the registry_global() event is fired and are removed by
* registry_global_remove(). These objects do not have an associated
* WpProxy, unless there is at least one WpObjectManager that is interested
* in them. In this case, a WpProxy is constructed and it is owned by the
* WpGlobal until the global is removed by the registry_global_remove() event.
*
* 2) PipeWire global objects, which were constructed by this process, either
* by calling into a remove factory (see wp_node_new_from_factory()) or
* by exporting a local object (WpImplSession etc...).
*
* These objects are also represented by a WpGlobal, which may however be
* constructed before they appear on the registry. The associated WpProxy
* calls into wp_registry_prepare_new_global() at the time it receives
* the 'bound' event and creates a global that has the
* WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
* these globals are "owned" by the WpProxy and the WpGlobal has no ref
* on the WpProxy itself. This allows destroying the proxy in client code
* by dropping its last reference.
*
* Normally, these global objects also appear on the pipewire registry. When
* this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
* and that keeps an additional reference on the global (both flags must
* be dropped before the WpGlobal is destroyed).
*
* In some cases, such an object might appear first on the registry and
* then receive the 'bound' event. In order to handle this situation, globals
* are not advertised immediately when they appear on the registry, but
* they are added on a tmp_globals list instead, which is emptied on the
* next core sync. In all cases, the proxy 'bound' and the registry 'global'
* events will be fired in the same sync cycle, so we can catch a late
* 'bound' event and still associate the proxy with the WpGlobal before
* object managers are notified about the existence of this global.
*
* 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
*
* These are local objects that have nothing to do with PipeWire. They do not
* have a global id and they are also not subclasses of WpProxy. The registry
* always owns a reference on them, so that they are kept alive for as long
* as the WpCore is alive.
*/
static void
object_manager_destroyed (gpointer data, GObject * om)
{
WpRegistry *self = data;
g_ptr_array_remove_fast (self->object_managers, om);
}
/* find the subclass of WpPipewireGloabl that can handle
the given pipewire interface type of the given version */
static inline GType
find_proxy_instance_type (const char * type, guint32 version)
{
g_autofree GType *children;
guint n_children;
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
for (guint i = 0; i < n_children; i++) {
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
klass->pw_iface_version == version) {
g_type_class_unref (klass);
return children[i];
}
g_type_class_unref (klass);
}
return WP_TYPE_GLOBAL_PROXY;
}
/* called by the registry when a global appears */
static void
registry_global (void *data, uint32_t id, uint32_t permissions,
const char *type, uint32_t version, const struct spa_dict *props)
{
WpRegistry *self = data;
GType gtype = find_proxy_instance_type (type, version);
wp_debug_object (wp_registry_get_core (self),
"global:%u perm:0x%x type:%s/%u -> %s",
id, permissions, type, version, g_type_name (gtype));
wp_registry_prepare_new_global (self, id, permissions,
WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
}
/* called by the registry when a global is removed */
static void
registry_global_remove (void *data, uint32_t id)
{
WpRegistry *self = data;
WpGlobal *global = NULL;
if (id < self->globals->len)
global = g_ptr_array_index (self->globals, id);
/* if not found, look in the tmp_globals, as it may still not be exposed */
if (!global) {
for (guint i = 0; i < self->tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
if (g->id == id) {
global = g;
break;
}
}
}
g_return_if_fail (global &&
global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
wp_debug_object (wp_registry_get_core (self),
"global removed:%u type:%s", id, g_type_name (global->type));
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = registry_global,
.global_remove = registry_global_remove,
};
void
wp_registry_init (WpRegistry *self)
{
self->globals =
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
self->tmp_globals =
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
self->objects = g_ptr_array_new_with_free_func (g_object_unref);
self->object_managers = g_ptr_array_new ();
self->features = g_ptr_array_new_with_free_func (g_free);
}
void
wp_registry_clear (WpRegistry *self)
{
wp_registry_detach (self);
g_clear_pointer (&self->globals, g_ptr_array_unref);
g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
g_clear_pointer (&self->features, g_ptr_array_unref);
/* remove all the registered objects
this will normally also destroy the object managers, eventually, since
they are normally ref'ed by modules, which are registered objects */
{
g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);
while (objlist->len > 0) {
g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
objlist->len - 1);
wp_registry_notify_rm_object (self, object);
}
}
/* in case there are any object managers left,
remove the weak ref on them and let them be... */
{
g_autoptr (GPtrArray) object_mgrs;
GObject *om;
object_mgrs = g_steal_pointer (&self->object_managers);
while (object_mgrs->len > 0) {
om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
g_object_weak_unref (om, object_manager_destroyed, self);
}
}
}
void
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
{
self->pw_registry = pw_core_get_registry (pw_core,
PW_VERSION_REGISTRY, 0);
pw_registry_add_listener (self->pw_registry, &self->listener,
&registry_events, self);
}
void
wp_registry_detach (WpRegistry *self)
{
if (self->pw_registry) {
spa_hook_remove (&self->listener);
pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
self->pw_registry = NULL;
}
/* remove pipewire globals */
GPtrArray *objlist = self->globals;
while (objlist && objlist->len > 0) {
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
objlist->len - 1);
if (!global)
continue;
if (global->proxy)
wp_registry_notify_rm_object (self, global->proxy);
/* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
by the registry; set registry to NULL to avoid further interference */
global->registry = NULL;
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
/* the registry's ref on global is dropped here; it may still live if
there is a proxy that owns a ref on it, but global->registry is set
to NULL, so there is no further interference */
}
/* drop tmp globals as well */
objlist = self->tmp_globals;
while (objlist && objlist->len > 0) {
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
objlist->len - 1);
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
}
}
static gboolean
expose_tmp_globals (WpCore *core)
{
WpRegistry *self = wp_core_get_registry (core);
g_autoptr (GPtrArray) tmp_globals = NULL;
g_autoptr (GPtrArray) object_managers = NULL;
/* in case the registry was cleared in the meantime... */
if (G_UNLIKELY (!self->tmp_globals))
return G_SOURCE_REMOVE;
/* steal the tmp_globals list and replace it with an empty one */
tmp_globals = self->tmp_globals;
self->tmp_globals =
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
wp_debug_object (core, "exposing %u new globals", tmp_globals->len);
/* traverse in the order that the globals appeared on the registry */
for (guint i = 0; i < tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
/* if global was already removed, drop it */
if (g->flags == 0 || g->id == SPA_ID_INVALID)
continue;
/* if old global is owned by proxy, remove it */
if (self->globals->len > g->id) {
WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
}
g_return_val_if_fail (self->globals->len <= g->id ||
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
/* set the registry, so that wp_global_rm_flag() can work full-scale */
g->registry = self;
/* store it in the globals list */
if (self->globals->len <= g->id)
g_ptr_array_set_size (self->globals, g->id + 1);
g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
}
object_managers = g_ptr_array_copy (self->object_managers,
(GCopyFunc) g_object_ref, NULL);
g_ptr_array_set_free_func (object_managers, g_object_unref);
/* notify object managers */
for (guint i = 0; i < object_managers->len; i++) {
WpObjectManager *om = g_ptr_array_index (object_managers, i);
for (guint i = 0; i < tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
/* if global was already removed, drop it */
if (g->flags == 0 || g->id == SPA_ID_INVALID)
continue;
wp_object_manager_add_global (om, g);
}
wp_object_manager_maybe_objects_changed (om);
}
return G_SOURCE_REMOVE;
}
/*
* \param new_global (out) (transfer full) (optional): the new global
*
* This is normally called up to 2 times in the same sync cycle:
* one from registry_global(), another from the proxy bound event
* Unfortunately the order in which those 2 events happen is specific
* to the implementation of the object, which is why this is implemented
* with a temporary globals list that get exposed later to the object managers
*/
void
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
guint32 permissions, guint32 flag, GType type,
WpGlobalProxy *proxy, const struct spa_dict *props,
WpGlobal ** new_global)
{
g_autoptr (WpGlobal) global = NULL;
WpCore *core = wp_registry_get_core (self);
g_return_if_fail (flag != 0);
for (guint i = 0; i < self->tmp_globals->len; i++) {
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
if (g->id == id) {
global = wp_global_ref (g);
break;
}
}
wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
global ? "reuse" : "new", id, g_type_name (type),
(global && global->proxy) ? global->proxy : proxy);
if (!global) {
global = g_rc_box_new0 (WpGlobal);
global->flags = flag;
global->id = id;
global->type = type;
global->permissions = permissions;
global->properties = props ?
wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
global->proxy = proxy;
g_ptr_array_add (self->tmp_globals, wp_global_ref (global));
/* ensure we have 'object.id' so that we can filter by id on object managers */
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);
/* schedule exposing when adding the first global */
if (self->tmp_globals->len == 1) {
wp_core_idle_add_closure (core, NULL,
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
}
} else {
/* store the most permissive permissions */
if (permissions > global->permissions)
global->permissions = permissions;
global->flags |= flag;
/* store the most deep type (i.e. WpImplNode instead of WpNode),
so that object-manager interests can work more accurately
if the interest is on a specific subclass */
if (g_type_depth (type) > g_type_depth (global->type))
global->type = type;
if (proxy) {
g_return_if_fail (global->proxy == NULL);
global->proxy = proxy;
}
if (props)
wp_properties_update_from_dict (global->properties, props);
}
if (new_global)
*new_global = g_steal_pointer (&global);
}
void
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
{
for (guint i = 0; i < self->object_managers->len; i++) {
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
wp_object_manager_add_object (om, object);
wp_object_manager_maybe_objects_changed (om);
}
}
void
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
{
for (guint i = 0; i < self->object_managers->len; i++) {
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
wp_object_manager_rm_object (om, object);
wp_object_manager_maybe_objects_changed (om);
}
}
void
wp_registry_install_object_manager (WpRegistry * self, WpObjectManager * om)
{
guint i;
g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, self);
g_ptr_array_add (self->object_managers, om);
/* add pre-existing objects to the object manager,
in case it's interested in them */
for (i = 0; i < self->globals->len; i++) {
WpGlobal *g = g_ptr_array_index (self->globals, i);
/* check if null because the globals array can have gaps */
if (g)
wp_object_manager_add_global (om, g);
}
for (i = 0; i < self->objects->len; i++) {
GObject *o = g_ptr_array_index (self->objects, i);
wp_object_manager_add_object (om, o);
}
wp_object_manager_maybe_objects_changed (om);
}
/* WpGlobal */
G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)
void
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
{
WpRegistry *reg = global->registry;
guint32 id = global->id;
/* no flag to remove */
if (!(global->flags & rm_flag))
return;
wp_trace_boxed (WP_TYPE_GLOBAL, global,
"remove global %u flag 0x%x [flags:0x%x, reg:%p]",
id, rm_flag, global->flags, reg);
/* global was owned by the proxy; by removing the flag, we clear out
also the proxy pointer, which is presumably no longer valid and we
notify all listeners that the proxy is gone */
if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
if (reg && global->proxy) {
wp_registry_notify_rm_object (reg, global->proxy);
}
global->proxy = NULL;
}
/* registry removed the global */
else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;
/* destroy the proxy if it exists */
if (global->proxy) {
/* steal the proxy to avoid calling wp_registry_notify_rm_object()
again while removing OWNED_BY_PROXY;
keep a temporary ref so that _deactivate() doesn't crash in case the
pw-proxy-destroyed signal causes external references to be dropped */
g_autoptr (WpGlobalProxy) proxy =
g_object_ref (g_steal_pointer (&global->proxy));
/* notify all listeners that the proxy is gone */
if (reg)
wp_registry_notify_rm_object (reg, proxy);
/* remove FEATURE_BOUND to destroy the underlying pw_proxy */
wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);
/* stop all in-progress activations */
wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");
/* if the proxy is not owning the global, unref it */
if (global->flags == 0)
g_object_unref (proxy);
}
/* It's possible to receive consecutive {add, remove, add} events for the
* same id. Since the WpGlobal might not be destroyed immediately below,
* (e.g. it's in tmp_globals list), we must invalidate the id now, so that
* this WpGlobal is not used in reference to objects added later.
*/
global->id = SPA_ID_INVALID;
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
}
/* drop the registry's ref on global when it does not appear on the registry anymore */
if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
}
}
struct pw_proxy *
wp_global_bind (WpGlobal * global)
{
g_return_val_if_fail (global->proxy, NULL);
g_return_val_if_fail (global->registry, NULL);
WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
return pw_registry_bind (global->registry->pw_registry, global->id,
klass->pw_iface_type, klass->pw_iface_version, 0);
}

View file

@ -46,6 +46,9 @@ void wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
void wp_registry_notify_add_object (WpRegistry * self, gpointer object);
void wp_registry_notify_rm_object (WpRegistry * self, gpointer object);
void wp_registry_install_object_manager (WpRegistry * self,
WpObjectManager * om);
static inline void
wp_registry_mark_feature_provided (WpRegistry * reg, const gchar * feature)
{

218
lib/wp/proc-utils.c Normal file
View file

@ -0,0 +1,218 @@
/* WirePlumber
*
* Copyright © 2024 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <fcntl.h>
#include <stdio.h>
#include <spa/utils/cleanup.h>
#include "log.h"
#include "proc-utils.h"
#define MAX_ARGS 1024
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-proc-utils")
/*! \defgroup wpprocutils Process Utilities */
/*!
* \struct WpProcInfo
*
* WpProcInfo holds information of a process.
*/
struct _WpProcInfo {
grefcount ref;
pid_t pid;
pid_t parent;
gchar *cgroup;
gchar *args[MAX_ARGS];
guint n_args;
};
G_DEFINE_BOXED_TYPE (WpProcInfo, wp_proc_info, wp_proc_info_ref,
wp_proc_info_unref)
/*!
* \brief Increases the reference count of a process information object
* \ingroup wpprocutils
* \param self a process information object
* \returns (transfer full): \a self with an additional reference count on it
*/
WpProcInfo *
wp_proc_info_ref (WpProcInfo * self)
{
g_ref_count_inc (&self->ref);
return self;
}
static void
wp_proc_info_free (WpProcInfo * self)
{
g_clear_pointer (&self->cgroup, g_free);
for (guint i = 0; i < MAX_ARGS; i++)
g_clear_pointer (&self->args[i], free);
g_slice_free (WpProcInfo, self);
}
/*!
* \brief Decreases the reference count on \a self and frees it when the ref
* count reaches zero.
* \ingroup wpprocutils
* \param self (transfer full): a process information object
*/
void
wp_proc_info_unref (WpProcInfo * self)
{
if (g_ref_count_dec (&self->ref))
wp_proc_info_free (self);
}
static WpProcInfo *
wp_proc_info_new (pid_t pid)
{
WpProcInfo *self = g_slice_new0 (WpProcInfo);
g_ref_count_init (&self->ref);
self->pid = pid;
self->parent = 0;
self->cgroup = NULL;
for (guint i = 0; i < MAX_ARGS; i++)
self->args[i] = NULL;
return self;
}
/*!
* \brief Gets the PID of a process information object
* \ingroup wpprocutils
* \param self the process information object
* \returns the PID of the process information object
*/
pid_t
wp_proc_info_get_pid (WpProcInfo * self)
{
return self->pid;
}
/*!
* \brief Gets the parent PID of a process information object
* \ingroup wpprocutils
* \param self the process information object
* \returns the parent PID of the process information object
*/
pid_t
wp_proc_info_get_parent_pid (WpProcInfo * self)
{
return self->parent;
}
/*!
* \brief Gets the number of args of a process information object
* \ingroup wpprocutils
* \param self the process information object
* \returns the number of args of the process information object
*/
guint
wp_proc_info_get_n_args (WpProcInfo * self)
{
return self->n_args;
}
/*!
* \brief Gets the indexed arg of a process information object
* \ingroup wpprocutils
* \param self the process information object
* \param index the index of the arg
* \returns the indexed arg of the process information object
*/
const gchar *
wp_proc_info_get_arg (WpProcInfo * self, guint index)
{
if (index >= self->n_args)
return NULL;
return self->args[index];
}
/*!
* \brief Gets the systemd cgroup of a process information object
* \ingroup wpprocutils
* \param self the process information object
* \returns the systemd cgroup of the process information object
*/
const gchar *
wp_proc_info_get_cgroup (WpProcInfo * self)
{
return self->cgroup;
}
static FILE *
fdopenat (int dirfd, const char *path, int flags, const char *mode, mode_t perm)
{
int fd = openat (dirfd, path, flags, perm);
if (fd >= 0) {
FILE *f = fdopen (fd, mode);
if (f)
return f;
close (fd);
}
return NULL;
}
/*!
* \brief Gets the process information of a given PID
* \ingroup wpprocutils
* \param pid the PID to get the process information from
* \returns: (transfer full): the process information of the given PID
*/
WpProcInfo *
wp_proc_utils_get_proc_info (pid_t pid)
{
WpProcInfo *ret = wp_proc_info_new (pid);
char path [64];
spa_autoclose int base_fd = -1;
FILE *file;
g_autofree gchar *line = NULL;
size_t size = 0;
snprintf (path, sizeof(path), "/proc/%d", pid);
base_fd = open (path,
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY, 0);
if (base_fd < 0) {
wp_info ("Could not open process info directory %s, skipping", path);
return ret;
}
/* Get parent PID */
file = fdopenat (base_fd, "status",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
while (getline (&line, &size, file) > 1)
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
break;
fclose (file);
}
/* Get cgroup */
file = fdopenat (base_fd, "cgroup",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
if (getline (&line, &size, file) > 1)
ret->cgroup = g_strstrip (g_strdup (line));
fclose (file);
}
/* Get args */
file = fdopenat (base_fd, "cmdline",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
ret->args[ret->n_args++] = g_strdup (line);
fclose (file);
}
return ret;
}

54
lib/wp/proc-utils.h Normal file
View file

@ -0,0 +1,54 @@
/* WirePlumber
*
* Copyright © 2024 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PROC_UTILS_H__
#define __WIREPLUMBER_PROC_UTILS_H__
#include <gio/gio.h>
G_BEGIN_DECLS
/*!
* \brief The WpProcInfo GType
* \ingroup wpprocutils
*/
#define WP_TYPE_PROC_INFO (wp_proc_info_get_type ())
WP_API
GType wp_proc_info_get_type (void);
typedef struct _WpProcInfo WpProcInfo;
WP_API
WpProcInfo *wp_proc_info_ref (WpProcInfo * self);
WP_API
void wp_proc_info_unref (WpProcInfo * self);
WP_API
pid_t wp_proc_info_get_pid (WpProcInfo * self);
WP_API
pid_t wp_proc_info_get_parent_pid (WpProcInfo * self);
WP_API
guint wp_proc_info_get_n_args (WpProcInfo * self);
WP_API
const gchar *wp_proc_info_get_arg (WpProcInfo * self, guint index);
WP_API
const gchar *wp_proc_info_get_cgroup (WpProcInfo * self);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpProcInfo, wp_proc_info_unref)
WP_API
WpProcInfo *wp_proc_utils_get_proc_info (pid_t pid);
G_END_DECLS
#endif

View file

@ -203,7 +203,7 @@ wp_properties_new_wrap (const struct pw_properties * props)
* allowing reading & writing properties on that \a props structure through
* the WpProperties API.
*
* In constrast with wp_properties_new_wrap(), this function assumes ownership
* In contrast with wp_properties_new_wrap(), this function assumes ownership
* of the \a props structure, so it will try to free \a props when it is destroyed.
*
* \ingroup wpproperties
@ -1036,7 +1036,7 @@ wp_properties_unref_and_take_pw_properties (WpProperties * self)
* \ingroup wpproperties
* \param self a properties object
* \param other a set of properties to match
* \returns TRUE if all matches were successfull, FALSE if at least one
* \returns TRUE if all matches were successful, FALSE if at least one
* property value did not match
*/
gboolean

View file

@ -82,7 +82,7 @@ wp_pipewire_object_default_init (WpPipewireObjectInterface * iface)
}
/*!
* \brief Retrieves the native infor structure of this object
* \brief Retrieves the native info structure of this object
* (pw_node_info, pw_port_info, etc...)
*
* \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
@ -213,8 +213,8 @@ wp_pipewire_object_get_param_info (WpPipewireObject * self)
* \param id (nullable): the parameter id to enumerate or NULL for all parameters
* \param filter (nullable): a param filter or NULL
* \param cancellable (nullable): a cancellable for the async operation
* \param callback (scope async): a callback to call with the result
* \param user_data (closure): data to pass to \a callback
* \param callback (scope async)(closure user_data): a callback to call with the result
* \param user_data data to pass to \a callback
*/
void
wp_pipewire_object_enum_params (WpPipewireObject * self, const gchar * id,

View file

@ -31,30 +31,28 @@ typedef enum { /*< flags >*/
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG = (1 << 8),
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE = (1 << 9),
/*!
* The minimal feature set for proxies implementing WpPipewireObject.
* This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
*/
WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL =
(WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO),
/*!
* The complete common feature set for proxies implementing
* WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
*/
WP_PIPEWIRE_OBJECT_FEATURES_ALL =
(WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL |
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS |
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT |
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE |
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG |
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE),
WP_PROXY_FEATURE_CUSTOM_START = (1 << 16), /*< skip >*/
} WpProxyFeatures;
/*!
* \brief The minimal feature set for proxies implementing WpPipewireObject.
* This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
* \ingroup wpproxy
*/
#define WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL \
(WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO)
/*!
* \brief The complete common feature set for proxies implementing
* WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
* \ingroup wpproxy
*/
#define WP_PIPEWIRE_OBJECT_FEATURES_ALL \
(WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | \
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS | \
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT | \
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE | \
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG | \
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE)
/*!
* \brief The WpProxy GType
* \ingroup wpproxy

View file

@ -407,7 +407,7 @@ on_session_item_proxy_destroyed_deferred (WpSessionItem * item)
}
/*!
* \brief Helper callback for sub-classes that deffers and unexports
* \brief Helper callback for sub-classes that defers and unexports
* the session item.
*
* Only meant to be used when the pipewire proxy destroyed signal is triggered.

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,88 @@
#include "object.h"
#include "spa-json.h"
#define WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "schema-"
#define WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "persistent-"
G_BEGIN_DECLS
/*!
* \brief The different spec types of a setting
* \ingroup wpsettings
*/
typedef enum {
WP_SETTINGS_SPEC_TYPE_UNKNOWN,
WP_SETTINGS_SPEC_TYPE_BOOL,
WP_SETTINGS_SPEC_TYPE_INT,
WP_SETTINGS_SPEC_TYPE_FLOAT,
WP_SETTINGS_SPEC_TYPE_STRING,
WP_SETTINGS_SPEC_TYPE_ARRAY,
WP_SETTINGS_SPEC_TYPE_OBJECT,
} WpSettingsSpecType;
typedef struct _WpSettingsSpec WpSettingsSpec;
/*!
* \brief The WpSettingsSpec GType
* \ingroup wpsettings
*/
#define WP_TYPE_SETTINGS_SPEC (wp_settings_spec_get_type ())
WP_API
GType wp_settings_spec_get_type (void);
WP_API
WpSettingsSpec *wp_settings_spec_ref (WpSettingsSpec * self);
WP_API
void wp_settings_spec_unref (WpSettingsSpec * self);
WP_API
const gchar * wp_settings_spec_get_name (WpSettingsSpec * self);
WP_API
const gchar * wp_settings_spec_get_description (WpSettingsSpec * self);
WP_API
WpSettingsSpecType wp_settings_spec_get_value_type (WpSettingsSpec * self);
WP_API
WpSpaJson * wp_settings_spec_get_default_value (WpSettingsSpec * self);
WP_API
WpSpaJson * wp_settings_spec_get_min_value (WpSettingsSpec * self);
WP_API
WpSpaJson * wp_settings_spec_get_max_value (WpSettingsSpec * self);
WP_API
gboolean wp_settings_spec_check_value (WpSettingsSpec * self, WpSpaJson *value);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsSpec, wp_settings_spec_unref)
/*!
* \brief The WpSettingsItem GType
* \ingroup wpsettings
*/
#define WP_TYPE_SETTINGS_ITEM (wp_settings_item_get_type ())
WP_API
GType wp_settings_item_get_type (void);
typedef struct _WpSettingsItem WpSettingsItem;
WP_API
WpSettingsItem *wp_settings_item_ref (WpSettingsItem *self);
WP_API
void wp_settings_item_unref (WpSettingsItem *self);
WP_API
const gchar * wp_settings_item_get_key (WpSettingsItem * self);
WP_API
WpSpaJson * wp_settings_item_get_value (WpSettingsItem * self);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsItem, wp_settings_item_unref)
/*!
* \brief Flags to be used as WpObjectFeatures on WpSettings subclasses.
* \ingroup wpsettings
@ -33,8 +113,10 @@ WP_API
G_DECLARE_FINAL_TYPE (WpSettings, wp_settings, WP, SETTINGS, WpObject)
WP_API
WpSettings * wp_settings_get_instance (WpCore * core,
const gchar *metadata_name);
WpSettings * wp_settings_new (WpCore * core, const gchar * metadata_name);
WP_API
WpSettings * wp_settings_find (WpCore * core, const gchar * metadata_name);
/*!
* \brief callback conveying the changed setting and its json value
@ -62,7 +144,38 @@ gboolean wp_settings_unsubscribe (WpSettings *self,
guintptr subscription_id);
WP_API
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *setting);
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *name);
WP_API
WpSpaJson * wp_settings_get_saved (WpSettings *self, const gchar *name);
WP_API
WpSettingsSpec * wp_settings_get_spec (WpSettings *self, const gchar *name);
WP_API
gboolean wp_settings_set (WpSettings *self, const gchar *name,
WpSpaJson *value);
WP_API
gboolean wp_settings_reset (WpSettings *self, const char *name);
WP_API
gboolean wp_settings_save (WpSettings *self, const char *name);
WP_API
gboolean wp_settings_delete (WpSettings *self, const char *name);
WP_API
void wp_settings_reset_all (WpSettings *self);
WP_API
void wp_settings_save_all (WpSettings *self);
WP_API
void wp_settings_delete_all (WpSettings *self);
WP_API
WpIterator * wp_settings_new_iterator (WpSettings *self);
G_END_DECLS

View file

@ -94,8 +94,8 @@ wp_si_adapter_get_ports_format (WpSiAdapter * self, const gchar **mode)
* \param self the session item
* \param format (transfer full) (nullable): the format to be set
* \param mode (nullable): the mode
* \param callback (scope async): the callback to call when the operation is done
* \param data (closure): user data for \a callback
* \param callback (scope async)(closure data): the callback to call when the operation is done
* \param data user data for \a callback
*/
void
wp_si_adapter_set_ports_format (WpSiAdapter * self, WpSpaPod *format,
@ -358,8 +358,8 @@ wp_si_acquisition_default_init (WpSiAcquisitionInterface * iface)
* \param self the session item
* \param acquisitor the link that is trying to acquire a port info item
* \param item the item that is being acquired
* \param callback (scope async): the callback to call when the operation is done
* \param data (closure): user data for \a callback
* \param callback (scope async)(closure data): the callback to call when the operation is done
* \param data user data for \a callback
*/
void
wp_si_acquisition_acquire (WpSiAcquisition * self, WpSiLink * acquisitor,

View file

@ -580,7 +580,7 @@ wp_spa_json_new_object_valist (const gchar *key, const gchar *format,
}
/*!
* \brief Checks wether the spa json is of type null or not
* \brief Checks whether the spa json is of type null or not
*
* \ingroup wpspajson
* \param self the spa json object
@ -593,7 +593,7 @@ wp_spa_json_is_null (WpSpaJson *self)
}
/*!
* \brief Checks wether the spa json is of type boolean or not
* \brief Checks whether the spa json is of type boolean or not
*
* \ingroup wpspajson
* \param self the spa json object
@ -606,7 +606,7 @@ wp_spa_json_is_boolean (WpSpaJson *self)
}
/*!
* \brief Checks wether the spa json is of type int or not
* \brief Checks whether the spa json is of type int or not
*
* \ingroup wpspajson
* \param self the spa json object
@ -619,7 +619,7 @@ wp_spa_json_is_int (WpSpaJson *self)
}
/*!
* \brief Checks wether the spa json is of type float or not
* \brief Checks whether the spa json is of type float or not
*
* \ingroup wpspajson
* \param self the spa json object
@ -632,7 +632,7 @@ wp_spa_json_is_float (WpSpaJson *self)
}
/*!
* \brief Checks wether the spa json is of type string or not
* \brief Checks whether the spa json is of type string or not
*
* \ingroup wpspajson
* \param self the spa json object
@ -1355,6 +1355,39 @@ wp_spa_json_parser_new_object (WpSpaJson *json)
return self;
}
/*!
* \brief Creates a new spa json parser for undefined type of data. The \a json
* object must be valid for the entire life-cycle of the returned parser.
*
* This function allows creating a parser object for any type of spa json and is
* mostly useful to parse non-standard JSON data that should be treated as if it
* were an object or array, but does not start with a '{' or '[' character. Such
* data can be for instance a comma-separated list of single values (array) or
* key-value pairs (object). Such data is also the main configuration file,
* which is an object but doesn't start with a '{' character.
*
* \note If the data is an array or object, the parser will not enter it and the
* only token it will be able to parse is the same \a json object that is passed
* in as an argument. Use wp_spa_json_parser_new_array() or
* wp_spa_json_parser_new_object() to parse arrays or objects.
*
* \ingroup wpspajson
* \param json the spa json to parse
* \returns (transfer full): The new spa json parser
* \since 0.5.0
*/
WpSpaJsonParser *
wp_spa_json_parser_new_undefined (WpSpaJson *json)
{
WpSpaJsonParser *self;
self = g_rc_box_new0 (WpSpaJsonParser);
self->json = json;
self->data[0] = *json->json;
self->pos = &self->data[0];
return self;
}
static int
check_nested_size (struct spa_json *parent, const gchar *data, int size)
{
@ -1491,6 +1524,10 @@ wp_spa_json_parser_get_string (WpSpaJsonParser *self)
/*!
* \brief Gets the spa json value from a spa json parser object
*
* \note the returned spa json object references the original data instead
* of copying it, therefore the original data must be valid for the entire
* life-cycle of the returned object
*
* \ingroup wpspajson
* \param self the spa json parser object
* \returns (transfer full): The spa json value or NULL if it could not be
@ -1500,7 +1537,8 @@ WpSpaJson *
wp_spa_json_parser_get_json (WpSpaJsonParser *self)
{
return wp_spa_json_parser_advance (self) ?
wp_spa_json_new_wrap (&self->curr) : NULL;
wp_spa_json_new_wrap_stringn (self->curr.cur,
self->curr.end - self->curr.cur) : NULL;
}
gboolean

View file

@ -244,6 +244,9 @@ WpSpaJsonParser *wp_spa_json_parser_new_array (WpSpaJson *json);
WP_API
WpSpaJsonParser *wp_spa_json_parser_new_object (WpSpaJson *json);
WP_API
WpSpaJsonParser *wp_spa_json_parser_new_undefined (WpSpaJson *json);
WP_API
gboolean wp_spa_json_parser_get_null (WpSpaJsonParser *self);

Some files were not shown because too many files have changed in this diff Show more