Compare commits

..

121 commits

Author SHA1 Message Date
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
105 changed files with 7091 additions and 1536 deletions

View file

@ -154,13 +154,22 @@ include:
# Fedora also ships that, but without the test plugins that we need... # Fedora also ships that, but without the test plugins that we need...
- git clone --depth=1 --branch="$PIPEWIRE_HEAD" - git clone --depth=1 --branch="$PIPEWIRE_HEAD"
https://gitlab.freedesktop.org/pipewire/pipewire.git https://gitlab.freedesktop.org/pipewire/pipewire.git
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" # Set build options based on PipeWire version
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled - |
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled case "$PIPEWIRE_HEAD" in
-Dvulkan=disabled -Dgstreamer=disabled -Dlibsystemd=disabled 1.0|1.2|1.4)
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled ;;
-Decho-cancel-webrtc=disabled -Dsession-managers=[] *)
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 -Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install - ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
# misc environment only for wireplumber # misc environment only for wireplumber
@ -236,6 +245,9 @@ build_on_fedora_no_docs:
stage: build stage: build
variables: variables:
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false 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: build_on_ubuntu_with_gir:
extends: extends:

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.

144
NEWS.rst
View file

@ -1,6 +1,145 @@
WirePlumber 0.5.12 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: Additions & Enhancements:
- Added mono audio configuration support via ``node.features.audio.mono`` - Added mono audio configuration support via ``node.features.audio.mono``
@ -27,9 +166,6 @@ Fixes:
- Improved device hook documentation and configuration (!736) - Improved device hook documentation and configuration (!736)
Past releases
~~~~~~~~~~~~~
WirePlumber 0.5.11 WirePlumber 0.5.11
.................. ..................

View file

@ -58,3 +58,68 @@ Possible permissions are any combination of:
client can't "see" (i.e. the client doesn't have ``r`` permission on them) client can't "see" (i.e. the client doesn't have ``r`` permission on them)
The special value ``all`` is also supported and it is synonym for ``rwxm`` The special value ``all`` is also supported and it is synonym for ``rwxm``
Permission Managers
-------------------
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``.
Example:
.. code-block::
access.permission-managers = [
{
name = "custom"
default_permissions = "all"
core_permissions = "rx"
rules = [
{
matches = [
{
media.class = "Audio/Source"
}
]
actions = {
set-permissions = "-"
}
}
]
}
]
access.rules = [
{
matches = [
{
application.name = "paplay"
}
]
actions = {
update-props = {
permission_manager_name = "custom"
}
}
}
]
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

@ -140,9 +140,9 @@ Policies
for enabling devices, linking streams, granting permissions to clients, for enabling devices, linking streams, granting permissions to clients,
etc, as appropriate for a desktop system. 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 that group streams based on their ``media.role`` property, and assigns a
priority to each role. Depending on the priority configuration, lower priority to each role. Depending on the priority configuration, lower
priority roles may be corked or ducked when a higher priority role stream priority roles may be corked or ducked when a higher priority role stream

View file

@ -43,6 +43,10 @@ previous section: :ref:`config_configuration_option_types`.
device route (e.g. ALSA PCM sinks). This is used when the route is restored 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. 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) :Default value: ``0.4 ^ 3`` (40% on the cubic scale)
.. describe:: device.routes.default-source-volume .. describe:: device.routes.default-source-volume
@ -51,6 +55,10 @@ previous section: :ref:`config_configuration_option_types`.
device route (e.g. ALSA PCM sources). This is used when the route is restored 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. 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%) :Default value: ``1.0`` (100%)
.. describe:: linking.allow-moving-streams .. describe:: linking.allow-moving-streams

View file

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

View file

@ -23,6 +23,7 @@ C API Documentation
c_api/link_api.rst c_api/link_api.rst
c_api/device_api.rst c_api/device_api.rst
c_api/client_api.rst c_api/client_api.rst
c_api/permission_manager_api.rst
c_api/metadata_api.rst c_api/metadata_api.rst
c_api/spa_device_api.rst c_api/spa_device_api.rst
c_api/impl_node_api.rst c_api/impl_node_api.rst

View file

@ -18,6 +18,7 @@ sphinx_files += files(
'obj_manager_api.rst', 'obj_manager_api.rst',
'object_api.rst', 'object_api.rst',
'pipewire_object_api.rst', 'pipewire_object_api.rst',
'permission_manager_api.rst',
'plugin_api.rst', 'plugin_api.rst',
'port_api.rst', 'port_api.rst',
'properties_api.rst', 'properties_api.rst',

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

@ -42,7 +42,7 @@ assignments:
} }
After that, once the media class of a device node has been select for a 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 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: media class, WirePlumber will select one based on a set of priorities:

View file

@ -9,6 +9,7 @@
#include "client.h" #include "client.h"
#include "log.h" #include "log.h"
#include "private/pipewire-object-mixin.h" #include "private/pipewire-object-mixin.h"
#include "private/permission-manager.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client") WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
@ -25,6 +26,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
struct _WpClient struct _WpClient
{ {
WpGlobalProxy parent; WpGlobalProxy parent;
GWeakRef permission_manager;
}; };
static void wp_client_pw_object_mixin_priv_interface_init ( 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 static void
wp_client_init (WpClient * self) wp_client_init (WpClient * self)
{ {
g_weak_ref_init (&self->permission_manager, NULL);
} }
static void static void
@ -76,11 +79,27 @@ wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
static void static void
wp_client_pw_proxy_destroyed (WpProxy * proxy) 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_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
WP_PROXY_CLASS (wp_client_parent_class)->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 static void
wp_client_class_init (WpClientClass * klass) wp_client_class_init (WpClientClass * klass)
{ {
@ -88,6 +107,7 @@ wp_client_class_init (WpClientClass * klass)
WpObjectClass *wpobject_class = (WpObjectClass *) klass; WpObjectClass *wpobject_class = (WpObjectClass *) klass;
WpProxyClass *proxy_class = (WpProxyClass *) klass; WpProxyClass *proxy_class = (WpProxyClass *) klass;
object_class->finalize = wp_impl_node_finalize;
object_class->get_property = wp_pw_object_mixin_get_property; object_class->get_property = wp_pw_object_mixin_get_property;
wpobject_class->get_supported_features = 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); 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__ #define __WIREPLUMBER_CLIENT_H__
#include "global-proxy.h" #include "global-proxy.h"
#include "permission-manager.h"
G_BEGIN_DECLS G_BEGIN_DECLS
@ -37,6 +38,10 @@ void wp_client_update_permissions_array (WpClient * self,
WP_API WP_API
void wp_client_update_properties (WpClient * self, WpProperties * updates); void wp_client_update_properties (WpClient * self, WpProperties * updates);
WP_API
void wp_client_attach_permission_manager (WpClient *self,
WpPermissionManager *pm);
G_END_DECLS G_END_DECLS
#endif #endif

View file

@ -438,7 +438,7 @@ spa_device_event_object_info (void *data, uint32_t id,
g_autoptr (WpProperties) props = NULL; g_autoptr (WpProperties) props = NULL;
type = spa_debug_type_short_name (info->type); 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", wp_debug_object (self, "object info: id:%u type:%s factory:%s",
id, type, info->factory_name); id, type, info->factory_name);

View file

@ -15,6 +15,161 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher") 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; typedef struct _EventData EventData;
struct _EventData struct _EventData
{ {
@ -49,7 +204,8 @@ struct _WpEventDispatcher
GObject parent; GObject parent;
GWeakRef core; 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 */ GSource *source; /* the event loop source */
GList *events; /* the events stack */ GList *events; /* the events stack */
struct spa_system *system; struct spa_system *system;
@ -104,7 +260,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
/* get the highest priority event */ /* get the highest priority event */
GList *levent = g_list_first (d->events); GList *levent = g_list_first (d->events);
while (levent) { if (levent) {
EventData *event_data = (EventData *) (levent->data); EventData *event_data = (EventData *) (levent->data);
WpEvent *event = event_data->event; WpEvent *event = event_data->event;
GCancellable *cancellable = wp_event_get_cancellable (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 */ /* get the next event */
levent = g_list_first (d->events); 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; return G_SOURCE_CONTINUE;
@ -160,7 +318,9 @@ static void
wp_event_dispatcher_init (WpEventDispatcher * self) wp_event_dispatcher_init (WpEventDispatcher * self)
{ {
g_weak_ref_init (&self->core, NULL); 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)); self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
((WpEventSource *) self->source)->dispatcher = self; ((WpEventSource *) self->source)->dispatcher = self;
@ -184,7 +344,8 @@ wp_event_dispatcher_finalize (GObject * object)
close (self->eventfd); 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_weak_ref_clear (&self->core);
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object); G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
@ -284,6 +445,10 @@ void
wp_event_dispatcher_register_hook (WpEventDispatcher * self, wp_event_dispatcher_register_hook (WpEventDispatcher * self,
WpEventHook * hook) 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_DISPATCHER (self));
g_return_if_fail (WP_IS_EVENT_HOOK (hook)); g_return_if_fail (WP_IS_EVENT_HOOK (hook));
@ -292,7 +457,74 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == NULL); g_return_if_fail (already_registered_dispatcher == NULL);
wp_event_hook_set_dispatcher (hook, self); 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);
} }
/*! /*!
@ -306,6 +538,9 @@ void
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self, wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpEventHook * hook) WpEventHook * hook)
{ {
GHashTableIter iter;
gpointer value;
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self)); g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
g_return_if_fail (WP_IS_EVENT_HOOK (hook)); 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); g_return_if_fail (already_registered_dispatcher == self);
wp_event_hook_set_dispatcher (hook, NULL); 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 * \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 * \ingroup wpeventdispatcher
* *
* \param self the event dispatcher * \param self the event dispatcher
@ -327,7 +580,56 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpIterator * WpIterator *
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self) wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
{ {
GPtrArray *items = GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL); 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); 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); WpEventHook * hook);
WP_API 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 G_END_DECLS

View file

@ -254,6 +254,24 @@ wp_event_hook_run (WpEventHook * self,
callback_data); 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() * \brief Finishes the async operation that was started by wp_event_hook_run()
* *
@ -321,36 +339,63 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
wp_interest_event_hook_get_instance_private (self); wp_interest_event_hook_get_instance_private (self);
g_autoptr (WpProperties) properties = wp_event_get_properties (event); g_autoptr (WpProperties) properties = wp_event_get_properties (event);
g_autoptr (GObject) subject = wp_event_get_subject (event); g_autoptr (GObject) subject = wp_event_get_subject (event);
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
guint i; guint i;
WpObjectInterest *interest = NULL; 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++) { for (i = 0; i < priv->interests->len; i++) {
interest = g_ptr_array_index (priv->interests, i); interest = g_ptr_array_index (priv->interests, i);
match = wp_object_interest_matches_full (interest, if (wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_CHECK_ALL, WP_INTEREST_MATCH_FLAGS_NONE,
gtype, subject, properties, properties); WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
/* 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)
return TRUE; return TRUE;
}
} }
return FALSE; 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 static void
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass) wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
{ {
@ -359,6 +404,8 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
object_class->finalize = wp_interest_event_hook_finalize; object_class->finalize = wp_interest_event_hook_finalize;
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event; 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); gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
/*< private >*/ /*< private >*/
WP_PADDING(5) WP_PADDING(4)
}; };
WP_API WP_API
@ -67,6 +69,9 @@ void wp_event_hook_run (WpEventHook * self,
WpEvent * event, GCancellable * cancellable, WpEvent * event, GCancellable * cancellable,
GAsyncReadyCallback callback, gpointer callback_data); GAsyncReadyCallback callback, gpointer callback_data);
WP_API
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
WP_API WP_API
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res, gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
GError ** error); GError ** error);

View file

@ -17,37 +17,11 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event") 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 struct _WpEvent
{ {
grefcount ref; grefcount ref;
GData *datalist; GData *datalist;
struct spa_list hooks; GPtrArray *hooks;
/* immutable fields */ /* immutable fields */
gint priority; gint priority;
@ -96,7 +70,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
WpEvent * self = g_new0 (WpEvent, 1); WpEvent * self = g_new0 (WpEvent, 1);
g_ref_count_init (&self->ref); g_ref_count_init (&self->ref);
g_datalist_init (&self->datalist); 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->priority = priority;
self->properties = properties ? self->properties = properties ?
@ -155,11 +129,7 @@ wp_event_get_name(WpEvent *self)
static void static void
wp_event_free (WpEvent * self) wp_event_free (WpEvent * self)
{ {
HookData *hook_data; g_clear_pointer (&self->hooks, g_ptr_array_unref);
spa_list_consume (hook_data, &self->hooks, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
g_datalist_clear (&self->datalist); g_datalist_clear (&self->datalist);
g_clear_pointer (&self->properties, wp_properties_unref); g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_object (&self->source); 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); 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 * \brief Collects all the hooks registered in the \a dispatcher that run for
* this \a event * this \a event
@ -355,199 +298,37 @@ hook_exists_in (const gchar *hook_name, struct spa_list *list)
gboolean gboolean
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher) wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
{ {
struct spa_list collected, result, remaining;
g_autoptr (WpIterator) all_hooks = NULL; g_autoptr (WpIterator) all_hooks = NULL;
g_auto (GValue) value = G_VALUE_INIT; 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 (event != NULL, FALSE);
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE); g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
/* hooks already collected */ /* Clear all current hooks */
if (!spa_list_is_empty (&event->hooks)) g_ptr_array_set_size (event->hooks, 0);
return TRUE;
spa_list_init (&collected); /* Get the event type */
spa_list_init (&result); event_type = wp_properties_get (event->properties, "event.type");
spa_list_init (&remaining); wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
event->name, event_type);
/* collect hooks that run for this event */ /* Collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher); all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
event_type);
while (wp_iterator_next (all_hooks, &value)) { while (wp_iterator_next (all_hooks, &value)) {
WpEventHook *hook = g_value_get_object (&value); WpEventHook *hook = g_value_get_object (&value);
if (wp_event_hook_runs_for_event (hook, event)) { if (wp_event_hook_runs_for_event (hook, event)) {
HookData *hook_data = hook_data_new (hook); g_ptr_array_add (event->hooks, g_object_ref (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_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)", wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook)); WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
} }
g_value_unset (&value); g_value_unset (&value);
} }
if (!spa_list_is_empty (&collected)) { return event->hooks->len > 0;
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);
wp_trace_boxed (WP_TYPE_EVENT, event,
"examining: %s", wp_event_hook_get_name (hook_data->hook));
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++;
}
wp_trace_boxed (WP_TYPE_EVENT, event, "depends: %s, satisfied: %u/%u",
dep, deps_satisfied, hook_data->dependencies->len);
}
if (deps_satisfied == hook_data->dependencies->len) {
wp_trace_boxed (WP_TYPE_EVENT, event,
"sorted: "WP_OBJECT_FORMAT"(%s)",
WP_OBJECT_ARGS (hook_data->hook),
wp_event_hook_get_name (hook_data->hook));
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);
} }
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 * \brief Returns an iterator that iterates over all the hooks that were
* collected by wp_event_collect_hooks() * collected by wp_event_collect_hooks()
@ -558,15 +339,8 @@ static const WpIteratorMethods event_hooks_iterator_methods = {
WpIterator * WpIterator *
wp_event_new_hooks_iterator (WpEvent * event) wp_event_new_hooks_iterator (WpEvent * event)
{ {
WpIterator *it = NULL; GPtrArray *hooks;
struct event_hooks_iterator_data *it_data; 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

@ -22,6 +22,7 @@ wp_lib_sources = files(
'object.c', 'object.c',
'object-interest.c', 'object-interest.c',
'object-manager.c', 'object-manager.c',
'permission-manager.c',
'plugin.c', 'plugin.c',
'port.c', 'port.c',
'proc-utils.c', 'proc-utils.c',
@ -69,6 +70,7 @@ wp_lib_headers = files(
'object.h', 'object.h',
'object-interest.h', 'object-interest.h',
'object-manager.h', 'object-manager.h',
'permission-manager.h',
'plugin.h', 'plugin.h',
'port.h', 'port.h',
'proc-utils.h', 'proc-utils.h',

View file

@ -121,6 +121,8 @@ wp_impl_module_finalize (GObject * object)
if (self->props) if (self->props)
wp_properties_unref (self->props); wp_properties_unref (self->props);
G_OBJECT_CLASS (wp_impl_module_parent_class)->finalize (object);
} }
static void static void

View file

@ -881,3 +881,51 @@ wp_object_interest_matches_full (WpObjectInterest * self,
} }
return result; 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

@ -130,6 +130,10 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
WpInterestMatchFlags flags, GType object_type, gpointer object, WpInterestMatchFlags flags, GType object_type, gpointer object,
WpProperties * pw_props, WpProperties * pw_global_props); 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_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
G_END_DECLS G_END_DECLS

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

@ -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_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
g_clear_pointer (&d->properties, wp_properties_unref); 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"); g_object_notify (G_OBJECT (instance), "properties");
} }

View file

@ -6,7 +6,9 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
#include <fcntl.h>
#include <stdio.h> #include <stdio.h>
#include <spa/utils/cleanup.h>
#include "log.h" #include "log.h"
#include "proc-utils.h" #include "proc-utils.h"
@ -145,6 +147,21 @@ wp_proc_info_get_cgroup (WpProcInfo * self)
return self->cgroup; 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 * \brief Gets the process information of a given PID
* \ingroup wpprocutils * \ingroup wpprocutils
@ -155,51 +172,46 @@ WpProcInfo *
wp_proc_utils_get_proc_info (pid_t pid) wp_proc_utils_get_proc_info (pid_t pid)
{ {
WpProcInfo *ret = wp_proc_info_new (pid); WpProcInfo *ret = wp_proc_info_new (pid);
g_autofree gchar *status = NULL; char path [64];
g_autoptr (GError) error = NULL; spa_autoclose int base_fd = -1;
gsize length = 0; 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 */ /* Get parent PID */
{ file = fdopenat (base_fd, "status",
g_autofree gchar *path = g_strdup_printf ("/proc/%d/status", pid); O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (g_file_get_contents (path, &status, &length, &error)) { if (file) {
const gchar *loc = strstr (status, "\nPPid:"); while (getline (&line, &size, file) > 1)
if (loc) { if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
const gint res = sscanf (loc, "\nPPid:%d\n", &ret->parent); break;
if (!res || res == EOF) fclose (file);
wp_warning ("failed to parse status PPID for PID %d", pid);
} else {
wp_warning ("failed to find status parent PID for PID %d", pid);
}
} else {
wp_warning ("failed to get status for PID %d: %s", pid, error->message);
}
} }
/* Get cgroup */ /* Get cgroup */
{ file = fdopenat (base_fd, "cgroup",
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cgroup", pid); O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (g_file_get_contents (path, &ret->cgroup, &length, &error)) { if (file) {
if (length > 0) if (getline (&line, &size, file) > 1)
ret->cgroup [length - 1] = '\0'; /* Remove EOF character */ ret->cgroup = g_strstrip (g_strdup (line));
} else { fclose (file);
wp_warning ("failed to get cgroup for PID %d: %s", pid, error->message);
}
} }
/* Get args */ /* Get args */
{ file = fdopenat (base_fd, "cmdline",
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cmdline", pid); O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
FILE *file = fopen (path, "rb"); if (file) {
if (file) { while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
g_autofree gchar *lineptr = NULL; ret->args[ret->n_args++] = g_strdup (line);
size_t size = 0; fclose (file);
while (getdelim (&lineptr, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
ret->args[ret->n_args++] = g_strdup (lineptr);
fclose (file);
} else {
wp_warning ("failed to get cmdline for PID %d: %m", pid);
}
} }
return ret; return ret;

View file

@ -47,6 +47,7 @@
#include "wpversion.h" #include "wpversion.h"
#include "factory.h" #include "factory.h"
#include "settings.h" #include "settings.h"
#include "permission-manager.h"
G_BEGIN_DECLS G_BEGIN_DECLS

View file

@ -1,5 +1,5 @@
project('wireplumber', ['c'], project('wireplumber', ['c'],
version : '0.5.12', version : '0.5.14',
license : 'MIT', license : 'MIT',
meson_version : '>= 0.59.0', meson_version : '>= 0.59.0',
default_options : [ default_options : [
@ -88,27 +88,24 @@ if build_modules
error('Specified Lua version "' + lua_version_requested + '" not found') error('Specified Lua version "' + lua_version_requested + '" not found')
endif endif
else else
lua_dep = dependency('lua-5.4', required: false) lua_versions = [
if not lua_dep.found() '-5.5', '5.5', '55',
lua_dep = dependency('lua5.4', required: false) '-5.4', '5.4', '54',
endif '-5.3', '5.3', '53',
if not lua_dep.found() ]
lua_dep = dependency('lua54', required: false)
endif foreach v : lua_versions
if not lua_dep.found() lua_dep = dependency('lua' + v, required: false)
lua_dep = dependency('lua-5.3', required: false) if lua_dep.found()
endif break
if not lua_dep.found() endif
lua_dep = dependency('lua5.3', required: false) endforeach
endif
if not lua_dep.found()
lua_dep = dependency('lua53', required: false)
endif
if not lua_dep.found() if not lua_dep.found()
lua_dep = dependency('lua', version: ['>=5.3.0'], required: false) lua_dep = dependency('lua', version: ['>=5.3.0'], required: false)
endif endif
if not lua_dep.found() if not lua_dep.found()
error ('Could not find lua. Lua version 5.4 or 5.3 required') error ('Could not find lua. Lua version 5.5, 5.4 or 5.3 required')
endif endif
endif endif
else else
@ -158,7 +155,25 @@ common_args = [
'-DG_LOG_USE_STRUCTURED', '-DG_LOG_USE_STRUCTURED',
'-DWP_USE_LOCAL_LOG_TOPIC_IN_G_LOG', '-DWP_USE_LOCAL_LOG_TOPIC_IN_G_LOG',
] ]
# Check if SPA_AUDIO_MAX_CHANNELS can be overridden
# (newer headers have #ifndef guards, older ones don't)
check_spa_max_channels_override = '''
#define SPA_AUDIO_MAX_CHANNELS 128u
#include <spa/param/audio/raw.h>
void main() { int x = SPA_AUDIO_MAX_CHANNELS; }
'''
spa_max_channels = 64
if cc.compiles(check_spa_max_channels_override,
dependencies: spa_dep,
args: ['-Werror'],
name: 'SPA_AUDIO_MAX_CHANNELS override')
common_args += ['-DSPA_AUDIO_MAX_CHANNELS=128u']
spa_max_channels = 128
endif
add_project_arguments(common_args, language: 'c') add_project_arguments(common_args, language: 'c')
summary({'SPA_AUDIO_MAX_CHANNELS': spa_max_channels})
i18n_conf = files() i18n_conf = files()
@ -167,7 +182,9 @@ if build_modules
subdir('modules') subdir('modules')
endif endif
subdir('src') subdir('src')
subdir('po') if build_daemon
subdir('po')
endif
subdir('docs') subdir('docs')
if get_option('tests') if get_option('tests')

View file

@ -146,8 +146,8 @@ static int
core_get_properties (lua_State *L) core_get_properties (lua_State *L)
{ {
WpCore * core = get_wp_core (L); WpCore * core = get_wp_core (L);
g_autoptr (WpProperties) p = wp_core_get_properties (core); WpProperties *p = wp_core_get_properties (core);
wplua_properties_to_table (L, p); wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
return 1; return 1;
} }
@ -155,7 +155,7 @@ static int
core_get_info (lua_State *L) core_get_info (lua_State *L)
{ {
WpCore * core = get_wp_core (L); WpCore * core = get_wp_core (L);
g_autoptr (WpProperties) p = wp_core_get_remote_properties (core); WpProperties *p = wp_core_get_remote_properties (core);
lua_newtable (L); lua_newtable (L);
lua_pushinteger (L, wp_core_get_remote_cookie (core)); lua_pushinteger (L, wp_core_get_remote_cookie (core));
@ -168,7 +168,7 @@ core_get_info (lua_State *L)
lua_setfield (L, -2, "host_name"); lua_setfield (L, -2, "host_name");
lua_pushstring (L, wp_core_get_remote_version (core)); lua_pushstring (L, wp_core_get_remote_version (core));
lua_setfield (L, -2, "version"); lua_setfield (L, -2, "version");
wplua_properties_to_table (L, p); wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
lua_setfield (L, -2, "properties"); lua_setfield (L, -2, "properties");
return 1; return 1;
} }
@ -297,8 +297,13 @@ static int
core_update_properties (lua_State *L) core_update_properties (lua_State *L)
{ {
WpCore *core = get_wp_core(L); WpCore *core = get_wp_core(L);
luaL_checktype (L, 1, LUA_TTABLE); WpProperties *props = NULL;
wp_core_update_properties (core, wplua_table_to_properties (L, 1)); if (lua_istable (L, 1))
props = wplua_table_to_properties (L, 1);
else
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
wp_core_update_properties (core, props);
return 0; return 0;
} }
@ -599,6 +604,28 @@ push_wpiterator (lua_State *L, WpIterator *it)
return 2; return 2;
} }
static int
iterator_reset (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
wp_iterator_reset (it);
return 0;
}
static int
iterator_iterate (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
return push_wpiterator (L, wp_iterator_ref (it));
}
static const luaL_Reg iterator_funcs[] = {
{ "next", iterator_next },
{ "reset", iterator_reset },
{ "iterate", iterator_iterate },
{ NULL, NULL }
};
/* Settings WpIterator */ /* Settings WpIterator */
static int static int
@ -837,7 +864,11 @@ object_interest_matches (lua_State *L)
matches = wp_object_interest_matches (interest, wplua_toobject (L, 2)); matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
} }
else if (lua_istable (L, 2)) { else if (lua_istable (L, 2)) {
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2); g_autoptr (WpProperties) props = NULL;
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
matches = wp_object_interest_matches (interest, props); matches = wp_object_interest_matches (interest, props);
} else } else
luaL_argerror (L, 2, "expected GObject or table"); luaL_argerror (L, 2, "expected GObject or table");
@ -997,10 +1028,11 @@ impl_metadata_new (lua_State *L)
const char *name = luaL_checkstring (L, 1); const char *name = luaL_checkstring (L, 1);
WpProperties *properties = NULL; WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) { if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L), WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L),
name, properties); name, properties);
@ -1017,10 +1049,11 @@ device_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1); const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL; WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) { if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L), WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L),
factory, properties); factory, properties);
@ -1037,10 +1070,11 @@ spa_device_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1); const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL; WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) { if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L), WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L),
factory, properties); factory, properties);
@ -1105,10 +1139,11 @@ node_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1); const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL; WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) { if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (
wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
WpNode *d = wp_node_new_from_factory (get_wp_export_core (L), WpNode *d = wp_node_new_from_factory (get_wp_export_core (L),
factory, properties); factory, properties);
@ -1214,10 +1249,11 @@ impl_node_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1); const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL; WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) { if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L), WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L),
factory, properties); factory, properties);
@ -1250,10 +1286,11 @@ link_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1); const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL; WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) { if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties); WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties);
if (l) if (l)
@ -1329,9 +1366,12 @@ static int
client_update_properties (lua_State *L) client_update_properties (lua_State *L)
{ {
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT); WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
WpProperties *properties = NULL;
luaL_checktype (L, 2, LUA_TTABLE); if (lua_istable (L, 2))
WpProperties *properties = wplua_table_to_properties (L, 2); properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
wp_client_update_properties (client, properties); wp_client_update_properties (client, properties);
return 0; return 0;
@ -1348,10 +1388,22 @@ client_send_error (lua_State *L)
return 0; return 0;
} }
static int
client_attach_permission_manager (lua_State *L)
{
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
WpPermissionManager *pm =
wplua_checkobject (L, 2, WP_TYPE_PERMISSION_MANAGER);
wp_client_attach_permission_manager (client, pm);
return 0;
}
static const luaL_Reg client_methods[] = { static const luaL_Reg client_methods[] = {
{ "update_permissions", client_update_permissions }, { "update_permissions", client_update_permissions },
{ "update_properties", client_update_properties }, { "update_properties", client_update_properties },
{ "send_error", client_send_error }, { "send_error", client_send_error },
{ "attach_permission_manager", client_attach_permission_manager },
{ NULL, NULL } { NULL, NULL }
}; };
@ -1391,46 +1443,12 @@ static int
session_item_configure (lua_State *L) session_item_configure (lua_State *L)
{ {
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM); WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
WpProperties *props = wp_properties_new_empty (); WpProperties *props;
/* validate arguments */ if (lua_istable (L, 2))
luaL_checktype (L, 2, LUA_TTABLE); props = wplua_table_to_properties (L, 2);
else
/* build the configuration properties */ props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
lua_pushnil (L);
while (lua_next (L, 2)) {
const gchar *key = NULL;
g_autofree gchar *var = NULL;
switch (lua_type (L, -1)) {
case LUA_TBOOLEAN:
var = g_strdup_printf ("%u", lua_toboolean (L, -1));
break;
case LUA_TNUMBER:
if (lua_isinteger (L, -1))
var = g_strdup_printf ("%lld", lua_tointeger (L, -1));
else
var = g_strdup_printf ("%f", lua_tonumber (L, -1));
break;
case LUA_TSTRING:
var = g_strdup (lua_tostring (L, -1));
break;
case LUA_TUSERDATA: {
GValue *v = lua_touserdata (L, -1);
gpointer p = g_value_peek_pointer (v);
var = g_strdup_printf ("%p", p);
break;
}
default:
luaL_error (L, "configure does not support lua type ",
lua_typename(L, lua_type(L, -1)));
break;
}
key = luaL_tolstring (L, -2, NULL);
wp_properties_set (props, key, var);
lua_pop (L, 2);
}
lua_pushboolean (L, wp_session_item_configure (si, props)); lua_pushboolean (L, wp_session_item_configure (si, props));
return 1; return 1;
@ -1452,12 +1470,23 @@ session_item_remove (lua_State *L)
return 0; return 0;
} }
static int
session_item_get_property (lua_State *L)
{
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_session_item_get_property (si, key);
lua_pushstring (L, val);
return 1;
}
static const luaL_Reg session_item_methods[] = { static const luaL_Reg session_item_methods[] = {
{ "get_associated_proxy", session_item_get_associated_proxy }, { "get_associated_proxy", session_item_get_associated_proxy },
{ "reset", session_item_reset }, { "reset", session_item_reset },
{ "configure", session_item_configure }, { "configure", session_item_configure },
{ "register", session_item_register }, { "register", session_item_register },
{ "remove", session_item_remove }, { "remove", session_item_remove },
{ "get_property", session_item_get_property },
{ NULL, NULL } { NULL, NULL }
}; };
@ -1527,19 +1556,24 @@ on_enum_params_done (WpPipewireObject * pwobj, GAsyncResult * res,
GClosure * closure) GClosure * closure)
{ {
g_autoptr (GError) error = NULL; g_autoptr (GError) error = NULL;
GValue val = G_VALUE_INIT; GValue vals[2] = { G_VALUE_INIT, G_VALUE_INIT };
int n_vals = 0; int n_vals = 1;
WpIterator *it; WpIterator *it;
it = wp_pipewire_object_enum_params_finish (pwobj, res, &error); it = wp_pipewire_object_enum_params_finish (pwobj, res, &error);
g_value_init (&vals[0], WP_TYPE_ITERATOR);
g_value_set_boxed (&vals[0], it);
if (!it) { if (!it) {
g_value_init (&val, G_TYPE_STRING); g_value_init (&vals[1], G_TYPE_STRING);
g_value_set_string (&val, error->message); g_value_set_string (&vals[1], error->message);
n_vals = 1; n_vals = 2;
} }
g_clear_pointer (&it, wp_iterator_unref); g_clear_pointer (&it, wp_iterator_unref);
g_closure_invoke (closure, NULL, n_vals, &val, NULL); g_closure_invoke (closure, NULL, n_vals, vals, NULL);
g_value_unset (&val);
g_value_unset (&vals[0]);
g_value_unset (&vals[1]);
g_closure_invalidate (closure); g_closure_invalidate (closure);
g_closure_unref (closure); g_closure_unref (closure);
} }
@ -1575,11 +1609,22 @@ pipewire_object_set_param (lua_State *L)
return 0; return 0;
} }
static int
pipewire_object_get_property (lua_State *L)
{
WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_pipewire_object_get_property (pwobj, key);
lua_pushstring (L, val);
return 1;
}
static const luaL_Reg pipewire_object_methods[] = { static const luaL_Reg pipewire_object_methods[] = {
{ "enum_params", pipewire_object_enum_params }, { "enum_params", pipewire_object_enum_params },
{ "iterate_params", pipewire_object_iterate_params }, { "iterate_params", pipewire_object_iterate_params },
{ "set_param" , pipewire_object_set_param }, { "set_param" , pipewire_object_set_param },
{ "set_params" , pipewire_object_set_param }, /* deprecated, compat only */ { "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
{ "get_property", pipewire_object_get_property },
{ NULL, NULL } { NULL, NULL }
}; };
@ -1606,9 +1651,14 @@ static int
state_save (lua_State *L) state_save (lua_State *L)
{ {
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE); WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
luaL_checktype (L, 2, LUA_TTABLE); g_autoptr (WpProperties) props = NULL;
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
g_autoptr (GError) error = NULL; g_autoptr (GError) error = NULL;
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
gboolean saved = wp_state_save (state, props, &error); gboolean saved = wp_state_save (state, props, &error);
lua_pushboolean (L, saved); lua_pushboolean (L, saved);
lua_pushstring (L, error ? error->message : ""); lua_pushstring (L, error ? error->message : "");
@ -1619,8 +1669,13 @@ static int
state_save_after_timeout (lua_State *L) state_save_after_timeout (lua_State *L)
{ {
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE); WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
luaL_checktype (L, 2, LUA_TTABLE); g_autoptr (WpProperties) props = NULL;
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
wp_state_save_after_timeout (state, get_wp_core (L), props); wp_state_save_after_timeout (state, get_wp_core (L), props);
return 0; return 0;
} }
@ -1629,8 +1684,8 @@ static int
state_load (lua_State *L) state_load (lua_State *L)
{ {
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE); WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
g_autoptr (WpProperties) props = wp_state_load (state); WpProperties *props = wp_state_load (state);
wplua_properties_to_table (L, props); wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1; return 1;
} }
@ -1648,17 +1703,18 @@ static int
impl_module_new (lua_State *L) impl_module_new (lua_State *L)
{ {
const char *name, *args = NULL; const char *name, *args = NULL;
WpProperties *properties = NULL; g_autoptr (WpProperties) properties = NULL;
name = luaL_checkstring (L, 1); name = luaL_checkstring (L, 1);
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
args = luaL_checkstring (L, 2); args = luaL_checkstring (L, 2);
if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) { if (lua_istable (L, 3))
luaL_checktype (L, 3, LUA_TTABLE);
properties = wplua_table_to_properties (L, 3); properties = wplua_table_to_properties (L, 3);
} else if (!lua_isnone (L, 3) && !lua_isnil (L, 3))
properties = wp_properties_ref (wplua_checkboxed (L, 3,
WP_TYPE_PROPERTIES));
WpImplModule *m = wp_impl_module_load (get_wp_export_core (L), WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
name, args, properties); name, args, properties);
@ -1681,9 +1737,10 @@ conf_new (lua_State *L)
WpProperties *p = NULL; WpProperties *p = NULL;
WpConf *conf = NULL; WpConf *conf = NULL;
if (lua_istable (L, 2)) { if (lua_istable (L, 2))
p = wplua_table_to_properties (L, 2); p = wplua_table_to_properties (L, 2);
} else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
p = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
conf = wp_conf_new (path, p); conf = wp_conf_new (path, p);
if (conf) { if (conf) {
@ -1721,7 +1778,7 @@ conf_get_section_as_properties (lua_State *L)
const char *section = NULL; const char *section = NULL;
g_autoptr (WpConf) conf = NULL; g_autoptr (WpConf) conf = NULL;
g_autoptr (WpSpaJson) s = NULL; g_autoptr (WpSpaJson) s = NULL;
g_autoptr (WpProperties) props = NULL; WpProperties *props = NULL;
int argi = 1; int argi = 1;
/* check if called as method on object */ /* check if called as method on object */
@ -1736,6 +1793,8 @@ conf_get_section_as_properties (lua_State *L)
if (lua_istable (L, argi)) if (lua_istable (L, argi))
props = wplua_table_to_properties (L, argi); props = wplua_table_to_properties (L, argi);
else if (!lua_isnone (L, argi) && !lua_isnil (L, argi))
props = wp_properties_ref (wplua_checkboxed (L, argi, WP_TYPE_PROPERTIES));
else else
props = wp_properties_new_empty (); props = wp_properties_new_empty ();
@ -1744,7 +1803,7 @@ conf_get_section_as_properties (lua_State *L)
if (s && wp_spa_json_is_object (s)) if (s && wp_spa_json_is_object (s))
wp_properties_update_from_json (props, s); wp_properties_update_from_json (props, s);
} }
wplua_properties_to_table (L, props); wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1; return 1;
} }
@ -1901,10 +1960,12 @@ json_utils_match_rules (lua_State *L)
gboolean res; gboolean res;
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
luaL_checktype (L, 2, LUA_TTABLE);
luaL_checktype (L, 3, LUA_TFUNCTION); luaL_checktype (L, 3, LUA_TFUNCTION);
properties = wplua_table_to_properties (L, 2); if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb, res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
L, &error); L, &error);
@ -1920,17 +1981,21 @@ json_utils_match_rules (lua_State *L)
static int static int
json_utils_match_rules_update_properties (lua_State *L) json_utils_match_rules_update_properties (lua_State *L)
{ {
g_autoptr (WpProperties) properties = NULL; WpProperties *properties = NULL;
WpSpaJson *json; WpSpaJson *json;
int count; int count;
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
count = wp_json_utils_match_rules_update_properties (json, properties); count = wp_json_utils_match_rules_update_properties (json, properties);
wplua_properties_to_table (L, properties); wplua_pushboxed (L, WP_TYPE_PROPERTIES, properties);
lua_pushinteger (L, count); lua_pushinteger (L, count);
return 2; return 2;
} }
@ -2012,6 +2077,108 @@ static const luaL_Reg proc_utils_funcs[] = {
{ NULL, NULL } { NULL, NULL }
}; };
/* Properties */
static int
properties_new (lua_State *L)
{
WpProperties *props;
if (lua_istable (L, 1))
props = wplua_table_to_properties (L, 1);
else if (!lua_isnone (L, 1) && !lua_isnil (L, 1))
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
else
props = wp_properties_new_empty ();
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1;
}
static int
properties_get_boolean (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_properties_get (props, key);
if (val)
lua_pushboolean (L, spa_atob (val));
else
lua_pushnil (L);
return 1;
}
static int
properties_get_int (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_properties_get (props, key);
if (val) {
gint64 int_val = 0;
if (spa_atoi64 (val, &int_val, 10))
lua_pushinteger (L, int_val);
else
lua_pushnil (L);
} else {
lua_pushnil (L);
}
return 1;
}
static int
properties_get_float (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_properties_get (props, key);
if (val) {
double d_val = 0;
if (spa_atod (val, &d_val))
lua_pushnumber (L, d_val);
else
lua_pushnil (L);
} else {
lua_pushnil (L);
}
return 1;
}
static int
properties_get_count (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
lua_pushinteger (L, wp_properties_get_count (props));
return 1;
}
static int
properties_copy (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
WpProperties *copy = wp_properties_copy (props);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, copy);
return 1;
}
static int
properties_parse (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
wplua_properties_to_table (L, props);
return 1;
}
static const luaL_Reg properties_funcs[] = {
{ "get_boolean", properties_get_boolean },
{ "get_int", properties_get_int },
{ "get_float", properties_get_float },
{ "get_count", properties_get_count },
{ "copy", properties_copy },
{ "parse", properties_parse },
{ NULL, NULL }
};
/* WpSettings */ /* WpSettings */
static int static int
@ -2305,8 +2472,8 @@ static int
event_get_properties (lua_State *L) event_get_properties (lua_State *L)
{ {
WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT); WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
g_autoptr (WpProperties) props = wp_event_get_properties (event); WpProperties *props = wp_event_get_properties (event);
wplua_properties_to_table (L, props); wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1; return 1;
} }
@ -2428,10 +2595,11 @@ event_dispatcher_push_event (lua_State *L)
lua_pop (L, 1); lua_pop (L, 1);
lua_pushliteral (L, "properties"); lua_pushliteral (L, "properties");
if (lua_gettable (L, 1) != LUA_TNIL) { if (lua_istable (L, -1))
luaL_checktype (L, -1, LUA_TTABLE);
properties = wplua_table_to_properties (L, -1); properties = wplua_table_to_properties (L, -1);
} else if (!lua_isnil (L, -1) && !lua_isnone (L, -1) && !lua_isstring (L, -1))
properties = wp_properties_ref (
wplua_checkboxed (L, -1, WP_TYPE_PROPERTIES));
lua_pop (L, 1); lua_pop (L, 1);
lua_pushliteral (L, "source"); lua_pushliteral (L, "source");
@ -2461,6 +2629,132 @@ static const luaL_Reg event_dispatcher_funcs[] = {
{ NULL, NULL } { NULL, NULL }
}; };
/* WpPermissionManager */
static int
permission_manager_new (lua_State *L)
{
WpPermissionManager *pm = wp_permission_manager_new (get_wp_core (L));
wplua_pushobject (L, pm);
return 1;
}
static int
permission_manager_set_default_permissions (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1,
WP_TYPE_PERMISSION_MANAGER);
guint32 perms = PW_PERM_ALL;
if (lua_isinteger (L, 2)) {
perms = luaL_checkinteger (L, 2);
} else if (lua_isstring (L, 2)) {
const gchar *perms_str = luaL_checkstring (L, 2);
if (!client_parse_permissions (perms_str, &perms))
luaL_error (L, "invalid permission string: '%s'", perms_str);
} else {
luaL_error (L, "invalid permission argument");
}
wp_permission_manager_set_default_permissions (pm, perms);
return 0;
}
static int
permission_manager_set_core_permissions (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1,
WP_TYPE_PERMISSION_MANAGER);
guint32 perms = PW_PERM_ALL;
if (lua_isinteger (L, 2)) {
perms = luaL_checkinteger (L, 2);
} else if (lua_isstring (L, 2)) {
const gchar *perms_str = luaL_checkstring (L, 2);
if (!client_parse_permissions (perms_str, &perms))
luaL_error (L, "invalid permission string: '%s'", perms_str);
} else {
luaL_error (L, "invalid permission argument");
}
wp_permission_manager_set_core_permissions (pm, perms);
return 0;
}
static int
permission_manager_add_interest_match (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1,
WP_TYPE_PERMISSION_MANAGER);
GClosure * closure = wplua_function_to_closure (L, 2);
WpObjectInterest *interest = wplua_checkboxed (L, 3, WP_TYPE_OBJECT_INTEREST);
guint32 id;
id = wp_permission_manager_add_interest_match_closure (pm, closure,
wp_object_interest_ref (interest));
lua_pushinteger (L, id);
return 1;
}
static int
permission_manager_add_interest_match_simple (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1,
WP_TYPE_PERMISSION_MANAGER);
guint32 perms = luaL_checkinteger (L, 2);
WpObjectInterest *interest = wplua_checkboxed (L, 3, WP_TYPE_OBJECT_INTEREST);
guint32 id;
id = wp_permission_manager_add_interest_match_simple (pm, perms,
wp_object_interest_ref (interest));
lua_pushinteger (L, id);
return 1;
}
static int
permission_manager_add_rules_match (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1, WP_TYPE_PERMISSION_MANAGER);
WpSpaJson *rules = wplua_checkboxed (L, 2, WP_TYPE_SPA_JSON);
guint32 id;
id = wp_permission_manager_add_rules_match (pm, wp_spa_json_ref (rules));
lua_pushinteger (L, id);
return 1;
}
static int
permission_manager_remove_match (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1,
WP_TYPE_PERMISSION_MANAGER);
guint interest_id = luaL_checkinteger (L, 2);
wp_permission_manager_remove_match (pm, interest_id);
return 0;
}
static int
permission_manager_update_permissions (lua_State *L)
{
WpPermissionManager *pm = wplua_checkobject (L, 1,
WP_TYPE_PERMISSION_MANAGER);
wp_permission_manager_update_permissions (pm);
return 0;
}
static const luaL_Reg permission_manager_funcs[] = {
{ "set_default_permissions", permission_manager_set_default_permissions },
{ "set_core_permissions", permission_manager_set_core_permissions },
{ "add_interest_match", permission_manager_add_interest_match },
{ "add_interest_match_simple", permission_manager_add_interest_match_simple },
{ "add_rules_match", permission_manager_add_rules_match },
{ "remove_match", permission_manager_remove_match },
{ "update_permissions", permission_manager_update_permissions },
{ NULL, NULL }
};
/* WpEventHook */ /* WpEventHook */
static int static int
@ -2984,6 +3278,12 @@ wp_lua_scripting_api_init (lua_State *L)
conf_new, conf_methods); conf_new, conf_methods);
wplua_register_type_methods (L, WP_TYPE_PROC_INFO, wplua_register_type_methods (L, WP_TYPE_PROC_INFO,
NULL, proc_info_funcs); NULL, proc_info_funcs);
wplua_register_type_methods (L, WP_TYPE_ITERATOR,
NULL, iterator_funcs);
wplua_register_type_methods (L, WP_TYPE_PROPERTIES,
properties_new, properties_funcs);
wplua_register_type_methods (L, WP_TYPE_PERMISSION_MANAGER,
permission_manager_new, permission_manager_funcs);
if (!wplua_load_uri (L, URI_API, &error) || if (!wplua_load_uri (L, URI_API, &error) ||
!wplua_pcall (L, 0, 0, &error)) { !wplua_pcall (L, 0, 0, &error)) {

View file

@ -184,6 +184,28 @@ local Feature = {
}, },
} }
PERM_R_VAL = 0400
PERM_W_VAL = 0200
PERM_X_VAL = 0100
PERM_M_VAL = 0010
PERM_L_VAL = 0020
local Perm = {
NONE = 0,
R = PERM_R_VAL,
W = PERM_W_VAL,
X = PERM_X_VAL,
M = PERM_M_VAL,
L = PERM_L_VAL,
RW = (PERM_R_VAL | PERM_W_VAL),
RX = (PERM_R_VAL | PERM_X_VAL),
WX = (PERM_W_VAL | PERM_X_VAL),
RWX = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL),
RWXM = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL | PERM_M_VAL),
RWXML = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL | PERM_M_VAL | PERM_L_VAL),
ALL = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL | PERM_M_VAL),
}
-- Allow calling Conf() to instantiate a new WpConf -- Allow calling Conf() to instantiate a new WpConf
WpConf["__new"] = WpConf_new WpConf["__new"] = WpConf_new
@ -192,6 +214,7 @@ SANDBOX_EXPORT = {
Id = Id, Id = Id,
Features = Features, Features = Features,
Feature = Feature, Feature = Feature,
Perm = Perm,
GLib = GLib, GLib = GLib,
I18n = I18n, I18n = I18n,
Log = WpLog, Log = WpLog,
@ -217,6 +240,8 @@ SANDBOX_EXPORT = {
Conf = WpConf, Conf = WpConf,
JsonUtils = JsonUtils, JsonUtils = JsonUtils,
ProcUtils = ProcUtils, ProcUtils = ProcUtils,
Properties = WpProperties_new,
PermissionManager = WpPermissionManager_new,
SimpleEventHook = WpSimpleEventHook_new, SimpleEventHook = WpSimpleEventHook_new,
AsyncEventHook = WpAsyncEventHook_new, AsyncEventHook = WpAsyncEventHook_new,
} }

View file

@ -300,40 +300,54 @@ spa_json_object_new (lua_State *L)
{ {
g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object (); g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object ();
luaL_checktype (L, 1, LUA_TTABLE); if (lua_istable (L, 1)) {
luaL_checktype (L, 1, LUA_TTABLE);
lua_pushnil (L); lua_pushnil (L);
while (lua_next (L, -2)) { while (lua_next (L, -2)) {
/* We only add table values with string keys */ /* We only add table values with string keys */
if (lua_type (L, -2) == LUA_TSTRING) { if (lua_type (L, -2) == LUA_TSTRING) {
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2)); wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
switch (lua_type (L, -1)) { switch (lua_type (L, -1)) {
case LUA_TBOOLEAN: case LUA_TBOOLEAN:
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1)); wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
break; break;
case LUA_TNUMBER: case LUA_TNUMBER:
if (lua_isinteger (L, -1)) if (lua_isinteger (L, -1))
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1)); wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
else else
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1)); wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
break; break;
case LUA_TSTRING: case LUA_TSTRING:
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1)); wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
break; break;
case LUA_TUSERDATA: { case LUA_TUSERDATA: {
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON); WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
wp_spa_json_builder_add_json (builder, json); wp_spa_json_builder_add_json (builder, json);
break; break;
}
default:
luaL_error (L, "Json does not support lua type %s",
lua_typename(L, lua_type(L, -1)));
break;
} }
default:
luaL_error (L, "Json does not support lua type %s",
lua_typename(L, lua_type(L, -1)));
break;
} }
}
lua_pop (L, 1); lua_pop (L, 1);
}
} else {
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
for (it = wp_properties_new_iterator (props); wp_iterator_next (it, &item);
g_value_unset (&item)) {
WpPropertiesItem *pi = g_value_get_boxed (&item);
const gchar *key = wp_properties_item_get_key (pi);
const gchar *value = wp_properties_item_get_value (pi);
wp_spa_json_builder_add_property (builder, key);
wp_spa_json_builder_add_string (builder, value);
}
} }
wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder)); wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));

View file

@ -29,8 +29,9 @@ _wplua_gboxed___index (lua_State *L)
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed"); GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
luaL_argcheck (L, obj_v != NULL, 1, luaL_argcheck (L, obj_v != NULL, 1,
"expected userdata storing GValue<GBoxed>"); "expected userdata storing GValue<GBoxed>");
const gchar *key = luaL_checkstring (L, 2); const gchar *key = luaL_tolstring (L, 2, NULL);
GType type = G_VALUE_TYPE (obj_v); GType type = G_VALUE_TYPE (obj_v);
GType boxed_type = type;
lua_CFunction func = NULL; lua_CFunction func = NULL;
GHashTable *vtables; GHashTable *vtables;
@ -53,6 +54,104 @@ _wplua_gboxed___index (lua_State *L)
lua_pushcfunction (L, func); lua_pushcfunction (L, func);
return 1; return 1;
} }
/* If WpProperties type, just return the property value for that key */
if (boxed_type == WP_TYPE_PROPERTIES) {
WpProperties * props = g_value_get_boxed (obj_v);
const gchar *val = wp_properties_get (props, key);
lua_pushstring (L, val);
return 1;
}
return 0;
}
static int
_wplua_gboxed___newindex (lua_State *L)
{
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
luaL_argcheck (L, obj_v != NULL, 1,
"expected userdata storing GValue<GBoxed>");
const gchar *key = luaL_tolstring (L, 2, NULL);
GType type = G_VALUE_TYPE (obj_v);
/* Set property value */
if (type == WP_TYPE_PROPERTIES) {
WpProperties * props = g_value_dup_boxed (obj_v);
g_autofree gchar *val = NULL;
luaL_checkany (L, 3);
switch (lua_type (L, 3)) {
case LUA_TNIL:
break;
case LUA_TUSERDATA: {
if (wplua_gvalue_userdata_type (L, 3) != G_TYPE_INVALID) {
GValue *v = lua_touserdata (L, 3);
gpointer p = g_value_peek_pointer (v);
val = g_strdup_printf ("%p", p);
break;
} else {
val = g_strdup (luaL_tolstring (L, 3, NULL));
break;
}
}
default:
val = g_strdup (luaL_tolstring (L, 3, NULL));
break;
}
props = wp_properties_ensure_unique_owner (props);
wp_properties_set (props, key, val);
g_value_take_boxed (obj_v, props);
} else {
luaL_error (L, "cannot assign property '%s' to boxed type %s",
key, g_type_name (type));
}
return 0;
}
static int
properties_iterator_next (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
g_auto (GValue) item = G_VALUE_INIT;
if (wp_iterator_next (it, &item)) {
WpPropertiesItem *si = g_value_get_boxed (&item);
const gchar *k = wp_properties_item_get_key (si);
const gchar *v = wp_properties_item_get_value (si);
lua_pushstring (L, k);
lua_pushstring (L, v);
return 2;
} else {
lua_pushnil (L);
lua_pushnil (L);
return 2;
}
}
static int
push_properties_wpiterator (lua_State *L, WpIterator *it)
{
lua_pushcfunction (L, properties_iterator_next);
wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
return 2;
}
static int
_wplua_gboxed___pairs (lua_State *L)
{
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
luaL_argcheck (L, obj_v != NULL, 1,
"expected userdata storing GValue<GBoxed>");
GType type = G_VALUE_TYPE (obj_v);
if (type == WP_TYPE_PROPERTIES) {
WpProperties * props = g_value_get_boxed (obj_v);
WpIterator *it = wp_properties_new_iterator (props);
return push_properties_wpiterator (L, it);
} else {
luaL_error (L, "cannot do pairs of boxed type %s", g_type_name (type));
}
return 0; return 0;
} }
@ -69,6 +168,8 @@ _wplua_init_gboxed (lua_State *L)
{ "__gc", _wplua_gvalue_userdata___gc }, { "__gc", _wplua_gvalue_userdata___gc },
{ "__eq", _wplua_gboxed___eq }, { "__eq", _wplua_gboxed___eq },
{ "__index", _wplua_gboxed___index }, { "__index", _wplua_gboxed___index },
{ "__newindex", _wplua_gboxed___newindex },
{ "__pairs", _wplua_gboxed___pairs },
{ NULL, NULL } { NULL, NULL }
}; };

View file

@ -14,7 +14,6 @@ WpProperties *
wplua_table_to_properties (lua_State *L, int idx) wplua_table_to_properties (lua_State *L, int idx)
{ {
WpProperties *p = wp_properties_new_empty (); WpProperties *p = wp_properties_new_empty ();
const gchar *key, *value;
int table = lua_absindex (L, idx); int table = lua_absindex (L, idx);
if (lua_type (L, table) != LUA_TTABLE) { if (lua_type (L, table) != LUA_TTABLE) {
@ -24,11 +23,34 @@ wplua_table_to_properties (lua_State *L, int idx)
lua_pushnil(L); lua_pushnil(L);
while (lua_next (L, table) != 0) { while (lua_next (L, table) != 0) {
const gchar *key = luaL_tolstring (L, -2, NULL);
g_autofree gchar *value = NULL;
/* copy key & value to convert them to string */ /* copy key & value to convert them to string */
key = luaL_tolstring (L, -2, NULL); luaL_checkany (L, -2);
value = luaL_tolstring (L, -2, NULL); switch (lua_type (L, -2)) {
case LUA_TNIL:
lua_pop (L, 2);
break;
case LUA_TUSERDATA: {
if (wplua_gvalue_userdata_type(L, -2) != G_TYPE_INVALID) {
GValue *v = lua_touserdata (L, -2);
gpointer p = g_value_peek_pointer (v);
value = g_strdup_printf ("%p", p);
lua_pop (L, 2);
} else {
value = g_strdup (luaL_tolstring (L, -2, NULL));
lua_pop (L, 3);
}
break;
}
default:
value = g_strdup (luaL_tolstring (L, -2, NULL));
lua_pop (L, 3);
break;
}
wp_properties_set (p, key, value); wp_properties_set (p, key, value);
lua_pop (L, 3);
} }
/* sort, because the lua table has a random order and it's too messy to read */ /* sort, because the lua table has a random order and it's too messy to read */
@ -313,10 +335,7 @@ wplua_gvalue_to_lua (lua_State *L, const GValue *v)
lua_pushlightuserdata (L, g_value_get_pointer (v)); lua_pushlightuserdata (L, g_value_get_pointer (v));
break; break;
case G_TYPE_BOXED: case G_TYPE_BOXED:
if (G_VALUE_TYPE (v) == WP_TYPE_PROPERTIES) wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
wplua_properties_to_table (L, g_value_get_boxed (v));
else
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
break; break;
case G_TYPE_OBJECT: case G_TYPE_OBJECT:
case G_TYPE_INTERFACE: { case G_TYPE_INTERFACE: {

View file

@ -103,14 +103,15 @@ static void
bind_call (GObject * obj, GAsyncResult * res, gpointer data) bind_call (GObject * obj, GAsyncResult * res, gpointer data)
{ {
WpModemManager *wpmm = WP_MODEM_MANAGER (data); WpModemManager *wpmm = WP_MODEM_MANAGER (data);
GError *err = NULL; g_autoptr (GError) err = NULL;
GDBusProxy *call; GDBusProxy *call;
GVariant *prop; g_autoptr (GVariant) prop = NULL;
gint init_state; gint init_state;
call = g_dbus_proxy_new_finish (res, &err); call = g_dbus_proxy_new_finish (res, &err);
if (call == NULL) { if (call == NULL) {
wp_warning_object (wpmm, "Failed to get call"); g_prefix_error (&err, "Failed to get call: ");
wp_warning_object (wpmm, "%s", err->message);
return; return;
} }
@ -122,8 +123,6 @@ bind_call (GObject * obj, GAsyncResult * res, gpointer data)
if (is_active_state (init_state)) if (is_active_state (init_state))
active_calls_inc (wpmm); active_calls_inc (wpmm);
g_variant_unref (prop);
} }
wpmm->calls = g_list_prepend (wpmm->calls, call); wpmm->calls = g_list_prepend (wpmm->calls, call);
@ -165,7 +164,7 @@ on_voice_signal (GDBusProxy * iface,
g_object_get (wpmm->dbus, "connection", &conn, NULL); g_object_get (wpmm->dbus, "connection", &conn, NULL);
if (!g_strcmp0 (signal, "CallAdded")) { if (!g_strcmp0 (signal, "CallAdded")) {
g_variant_get (params, "(o)", &path); g_variant_get (params, "(&o)", &path);
g_dbus_proxy_new (conn, g_dbus_proxy_new (conn,
G_DBUS_PROXY_FLAGS_NONE, G_DBUS_PROXY_FLAGS_NONE,
NULL, NULL,
@ -175,9 +174,8 @@ on_voice_signal (GDBusProxy * iface,
NULL, NULL,
bind_call, bind_call,
wpmm); wpmm);
g_free (path);
} else if (!g_strcmp0 (signal, "CallDeleted")) { } else if (!g_strcmp0 (signal, "CallDeleted")) {
g_variant_get (params, "(o)", &path); g_variant_get (params, "(&o)", &path);
// The user shouldn't have hundreds of calls, so just linear search. // The user shouldn't have hundreds of calls, so just linear search.
deleted = g_list_find_custom (wpmm->calls, path, match_call_path); deleted = g_list_find_custom (wpmm->calls, path, match_call_path);
@ -185,8 +183,6 @@ on_voice_signal (GDBusProxy * iface,
g_object_unref (deleted->data); g_object_unref (deleted->data);
wpmm->calls = g_list_delete_link (wpmm->calls, deleted); wpmm->calls = g_list_delete_link (wpmm->calls, deleted);
} }
g_free (path);
} }
} }
@ -196,22 +192,23 @@ list_calls_done (GObject * obj,
gpointer data) gpointer data)
{ {
WpModemManager *wpmm = WP_MODEM_MANAGER (data); WpModemManager *wpmm = WP_MODEM_MANAGER (data);
GVariant *params; g_autoptr (GVariant) params = NULL;
GVariantIter *calls; g_autoptr (GVariantIter) calls = NULL;
gchar *path; gchar *path;
GError *err = NULL; g_autoptr (GError) err = NULL;
g_autoptr (GDBusConnection) conn = NULL; g_autoptr (GDBusConnection) conn = NULL;
params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err); params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err);
if (params == NULL) { if (params == NULL) {
g_prefix_error (&err, "Failed to list active calls on startup: "); g_prefix_error (&err, "Failed to list active calls on startup: ");
wp_warning_object (wpmm, "%s", err->message); wp_warning_object (wpmm, "%s", err->message);
g_clear_object (&err);
return; return;
} }
g_object_get (wpmm->dbus, "connection", &conn, NULL);
g_variant_get (params, "(ao)", &calls); g_variant_get (params, "(ao)", &calls);
while (g_variant_iter_loop (calls, "o", &path)) { while (g_variant_iter_loop (calls, "&o", &path)) {
g_dbus_proxy_new (conn, g_dbus_proxy_new (conn,
G_DBUS_PROXY_FLAGS_NONE, G_DBUS_PROXY_FLAGS_NONE,
NULL, NULL,
@ -222,9 +219,6 @@ list_calls_done (GObject * obj,
bind_call, bind_call,
wpmm); wpmm);
} }
g_variant_iter_free (calls);
g_variant_unref (params);
} }
static void static void
@ -353,7 +347,7 @@ static void
wp_modem_manager_enable (WpPlugin * self, WpTransition * transition) wp_modem_manager_enable (WpPlugin * self, WpTransition * transition)
{ {
WpModemManager *wpmm = WP_MODEM_MANAGER (self); WpModemManager *wpmm = WP_MODEM_MANAGER (self);
WpCore *core; g_autoptr (WpCore) core = NULL;
GError *err; GError *err;
g_autoptr (GDBusConnection) conn = NULL; g_autoptr (GDBusConnection) conn = NULL;

View file

@ -98,8 +98,10 @@ static void item_free (gpointer data)
{ {
Item *item = data; Item *item = data;
free(item->desktop_entry); g_clear_pointer (&item->desktop_entry, g_free);
free(item); g_clear_pointer (&item->flatpak_app_id, g_free);
g_clear_pointer (&item->flatpak_instance_id, g_free);
g_free (item);
} }
static Players *players_new (GDBusConnection *conn) static Players *players_new (GDBusConnection *conn)
@ -128,7 +130,7 @@ static void players_unref (Players *players)
return; return;
g_mutex_clear (&players->lock); g_mutex_clear (&players->lock);
g_clear_object (&players->items); g_clear_pointer (&players->items, g_hash_table_unref);
g_clear_object (&players->conn); g_clear_object (&players->conn);
g_clear_object (&players->cancellable); g_clear_object (&players->cancellable);
g_free (players); g_free (players);
@ -236,7 +238,7 @@ static void item_desktop_entry_cb (GObject *source_object, GAsyncResult* res, gp
} }
g_variant_get (result, "(v)", &value); g_variant_get (result, "(v)", &value);
if (!g_str_equal(g_variant_get_type_string (value), "s")) { if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name); wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name);
return; return;
} }
@ -426,6 +428,8 @@ wp_mpris_plugin_operation_finalize (GObject *object)
WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object); WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object);
g_clear_object (&self->conn); g_clear_object (&self->conn);
G_OBJECT_CLASS (wp_mpris_plugin_operation_parent_class)->finalize (object);
} }
static void static void
@ -529,9 +533,7 @@ wp_mpris_plugin_disable (WpPlugin * plugin)
static gpointer static gpointer
wp_mpris_plugin_get_players (WpMprisPlugin *self) wp_mpris_plugin_get_players (WpMprisPlugin *self)
{ {
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY); g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("av"));
g_variant_builder_init (&b, G_VARIANT_TYPE ("av"));
if (self->players) { if (self->players) {
g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock); g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock);

View file

@ -13,6 +13,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("m-portal-permissionstore")
#define DBUS_INTERFACE_NAME "org.freedesktop.impl.portal.PermissionStore" #define DBUS_INTERFACE_NAME "org.freedesktop.impl.portal.PermissionStore"
#define DBUS_OBJECT_PATH "/org/freedesktop/impl/portal/PermissionStore" #define DBUS_OBJECT_PATH "/org/freedesktop/impl/portal/PermissionStore"
#define DBUS_CALL_TIMEOUT_MSEC 3000
enum enum
{ {
@ -60,7 +61,8 @@ wp_portal_permissionstore_plugin_lookup (WpPortalPermissionStorePlugin *self,
/* Lookup */ /* Lookup */
res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME, res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Lookup", DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Lookup",
g_variant_new ("(ss)", table, id), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, g_variant_new ("(ss)", table, id), NULL, G_DBUS_CALL_FLAGS_NONE,
DBUS_CALL_TIMEOUT_MSEC, NULL,
&error); &error);
if (error) { if (error) {
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
@ -97,8 +99,8 @@ wp_portal_permissionstore_plugin_set (WpPortalPermissionStorePlugin *self,
/* Set */ /* Set */
res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME, res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Set", DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Set",
g_variant_new ("(sbs@a{sas}@v)", table, id, permissions, data), NULL, g_variant_new ("(sbs@a{sas}@v)", table, create, id, permissions, data),
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_CALL_TIMEOUT_MSEC, NULL, &error);
if (error) { if (error) {
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
g_dbus_error_strip_remote_error (error); g_dbus_error_strip_remote_error (error);

View file

@ -137,10 +137,11 @@ si_audio_adapter_get_default_clock_rate (WpSiAudioAdapter * self)
static gboolean static gboolean
is_unpositioned (struct spa_audio_info_raw *info) is_unpositioned (struct spa_audio_info_raw *info)
{ {
uint32_t i; uint32_t i, n_pos;
if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
return TRUE; return TRUE;
for (i = 0; i < info->channels; i++) n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
for (i = 0; i < n_pos; i++)
if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux && if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux) info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
return TRUE; return TRUE;
@ -197,7 +198,7 @@ si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node,
continue; continue;
if (position == NULL || if (position == NULL ||
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS)) !spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_N_ELEMENTS(raw_format.position)))
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED); SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
if (mono) { if (mono) {
@ -349,7 +350,8 @@ format_audio_raw_build (const struct spa_audio_info_raw *info)
if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) { if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
/* Build the position array spa pod */ /* Build the position array spa pod */
g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array (); g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
for (guint i = 0; i < info->channels; i++) guint n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
for (guint i = 0; i < n_pos; i++)
wp_spa_pod_builder_add_id (position_builder, info->position[i]); wp_spa_pod_builder_add_id (position_builder, info->position[i]);
/* Add the position property */ /* Add the position property */

View file

@ -195,12 +195,16 @@ on_link_activated (WpObject * proxy, GAsyncResult * res,
{ {
WpSiStandardLink *self = wp_transition_get_source_object (transition); WpSiStandardLink *self = wp_transition_get_source_object (transition);
guint len = self->node_links ? self->node_links->len : 0; guint len = self->node_links ? self->node_links->len : 0;
g_autoptr (GError) error = NULL;
/* Count the number of failed and active links */ /* Count the number of failed and active links */
if (wp_object_activate_finish (proxy, res, NULL)) if (wp_object_activate_finish (proxy, res, &error)) {
self->n_active_links++; self->n_active_links++;
else } else {
self->n_failed_links++; self->n_failed_links++;
wp_info_object (self, "Failed to activate link %p: %s", proxy,
error->message);
}
/* Wait for all links to finish activation */ /* Wait for all links to finish activation */
if (self->n_failed_links + self->n_active_links != len) if (self->n_failed_links + self->n_active_links != len)

View file

@ -38,6 +38,7 @@ typedef enum {
typedef enum { typedef enum {
RESCAN_CONTEXT_LINKING, RESCAN_CONTEXT_LINKING,
RESCAN_CONTEXT_DEFAULT_NODES, RESCAN_CONTEXT_DEFAULT_NODES,
RESCAN_CONTEXT_MEDIA_ROLE_VOLUME,
N_RESCAN_CONTEXTS, N_RESCAN_CONTEXTS,
} RescanContext; } RescanContext;
@ -48,6 +49,7 @@ rescan_context_get_type (void)
static const GEnumValue values[] = { static const GEnumValue values[] = {
{ RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" }, { RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" },
{ RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" }, { RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" },
{ RESCAN_CONTEXT_MEDIA_ROLE_VOLUME, "RESCAN_CONTEXT_MEDIA_ROLE_VOLUME", "media-role-volume" },
{ 0, NULL, NULL } { 0, NULL, NULL }
}; };
if (g_once_init_enter (&gtype_id)) { if (g_once_init_enter (&gtype_id)) {
@ -157,10 +159,14 @@ get_default_event_priority (const gchar *event_type)
if (g_str_has_prefix(event_type, "select-") || if (g_str_has_prefix(event_type, "select-") ||
g_str_has_prefix(event_type, "create-")) g_str_has_prefix(event_type, "create-"))
return 500; return 500;
if (g_str_has_prefix(event_type, "autoswitch-"))
return 400;
else if (!g_strcmp0 (event_type, "rescan-for-default-nodes")) else if (!g_strcmp0 (event_type, "rescan-for-default-nodes"))
return -490; return -490;
else if (!g_strcmp0 (event_type, "rescan-for-linking")) else if (!g_strcmp0 (event_type, "rescan-for-linking"))
return -500; return -500;
else if (!g_strcmp0 (event_type, "rescan-for-media-role-volume"))
return -510;
else if (!g_strcmp0 (event_type, "node-state-changed")) else if (!g_strcmp0 (event_type, "node-state-changed"))
return 50; return 50;
else if (!g_strcmp0 (event_type, "metadata-changed")) else if (!g_strcmp0 (event_type, "metadata-changed"))
@ -205,7 +211,8 @@ static gboolean
is_it_local_event (const gchar *event_type) is_it_local_event (const gchar *event_type)
{ {
if (g_str_has_prefix(event_type, "select-") || if (g_str_has_prefix(event_type, "select-") ||
g_str_has_prefix(event_type, "create-")) g_str_has_prefix(event_type, "create-") ||
g_str_has_prefix(event_type, "autoswitch-"))
return TRUE; return TRUE;
return FALSE; return FALSE;

308
po/bg.po
View file

@ -1,24 +1,18 @@
# Bulgarian translation of wireplumber po-file.
# Copyright (C) 2016 Valentin Laskov.
# Copyright (C) 2024 twlvnn kraftwerk.
# This file is licensed under the same license as the wireplumber package
# Valentin Laskov <laskov@festa.bg>, 2016. #zanata
# twlvnn kraftwerk <kraft_werk@tutanota.com>, 2024.
#
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WirePlumber master\n" "Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"POT-Creation-Date: 2025-02-05 03:57+0000\n" "issues\n"
"PO-Revision-Date: 2025-02-05 12:04+0100\n" "POT-Creation-Date: 2025-12-23 17:57+0000\n"
"PO-Revision-Date: 2026-02-21 13:01+0100\n"
"Last-Translator: twlvnn kraftwerk <kraft_werk@tutanota.com>\n" "Last-Translator: twlvnn kraftwerk <kraft_werk@tutanota.com>\n"
"Language-Team: Bulgarian <dict-notifications@fsa-bg.org>\n" "Language-Team: Bulgarian <dict-notifications@fsa-bg.org>\n"
"Language: bg\n" "Language: bg\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Gtranslator 47.0\n" "X-Generator: Poedit 3.8\n"
#. WirePlumber #. WirePlumber
#. #.
@ -48,7 +42,7 @@ msgstr "Разделяне на %s"
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. handle split HW node #. handle split HW node
@ -57,15 +51,15 @@ msgstr "Разделяне на %s"
#. ensure the device has an appropriate name #. ensure the device has an appropriate name
#. deduplicate devices with the same name #. deduplicate devices with the same name
#. ensure the device has a description #. ensure the device has a description
#: src/scripts/monitors/alsa.lua:433 #: src/scripts/monitors/alsa.lua:438
msgid "Loopback" msgid "Loopback"
msgstr "Локален интерфейс" msgstr "Локален интерфейс"
#: src/scripts/monitors/alsa.lua:435 #: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Вградено аудио" msgstr "Вградено аудио"
#: src/scripts/monitors/alsa.lua:437 #: src/scripts/monitors/alsa.lua:442
msgid "Modem" msgid "Modem"
msgstr "Модем" msgstr "Модем"
@ -74,6 +68,7 @@ msgstr "Модем"
#. form factor -> icon #. form factor -> icon
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP #. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available #. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device #. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires #. acquired at all times and destroys it if someone else acquires
@ -135,3 +130,284 @@ msgstr "Вградена предна камера"
#: src/scripts/monitors/libcamera/name-node.lua:63 #: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera" msgid "Built-in Back Camera"
msgstr "Вградена задна камера" msgstr "Вградена задна камера"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Винаги да се показва микрофона за Bluetooth слушалки и да се превключва в "
"режим слушалки при записване"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Автоматично превключване към профил за слушалки"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Запомняне и възстановяване на състоянието на Bluetooth слушалките"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Постоянно пространство"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Запомняне и възстановяване на профили на устройства"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Възстановяване на профил"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Запомняне и възстановяване на маршрутите на устройствата"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Възстановяване на маршрутите"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Стандартната сила на звука за аудио приемниците"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Стандартна сила на звука на аудио приемниците"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Стандартната сила на звука за аудио източниците"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Стандартна сила на звука на източника"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Автоматично заглушаване на всички аудио устройства, когато активните кабелни "
"слушалки/високоговорители са изключени, за да се предотврати нежелано "
"излъчване на звук"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Автоматично заглушаване при прекъсване на кабелните аудио връзки"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Автоматично заглушаване на всички аудио устройства, когато активните "
"Bluetooth слушалки/високоговорители са изключени, за да се предотврати "
"нежелано излъчване на звук"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Автоматично заглушаване при прекъсване на Bluetooth аудио връзката"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr ""
"Потоците за предаване могат да бъдат преместени чрез добавяне на PipeWire "
"метаданни по време на изпълнение"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Позволяване на преместване на потоците"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr ""
"Потоците, свързани със стандартното устройството, следват промените на "
"стандартните настройките"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Следване на стандартната цел"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr ""
"Спиране на музикалните плейъри, ако техният целеви приемник бъде премахнат"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Спиране на възпроизвеждането, ако изходното устройство е премахнато"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Нивото на звука, което да се прилага при понижаване на звука (= намаляване "
"на звука, за да се чува по-високоприоритетен поток) в политиката за "
"свързване на базата на роли"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Ниво на понижаване"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"Автоматично откриване на броя и позициите на каналите за HDMI устройства "
"(експериментално)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "Автоматично откриване на HDMI каналите (експериментално)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Време за откриване на камерата в милисекунди"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Време за откриване"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Включване на контролните портове на аудио възлите"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Контролни портове"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Включване на портовете за слушане на аудио елементи"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "Портове за слушане"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Настройване на всички аудио приемник възли в МОНО режим"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Моно"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Без преобразуване на аудио във F32 формат"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "Без DSP"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Дали да се препредава форматът при филтриращите възли или не"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Препредаване на формата"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr ""
"Запомняне и възстановяване на стандартните аудио/видео устройства за вход/"
"изход"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Възстановяване на стандартната цел"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Стандартната сила на звука за записващите възли"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Стандартна сила на звука на източника за записване"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr ""
"Стандартната роля на медия, която да се задава на потоци, които не я посочват"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Стандартната роля на медия"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Стандартната сила на звука за възпроизвеждащите възли"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Стандартна сила на звука"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Запомняне и възстановяване на свойствата на потоците"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Свойства за възстановяване"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Запомняне и възстановяване на целите за потоци"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Цел за възстановяване"

View file

@ -13,6 +13,16 @@ msgstr ""
msgid "Auto-switch to headset profile" msgid "Auto-switch to headset profile"
msgstr "" msgstr ""
#. /wireplumber.settings.schema/bluetooth.profile-preference/description
#: wireplumber.conf
msgid "Prefer better quality or better latency when auto-selecting profiles (only 'quality' or 'latency' values are accepted)"
msgstr ""
#. /wireplumber.settings.schema/bluetooth.profile-preference/name
#: wireplumber.conf
msgid "Bluetooth profile preference"
msgstr ""
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description #. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf #: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status" msgid "Remember and restore Bluetooth headset mode status"
@ -123,6 +133,16 @@ msgstr ""
msgid "Ducking level" msgid "Ducking level"
msgstr "" msgstr ""
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid "Automatically detect channel count and positions for HDMI devices (experimental)"
msgstr ""
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr ""
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description #. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf #: wireplumber.conf
msgid "The camera discovery timeout in milliseconds" msgid "The camera discovery timeout in milliseconds"
@ -155,7 +175,7 @@ msgstr ""
#. /wireplumber.settings.schema/node.features.audio.mono/description #. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf #: wireplumber.conf
msgid "Configure all audio nodes in MONO" msgid "Configure all audio device sink nodes in MONO"
msgstr "" msgstr ""
#. /wireplumber.settings.schema/node.features.audio.mono/name #. /wireplumber.settings.schema/node.features.audio.mono/name

285
po/ka.po
View file

@ -1,14 +1,14 @@
# Georgian translation for pipewire. # Georgian translation for pipewire.
# Copyright © 2008-2022 Free Software Foundation, Inc. # Copyright © 2008-2022 Free Software Foundation, Inc.
# This file is distributed under the same license as the pipewire package. # This file is distributed under the same license as the pipewire package.
# Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022. # Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022 2026.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: pipewire\n" "Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n" "issues\n"
"POT-Creation-Date: 2022-06-15 15:30+0000\n" "POT-Creation-Date: 2022-06-15 15:30+0000\n"
"PO-Revision-Date: 2022-07-25 13:53+0200\n" "PO-Revision-Date: 2026-01-29 15:48+0100\n"
"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n" "Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
"Language-Team: Georgian <(nothing)>\n" "Language-Team: Georgian <(nothing)>\n"
"Language: ka\n" "Language: ka\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.1.1\n" "X-Generator: Poedit 3.8\n"
#. WirePlumber #. WirePlumber
#. #.
@ -102,3 +102,282 @@ msgstr "ჩაშენებული წინა კამერა"
#: src/scripts/monitors/libcamera.lua:90 #: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera" msgid "Built-in Back Camera"
msgstr "ჩაშენებული უკანა კამერა" msgstr "ჩაშენებული უკანა კამერა"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"მიკროფონის ყოველთვის ჩვენება ბლუთუზის ყურსასმენებისთვის და გადართვა "
"ყურსასმენის რეჟიმზე ჩაწერისას"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "ავტოგადართვა ყურსაცვამის პროფილზე"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "ბლუთუზის ყურსასმენის რეჟიმის სტატუსის დამახსოვრება და აღდგენა"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "მუდმივი საცავი"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "მოწყობილობის პროფილების დამახსოვრება და აღდგენა"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "პროფილის აღდგენა"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "მოწყობილობის რაუტების დამახსოვრება და აღდგენა"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "მოწყობილობის რაუტები"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "აუდიოს მიმღების ნაგულისხმევი ხმის დონე"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "მიმღების ნაგულისხმევი ხმის დონე"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "ნაგულისხმევი ხმის დონე აუდიოს წყაროებისთვის"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "ნაგულისხმევი წყაროს ხმა"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"ყველა აუდიომოწყობილობის ავტომატური დადუმება, როცა აქტიური მავთულიანი "
"ყურსასმენი/დინამიკები გათიშულია, რომ თავიდან აიცილოთ გაუთვალისწინებელი ხმის "
"გამოცემა"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "ავტოდადუმება სადენიანი აუდიოს გათიშვისას"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"ყველა აუდიომოწყობილობის ავტომატური დადუმება, როცა აქტიური ბლუთუზი "
"ყურსასმენი/დინამიკები გათიშულია, რომ თავიდან აიცილოთ გაუთვალისწინებელი ხმის "
"გამოცემა"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "ავტოდადუმება ბლუთუზი აუდიოს გათიშვისას"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr ""
"ნაკადების გადატანა გაშვების დროს PipeWire-ის მეტამონაცემების დამატებით "
"შეგიძლიათ"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "მოძრავი ნაკადების დაშვება"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr ""
"ნაგულისხმევ მოწყობილობაზე დაკავშირებული ნაკადები მიჰყვება, როცა "
"ნაგულისხმევი იცვლება"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "ნაგულისხმევი სამიზნის მიყოლა"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "მედიის დამკვრელების შეჩერება სამიზნე მიმღების მოხსნისას"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "დაკვრის შეჩერება, თუ გამოტანა მოიხსნება"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"ხმის დონე დაქინგისას გამოსაყენებლად (= ხმის შემცირება უფრო მაღალი "
"პრიორიტეტის მქონე ნაკადისთვის, რომ ის გასაგონი გახდეს) როლზე-დამოკიდებულ "
"მიბმის პოლიტიკაში"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "დაქინგის დონე"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"არხების რაოდენობისა და მდებარეობების ავტომატური დადგენა HDMI "
"მოწყობილობებისთვის (ექსპერიმენტული)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "HDMI არხების ავტომატური დადგენა (ექსპერიმენტული)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "კამერის აღმოჩენის მოლოდინის ვადა მიმიწალებში"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "აღმოჩენის მოლოდინის ვადა"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "კონტროლის პორტების ჩართვა აუდიოკვანძებზე"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "კონტროლის პორტები"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "მონიტორინგის პორტების ჩართვა აუდიოკვანძებზე"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "მონიტორინგის პორტები"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "ყველა აუდიომოწყობილობის გამოტანის კვანძების მონოში მორგება"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "მონო"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "აუდიო F32 ფორმატში გადაყვანილი არ იქნება"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "DSP-ის გარეშე"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "მოხდება თუ არა ფორმატის გადაგზავნა ფილტრის კვანძებზე"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "ფორმატის გადაგზავნა"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr ""
"ნაგულისხმევი აუდიო/ვიდეო შეყვანა/გამოტანის მოწყობილობების დამახსოვრება და "
"აღდგენა"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "ნაგულისხმევი სამიზნის აღდგენა"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "ნაგულისხმევი ხმის დონე ჩამწერი კვანძებისთვის"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "ნაგულისხმევი ჩაწერის ხმის დონე"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr "ნაგულისხმევი media.role მისანიჭებლად ნაკადებზე, რომლებსაც ის არ აქვს"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "ნაგულისხმევი მედიის როლი"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "ნაგულისხმევი ხმის დონე დაკვრის კვანძებისთვის"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "ნაგულისხმევი ხმის დონე"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "ნაკადის თვისებების დამახსოვრება და აღდგენა"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "თვისებების აღდგენა"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "ნაკადის სამიზნეების დამახსოვრება და აღდგენა"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "სამიზნის აღდგენა"

399
po/kk.po
View file

@ -1,37 +1,41 @@
# Kazakh translation of pipewire. # Kazakh translation of pipewire.
# Copyright (C) 2020 The pipewire authors. # Copyright (C) 2020 The pipewire authors.
# This file is distributed under the same license as the pipewire package. # This file is distributed under the same license as the pipewire package.
# Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2020. # Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2020-2026.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues/new\n" "issues\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n" "POT-Creation-Date: 2025-12-23 17:57+0000\n"
"PO-Revision-Date: 2020-06-30 08:04+0500\n" "PO-Revision-Date: 2026-03-01 19:33+0500\n"
"Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n" "Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n"
"Language-Team: \n" "Language-Team: Kazakh <kk_KZ@googlegroups.com>\n"
"Language: kk\n" "Language: kk\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.3.1\n" "X-Generator: Poedit 3.8\n"
#. WirePlumber #. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd. #. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua #. unique device/node name tables
#. ensure config.properties is not nil #. SPA ids to node names: name = id_name_table[device_id][node_id]
#. preprocess rules and create Interest objects #. create the underlying hidden ALSA node
#. applies properties from config.rules when asked to #. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Бөлінген %s"
#. Connect ObjectConfig events to the right node
#. set the device id and spa factory name; REQUIRED, do not change #. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting #. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels #. try to negotiate the max amount of channels
#. set priority #. set priority
#. ensure the node has a media class #. ensure the node has a media class
#. ensure the node has a name #. ensure the node has a name
@ -41,16 +45,373 @@ msgstr ""
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. apply properties from config.rules #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. create the node #. create the node
#. ensure the device has an appropriate name #. ensure the device has an appropriate name
#. deduplicate devices with the same name #. deduplicate devices with the same name
#. ensure the device has a description #. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222 #: src/scripts/monitors/alsa.lua:438
msgid "Loopback"
msgstr "Loopback"
#: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Құрамындағы аудио" msgstr "Құрамындағы аудио"
#: src/scripts/monitors/alsa.lua:224 #: src/scripts/monitors/alsa.lua:442
msgid "Modem" msgid "Modem"
msgstr "Модем" msgstr "Модем"
#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device
#. name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from the rules in the configuration file
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:114
#, lua-format
msgid "BLE MIDI %d"
msgstr "BLE MIDI %d"
#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Ішкі алдыңғы камера"
#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Ішкі артқы камера"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-
#. profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Bluetooth гарнитуралары үшін микрофонды әрдайым көрсету және жазу кезінде "
"гарнитура режиміне ауысу"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Гарнитура профиліне автоматты түрде ауысу"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Bluetooth гарнитура режимінің күйін есте сақтау және қалпына келтіру"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Тұрақты сақтау орны"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Құрылғы профильдерін есте сақтау және қалпына келтіру"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Профильді қалпына келтіру"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Құрылғы маршруттарын есте сақтау және қалпына келтіру"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Маршруттарды қалпына келтіру"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Аудио қабылдағыштары үшін бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Қабылдағыштың бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/device.routes.default-source-
#. volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Аудио көздері үшін бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Көздің бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-
#. removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Күтпеген дыбыс шығуын болдырмау үшін белсенді сымды құлаққаптар/колонкалар "
"ажыратылған кезде барлық аудио құрылғылардың дыбысын автоматты түрде өшіру"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-
#. removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Сымды аудио ажыратылғанда дыбысты автоматты өшіру"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-
#. removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Күтпеген дыбыс шығуын болдырмау үшін белсенді Bluetooth құлаққаптары/"
"колонкалары ажыратылған кезде барлық аудио құрылғылардың дыбысын автоматты "
"түрде өшіру"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-
#. removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Bluetooth аудио ажыратылғанда дыбысты автоматты өшіру"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr ""
"Ағындарды орындалу уақытында PipeWire метадеректерін қосу арқылы жылжытуға "
"болады"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Ағындарды жылжытуға рұқсат ету"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr ""
"Бастапқы құрылғыға қосылған ағындар бастапқы құрылғы өзгергенде оның "
"соңынан ереді"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Бастапқы мақсаттың соңынан еру"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "Егер мақсатты қабылдағыш өшірілсе, медиа ойнатқыштарды аялдату"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Шығыс өшірілсе, ойнатуды аялдату"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Рөлге негізделген байланыстыру саясатында даккинг (= басымдылығы жоғары "
"ағын естілуі үшін дыбысты азайту) кезінде қолданылатын дыбыс деңгейі"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Даккинг деңгейі"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-
#. channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"HDMI құрылғылары үшін арналар санын және орындарын автоматты түрде анықтау "
"(эксперименталды)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "HDMI арналарын автоматты түрде анықтау (эксперименталды)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Камераны анықтаудың күту уақыты (миллисекундпен)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Анықтаудың күту уақыты"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Аудио тораптарында басқару порттарын іске қосу"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Басқару порттары"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Аудио тораптарында монитор порттарын іске қосу"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "Монитор порттары"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Барлық аудио құрылғының қабылдағыш тораптарын MONO режимінде баптау"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Моно"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Аудионы F32 пішіміне түрлендірмеу"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "DSP жоқ"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Сүзгі тораптарында пішімді алға жіберу немесе жібермеу"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Пішімді алға жіберу"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr ""
"Бастапқы аудио/видео кіріс/шығыс құрылғыларын есте сақтау және қалпына "
"келтіру"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Бастапқы мақсатты қалпына келтіру"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Түсіру тораптары үшін бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Түсірудің бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr "Оны көрсетпеген ағындарға тағайындалатын бастапқы media.role"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Бастапқы медиа рөлі"
#. /wireplumber.settings.schema/node.stream.default-playback-
#. volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Ойнату тораптары үшін бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Ойнатудың бастапқы дыбыс деңгейі"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Ағындардың қасиеттерін есте сақтау және қалпына келтіру"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Қасиеттерді қалпына келтіру"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Ағын мақсаттарын есте сақтау және қалпына келтіру"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Мақсатты қалпына келтіру"

View file

@ -2,24 +2,22 @@
# Copyright (C) 2024 WirePlumber's COPYRIGHT HOLDER # Copyright (C) 2024 WirePlumber's COPYRIGHT HOLDER
# This file is distributed under the same license as the WirePlumber package. # This file is distributed under the same license as the WirePlumber package.
# #
# Martin <miles@filmsi.net>, 2024, 2025. # Martin <miles@filmsi.net>, 2024, 2025.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WirePlumber master\n" "Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
"issues\n" "POT-Creation-Date: 2025-12-15 16:28+0000\n"
"POT-Creation-Date: 2025-08-21 03:57+0000\n" "PO-Revision-Date: 2025-12-15 23:31+0100\n"
"PO-Revision-Date: 2025-08-21 15:45+0200\n"
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n" "Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
"Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n" "Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n"
"Language: sl\n" "Language: sl\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n" "Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n"
"%100==4 ? 3 : 0);\n" "X-Generator: Poedit 3.8\n"
"X-Generator: Poedit 2.2.1\n"
#. WirePlumber #. WirePlumber
#. #.
@ -49,7 +47,7 @@ msgstr "Razdeli %s"
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. handle split HW node #. handle split HW node
@ -75,6 +73,7 @@ msgstr "Modem"
#. form factor -> icon #. form factor -> icon
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP #. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available #. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device #. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires #. acquired at all times and destroys it if someone else acquires
@ -201,6 +200,34 @@ msgstr "Privzeta glasnost za zvočne vire"
msgid "Default source volume" msgid "Default source volume"
msgstr "Privzeta izvorna glasnost" msgstr "Privzeta izvorna glasnost"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Samodejno utišaj vse zvočne naprave, ko so aktivne žične slušalke/zvočniki "
"odklopljeni, za preprečitev nenamernega zvočnega izhoda"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Samodejna utišaj zvok pri prekinitvi žične zvokovne povezave"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Samodejno utišaj vse zvočne naprave, ko so aktivne slušalke/zvočniki "
"Bluetooth odklopljeni, da preprečite nenamerni izhod zvoka"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Samodejni utišaj pri prekinitvi zvokovne povezave Bluetooth"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description #. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf #: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime" msgid "Streams may be moved by adding PipeWire metadata at runtime"
@ -247,6 +274,19 @@ msgstr ""
msgid "Ducking level" msgid "Ducking level"
msgstr "Stopnja umikanja" msgstr "Stopnja umikanja"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"Samodejno zaznaj število kanalov in položaje za naprave HDMI (poskusno)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "Samodejno zaznaj kanale HDMI (poskusno)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description #. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf #: wireplumber.conf
msgid "The camera discovery timeout in milliseconds" msgid "The camera discovery timeout in milliseconds"
@ -277,6 +317,16 @@ msgstr "Omogoči vrata nadzornih zvočnikov na zvočnih vozliščih"
msgid "Monitor ports" msgid "Monitor ports"
msgstr "Vrata zvočnih monitorjev" msgstr "Vrata zvočnih monitorjev"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Prilagodi vsa vozlišča zvokovnih ponorov v MONO"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Mono"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description #. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf #: wireplumber.conf
msgid "Do not convert audio to F32 format" msgid "Do not convert audio to F32 format"

392
po/sr.po
View file

@ -3,22 +3,24 @@
# This file is distributed under the same license as the pipewire package. # This file is distributed under the same license as the pipewire package.
# Igor Miletic (Игор Милетић) <grejigl-gnomeprevod@yahoo.ca>, 2009. # Igor Miletic (Игор Милетић) <grejigl-gnomeprevod@yahoo.ca>, 2009.
# Miloš Komarčević <kmilos@gmail.com>, 2009, 2012. # Miloš Komarčević <kmilos@gmail.com>, 2009, 2012.
# Марко Костић <marko.m.kostic@gmail.com>, 2026
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: pipewire\n" "Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues/new\n" "issues\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n" "POT-Creation-Date: 2025-12-23 17:57+0000\n"
"PO-Revision-Date: 2012-01-30 09:55+0000\n" "PO-Revision-Date: 2026-04-11 14:02+0200\n"
"Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n" "Last-Translator: Марко Костић <marko.m.kostic@gmail.com>\n"
"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n" "Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
"Language: \n" "Language: sr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"X-Generator: Poedit 3.9\n"
#. WirePlumber #. WirePlumber
#. #.
@ -26,13 +28,19 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua #. unique device/node name tables
#. ensure config.properties is not nil #. SPA ids to node names: name = id_name_table[device_id][node_id]
#. preprocess rules and create Interest objects #. create the underlying hidden ALSA node
#. applies properties from config.rules when asked to #. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Подели %s"
#. Connect ObjectConfig events to the right node
#. set the device id and spa factory name; REQUIRED, do not change #. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting #. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels #. try to negotiate the max amount of channels
#. set priority #. set priority
#. ensure the node has a media class #. ensure the node has a media class
#. ensure the node has a name #. ensure the node has a name
@ -42,16 +50,366 @@ msgstr ""
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. apply properties from config.rules #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. create the node #. create the node
#. ensure the device has an appropriate name #. ensure the device has an appropriate name
#. deduplicate devices with the same name #. deduplicate devices with the same name
#. ensure the device has a description #. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222 #: src/scripts/monitors/alsa.lua:438
msgid "Loopback"
msgstr "Затворена петља"
#: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Унутрашњи звук" msgstr "Унутрашњи звук"
#: src/scripts/monitors/alsa.lua:224 #: src/scripts/monitors/alsa.lua:442
msgid "Modem" msgid "Modem"
msgstr "Модем" msgstr "Модем"
#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from the rules in the configuration file
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:114
#, lua-format
msgid "BLE MIDI %d"
msgstr "БЛЕ МИДИ %d"
#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#.
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Уграђена предња камера"
#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Уграђена задња камера"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Увек прикажи микрофон за Блутут слушалице са микрофоном и пребаци на режим "
"слушалица са микрофоном приликом снимања"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Самостално пребаци на профил слушалица са микрофоном"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Запамти и поврати стање режима Блутут слушалица са микрофоном"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Трајно складиште"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Запамти и поврати профиле уређаја"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Поврати профил"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Запамти и поврати руте уређаја"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Поврати руте"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Подразумевана јачина звука за сливнике звука"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Подразумевана јачина звука сливника"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Подразумевана јачина звука за изворе звука"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Подразумевана јачина звука извора"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Самостално пригуши све звучне уређаје када се активне жичане слушалице/"
"звучници откаче како би се спречио нежељени излаз звука"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Самостално пригуши при откачивању жичаног звука"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Аутоматски пригуши све звучне уређаје када се активне Блутут слушалице/"
"звучници откаче како би се спречио нежељени излаз звука"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Самостално пригуши при откачивању Блутут звука"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr ""
"Токови се могу преместити додавањем метаподатака Пајпвајера (PipeWire) током "
"извршавања"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Дозволи премештање токова"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr ""
"Токови повезани на подразумевани уређај прате када се подразумевани промени"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Прати подразумевано одредиште"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "Паузирај медијске програме ако је њихов циљни сливник уклоњен"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Паузирај пуштање ако је излаз уклоњен"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Ниво јачине звука који се примењује при умањивању (= смањивање јачине звука "
"да би се чуо ток већег приоритета) у политици повезивања заснованој на "
"улогама"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Ниво умањивања"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"Самостално откриј број канала и положаје за ХДМИ уређаје (експериментално)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "Самостално откриј ХДМИ канале (експериментално)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Време истека откривања камере у милисекундама"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Време истека откривања"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Омогући управљачке прикључнике на звучним чворовима"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Управљачки прикључници"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Омогући надзорне прикључнике на звучним чворовима"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "Надзорни прикључници"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Подеси све сливнике звучних уређаја у МОНО режиму"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Моно"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Не претварај звук у формат Ф32"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "Без ДСП-а"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Проследи формат на чворове филтера или не"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Запис прослеђивања"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr "Запамти и поврати подразумеване звучне/видео улазне/излазне уређаје"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Поврати подразумевани циљ"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Подразумевана јачина звука за чворове снимања"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Подразумевана јачина снимања"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr ""
"Подразумевана медијска улога (media.role) за додељивање токовима који је не "
"наводе"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Подразумевана медијска улога"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Подразумевана јачина звука за чворове пуштања"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Подразумевана јачина пуштања"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Запамти и поврати својства токова"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Поврати својства"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Запамти и поврати циљеве тока"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Поврати циљ"

View file

@ -1,24 +1,26 @@
# Serbian(Latin) translations for pipewire # Serbian translations for pipewire
# Copyright (C) 2006 Lennart Poettering # Copyright (C) 2006 Lennart Poettering
# This file is distributed under the same license as the pipewire package. # This file is distributed under the same license as the pipewire package.
# Igor Miletic (Igor Miletić) <grejigl-gnomeprevod@yahoo.ca>, 2009. # Igor Miletic (Igor Miletić) <grejigl-gnomeprevod@yahoo.ca>, 2009.
# Miloš Komarčević <kmilos@gmail.com>, 2009, 2012. # Miloš Komarčević <kmilos@gmail.com>, 2009, 2012.
# Marko Kostić <marko.m.kostic@gmail.com>, 2026
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: pipewire\n" "Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues/new\n" "issues\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n" "POT-Creation-Date: 2025-12-23 17:57+0000\n"
"PO-Revision-Date: 2012-01-30 09:55+0000\n" "PO-Revision-Date: 2026-04-11 14:02+0200\n"
"Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n" "Last-Translator: Marko Kostić <marko.m.kostic@gmail.com>\n"
"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n" "Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
"Language: \n" "Language: sr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"X-Generator: Poedit 3.9\n"
#. WirePlumber #. WirePlumber
#. #.
@ -26,13 +28,19 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua #. unique device/node name tables
#. ensure config.properties is not nil #. SPA ids to node names: name = id_name_table[device_id][node_id]
#. preprocess rules and create Interest objects #. create the underlying hidden ALSA node
#. applies properties from config.rules when asked to #. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Podeli %s"
#. Connect ObjectConfig events to the right node
#. set the device id and spa factory name; REQUIRED, do not change #. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting #. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels #. try to negotiate the max amount of channels
#. set priority #. set priority
#. ensure the node has a media class #. ensure the node has a media class
#. ensure the node has a name #. ensure the node has a name
@ -42,16 +50,366 @@ msgstr ""
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. apply properties from config.rules #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. create the node #. create the node
#. ensure the device has an appropriate name #. ensure the device has an appropriate name
#. deduplicate devices with the same name #. deduplicate devices with the same name
#. ensure the device has a description #. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222 #: src/scripts/monitors/alsa.lua:438
msgid "Loopback"
msgstr "Zatvorena petlja"
#: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Unutrašnji zvuk" msgstr "Unutrašnji zvuk"
#: src/scripts/monitors/alsa.lua:224 #: src/scripts/monitors/alsa.lua:442
msgid "Modem" msgid "Modem"
msgstr "Modem" msgstr "Modem"
#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from the rules in the configuration file
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:114
#, lua-format
msgid "BLE MIDI %d"
msgstr "BLE MIDI %d"
#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#.
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Ugrađena prednja kamera"
#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Ugrađena zadnja kamera"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Uvek prikaži mikrofon za Blutut slušalice sa mikrofonom i prebaci na režim "
"slušalica sa mikrofonom prilikom snimanja"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Samostalno prebaci na profil slušalica sa mikrofonom"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Zapamti i povrati stanje režima Blutut slušalica sa mikrofonom"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Trajno skladište"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Zapamti i povrati profile uređaja"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Povrati profil"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Zapamti i povrati rute uređaja"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Povrati rute"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Podrazumevana jačina zvuka za slivnike zvuka"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Podrazumevana jačina zvuka slivnika"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Podrazumevana jačina zvuka za izvore zvuka"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Podrazumevana jačina zvuka izvora"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Samostalno priguši sve zvučne uređaje kada se aktivne žičane slušalice/"
"zvučnici otkače kako bi se sprečio neželjeni izlaz zvuka"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Samostalno priguši pri otkačivanju žičanog zvuka"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Automatski priguši sve zvučne uređaje kada se aktivne Blutut slušalice/"
"zvučnici otkače kako bi se sprečio neželjeni izlaz zvuka"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Samostalno priguši pri otkačivanju Blutut zvuka"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr ""
"Tokovi se mogu premestiti dodavanjem metapodataka Pajpvajera (PipeWire) "
"tokom izvršavanja"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Dozvoli premeštanje tokova"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr ""
"Tokovi povezani na podrazumevani uređaj prate kada se podrazumevani promeni"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Prati podrazumevano odredište"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "Pauziraj medijske programe ako je njihov ciljni slivnik uklonjen"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Pauziraj puštanje ako je izlaz uklonjen"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Nivo jačine zvuka koji se primenjuje pri umanjivanju (= smanjivanje jačine "
"zvuka da bi se čuo tok većeg prioriteta) u politici povezivanja zasnovanoj "
"na ulogama"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Nivo umanjivanja"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"Samostalno otkrij broj kanala i položaje za HDMI uređaje (eksperimentalno)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "Samostalno otkrij HDMI kanale (eksperimentalno)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Vreme isteka otkrivanja kamere u milisekundama"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Vreme isteka otkrivanja"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Omogući upravljačke priključnike na zvučnim čvorovima"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Upravljački priključnici"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Omogući nadzorne priključnike na zvučnim čvorovima"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "Nadzorni priključnici"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Podesi sve slivnike zvučnih uređaja u MONO režimu"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Mono"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Ne pretvaraj zvuk u format F32"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "Bez DSP-a"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Prosledi format na čvorove filtera ili ne"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Zapis prosleđivanja"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr "Zapamti i povrati podrazumevane zvučne/video ulazne/izlazne uređaje"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Povrati podrazumevani cilj"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Podrazumevana jačina zvuka za čvorove snimanja"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Podrazumevana jačina snimanja"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr ""
"Podrazumevana medijska uloga (media.role) za dodeljivanje tokovima koji je "
"ne navode"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Podrazumevana medijska uloga"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Podrazumevana jačina zvuka za čvorove puštanja"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Podrazumevana jačina puštanja"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Zapamti i povrati svojstva tokova"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Povrati svojstva"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Zapamti i povrati ciljeve toka"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Povrati cilj"

308
po/sv.po
View file

@ -1,9 +1,9 @@
# Swedish translation for pipewire. # Swedish translation for pipewire.
# Copyright © 2008-2024 Free Software Foundation, Inc. # Copyright © 2008-2026 Free Software Foundation, Inc.
# This file is distributed under the same license as the pipewire package. # This file is distributed under the same license as the pipewire package.
# Daniel Nylander <po@danielnylander.se>, 2008, 2012. # Daniel Nylander <po@danielnylander.se>, 2008, 2012.
# Josef Andersson <josef.andersson@fripost.org>, 2014, 2017. # Josef Andersson <josef.andersson@fripost.org>, 2014, 2017.
# Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021, 2022, 2024. # Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021, 2022, 2024, 2025, 2026.
# #
# Termer: # Termer:
# input/output: ingång/utgång (det handlar om ljud) # input/output: ingång/utgång (det handlar om ljud)
@ -19,8 +19,8 @@ msgstr ""
"Project-Id-Version: pipewire\n" "Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n" "issues\n"
"POT-Creation-Date: 2024-03-11 15:33+0000\n" "POT-Creation-Date: 2025-12-23 17:57+0000\n"
"PO-Revision-Date: 2024-01-11 01:08+0100\n" "PO-Revision-Date: 2026-02-24 01:32+0100\n"
"Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n" "Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
"Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n" "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
"Language: sv\n" "Language: sv\n"
@ -28,7 +28,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Poedit 3.4.2\n" "X-Generator: Poedit 3.8\n"
#. WirePlumber #. WirePlumber
#. #.
@ -37,9 +37,18 @@ msgstr ""
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. unique device/node name tables #. unique device/node name tables
#. SPA ids to node names: name = id_name_table[device_id][node_id]
#. create the underlying hidden ALSA node
#. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Dela %s"
#. Connect ObjectConfig events to the right node
#. set the device id and spa factory name; REQUIRED, do not change #. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting #. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels #. try to negotiate the max amount of channels
#. set priority #. set priority
#. ensure the node has a media class #. ensure the node has a media class
#. ensure the node has a name #. ensure the node has a name
@ -49,22 +58,24 @@ msgstr ""
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add vm.type for rule matching purposes #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. create the node #. create the node
#. ensure the device has an appropriate name #. ensure the device has an appropriate name
#. deduplicate devices with the same name #. deduplicate devices with the same name
#. ensure the device has a description #. ensure the device has a description
#: src/scripts/monitors/alsa.lua:214 #: src/scripts/monitors/alsa.lua:438
msgid "Loopback" msgid "Loopback"
msgstr "Loopback" msgstr "Loopback"
#: src/scripts/monitors/alsa.lua:216 #: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Inbyggt ljud" msgstr "Inbyggt ljud"
#: src/scripts/monitors/alsa.lua:218 #: src/scripts/monitors/alsa.lua:442
msgid "Modem" msgid "Modem"
msgstr "Modem" msgstr "Modem"
@ -73,6 +84,7 @@ msgstr "Modem"
#. form factor -> icon #. form factor -> icon
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP #. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available #. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device #. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires #. acquired at all times and destroys it if someone else acquires
@ -134,3 +146,277 @@ msgstr "Inbyggd främre kamera"
#: src/scripts/monitors/libcamera/name-node.lua:63 #: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera" msgid "Built-in Back Camera"
msgstr "Inbyggd bakre kamera" msgstr "Inbyggd bakre kamera"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Visa alltid mikrofon för Bluetooth-headsets, och växla till headset-läge vid "
"inspelning"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Växla automatiskt till headset-profil"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Kom ihåg och återställ lägesstatus för Bluetooth-headset"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Beständig lagring"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Kom ihåg och återställ enhetsprofiler"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Återställ profil"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Kom ihåg och återställ enhetsrutter"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Återställ rutter"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Standardvolymen för ljudutgångar"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Standardvolym för utgångar"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Standardvolymen för ljudkällor"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Standardkällvolym"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Tysta automatiskt alla ljudenheter då aktiva trådbundna hörlurar/högtalare "
"kopplas från för att förhindra oavsiktlig ljudutmatning"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Tysta automatiskt vid frånkoppling av trådbundet ljud"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Tysta automatiskt alla ljudenheter då aktiva Bluetooth-hörlurar/högtalare "
"kopplas från för att förhindra oavsiktlig ljudutmatning"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Tysta automatiskt vid frånkoppling av Bluetooth-ljud"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr ""
"Strömmar kan flyttas genom att lägga till PipeWire-metadata vid körning"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Tillåt flytt av strömmar"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr ""
"Strömmar anslutna till standardenheten följer när standardvärdet ändras"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Följ standardmål"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "Pausa mediespelare om deras målutgång tas bort"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Pausa uppspelning om utgången tas bort"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Volymnivån att tillämpa vid duckande (=reducera volymen så en högre "
"prioriterad ström ska vara hörbar) i den rollbaserade länkpolicyn"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Ducknivå"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"Upptäck automatiskt kanalantal och positioner för HDMI-enheter "
"(experimentellt)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "Upptäck automatiskt HDMI-kanaler (experimentellt)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Kamerans tidsgräns för upptäckt i millisekunder"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Tidsgräns för upptäckt"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Aktivera styrportar på ljudnoder"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Styrportar"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Aktivera övervakningsportar på ljudnoder"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "Övervakningsportar"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Konfigurera alla ljudenheters utgångsnoder i MONO"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Mono"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Konvertera inte ljud till F32-format"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "Ingen DSP"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Vidarebefordra format på filternoder eller inte"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Vidarebefordra format"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr ""
"Kom ihåg och återställ ljud/videoenheter att använda som ingångar/utgångar "
"som standard"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Återställ standardmål"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Standardvolymen för inspelningsnoder"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Standardinspelningsvolym"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr "media.role att tilldela som standard på strömmar som inte anger den"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Standardmediaroll"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Standardvolymen för uppspelningsnoder"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Standarduppspelningsvolym"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Kom ihåg och återställ egenskaper för strömmar"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Återställ egenskaper"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Kom ihåg och återställ mål för strömmar"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Återställ mål"

400
po/tr.po
View file

@ -1,26 +1,25 @@
# Turkish translation for PipeWire. # Turkish translation for WirePlumber.
# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER # Copyright (C) 2025 WirePlumber's COPYRIGHT HOLDER
# This file is distributed under the same license as the PipeWire package. # This file is distributed under the same license as the WirePlumber package.
# Necdet Yücel <necdetyucel@gmail.com>, 2014. #
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014. # Sabri Ünal <yakushabb@gmail.com>, 2025.
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017. # Emin Tufan Çetin <etcetin@gmail.com>, 2025
# Oğuz Ersen <oguzersen@protonmail.com>, 2021.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PipeWire master\n" "Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues/new\n" "issues\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n" "POT-Creation-Date: 2025-11-09 04:07+0000\n"
"PO-Revision-Date: 2021-12-06 21:31+0300\n" "PO-Revision-Date: 2025-11-09 08:00+0300\n"
"Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n" "Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
"Language-Team: Turkish <tr>\n" "Language-Team: Turkish <takim@gnome.org.tr>\n"
"Language: tr\n" "Language: tr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.4.2\n" "X-Generator: Poedit 3.8\n"
#. WirePlumber #. WirePlumber
#. #.
@ -28,13 +27,19 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua #. unique device/node name tables
#. ensure config.properties is not nil #. SPA ids to node names: name = id_name_table[device_id][node_id]
#. preprocess rules and create Interest objects #. create the underlying hidden ALSA node
#. applies properties from config.rules when asked to #. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Bölük %s"
#. Connect ObjectConfig events to the right node
#. set the device id and spa factory name; REQUIRED, do not change #. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting #. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels #. try to negotiate the max amount of channels
#. set priority #. set priority
#. ensure the node has a media class #. ensure the node has a media class
#. ensure the node has a name #. ensure the node has a name
@ -45,15 +50,360 @@ msgstr ""
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. create the node #. create the node
#. ensure the device has an appropriate name #. ensure the device has an appropriate name
#. deduplicate devices with the same name #. deduplicate devices with the same name
#. ensure the device has a description #. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222 #: src/scripts/monitors/alsa.lua:438
msgid "Built-in Audio" msgid "Loopback"
msgstr "Dahili Ses" msgstr "Geri Döngü"
#: src/scripts/monitors/alsa.lua:224 #: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio"
msgstr "Yerleşik Ses"
#: src/scripts/monitors/alsa.lua:442
msgid "Modem" msgid "Modem"
msgstr "Modem" msgstr "Modem"
#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from the rules in the configuration file
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:114
#, lua-format
msgid "BLE MIDI %d"
msgstr "BLE MIDI %d"
#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#.
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Yerleşik Ön Kamera"
#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Yerleşik Arka Kamera"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Bluetooth kulaklıklar için her zaman mikrofonu göster ve kayıt sırasında "
"kulaklık kipine geç"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Kulaklık profiline kendiliğinden geç"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Bluetooth kulaklık kipi durumunu anımsa ve geri yükle"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Kalıcı depolama"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Aygıt profillerini anımsa ve geri yükle"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Profili geri yükle"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Aygıt rotalarını anımsa ve geri yükle"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Rotaları geri yükle"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Ses alıcıları için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Öntanımlı alıcı ses düzeyi"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Ses kaynakları için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Öntanımlı kaynak ses düzeyi"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"İstenmeyen ses çıktısını önlemek için etkin kablolu kulaklık/hoparlör "
"bağlantısı kesildiğinde tüm ses aygıtlarını kendiliğinden sustur"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Kablolu ses bağlantısı kesildiğinde kendiliğinden sustur"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"İstenmeyen ses çıktısını önlemek için etkin Bluetooth kulaklık/hoparlör "
"bağlantısı kesildiğinde tüm ses aygıtlarını kendiliğinden sustur"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Bluetooth ses bağlantısı kesildiğinde kendiliğinden sustur"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr "Akışlar, çalışma zamanında PipeWire üst verileri eklenerek taşınabilir"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Akışları taşımaya izin ver"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr "Öntanımlı aygıta bağlı akışlar öntanımlı değiştiğinde izler"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Öntanımlı hedefi izle"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "Hedef alıcıları kaldırılırsa ortam oynatıcıları duraklat"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Çıktı kaldırılırsa oynatmayı duraklat"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Rol tabanlı bağlantı ilkesinde eğilirken (= daha öncelikli akışın "
"duyulabilmesi için ses düzeyinin azaltılması) uygulanacak ses düzeyi"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Eğilme düzeyi"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"HDMI aygıtları için kanal sayısını ve konumlarını kendiliğinden algıla "
"(deneysel)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "HDMI kanallarını kendiliğinden algıla (deneysel)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Kamera keşif zaman aşımı, saniye türünden"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Keşif zaman aşımı"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Ses düğümlerinde denetim bağlantı noktalarını etkinleştir"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Denetim bağlantı noktaları"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Ses düğümlerinde izleme bağlantı noktalarını etkinleştir"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "İzleme bağlantı noktaları"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio nodes in MONO"
msgstr "Tüm ses düğümlerini MONO olarak yapılandır"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Mono"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Sesi F32 biçimine dönüştürme"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "DSP yok"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Süzgeç düğümlerinde biçimi ilet ya da iletme"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Biçimi ilet"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr "Öntanımlı ses/video girdi/çıktı aygıtlarını anımsa ve geri yükle"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Öntanımlı hedefi geri yükle"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Yakalama düğümleri için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Öntanımlı yakalama ses düzeyi"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr "Belirtilmeyen akışlarda atanacak öntanımlı ortam rolü"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Öntanımlı ortam rolü"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Oynatma düğümleri için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Öntanımlı oynatma ses düzeyi"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Akışların özelliklerini anımsa ve geri yükle"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Özellikleri geri yükle"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Akış hedeflerini anımsa ve geri yükle"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Hedefi geri yükle"

View file

@ -13,8 +13,8 @@ msgstr ""
"Project-Id-Version: pipewire.master-tx\n" "Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n" "issues\n"
"POT-Creation-Date: 2025-10-01 16:13+0000\n" "POT-Creation-Date: 2025-12-15 16:28+0000\n"
"PO-Revision-Date: 2025-10-02 07:57+0800\n" "PO-Revision-Date: 2025-12-16 10:10+0800\n"
"Last-Translator: lumingzh <lumingzh@qq.com>\n" "Last-Translator: lumingzh <lumingzh@qq.com>\n"
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n" "Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
"Language: zh_CN\n" "Language: zh_CN\n"
@ -53,7 +53,7 @@ msgstr "分离 %s"
#. also sanitize nick, replace ':' with ' ' #. also sanitize nick, replace ':' with ' '
#. ensure the node has a description #. ensure the node has a description
#. also sanitize description, replace ':' with ' ' #. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes #. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. handle split HW node #. handle split HW node
@ -79,6 +79,7 @@ msgstr "调制解调器"
#. form factor -> icon #. form factor -> icon
#. apply properties from rules defined in JSON .conf file #. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP #. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available #. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device #. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires #. acquired at all times and destroys it if someone else acquires
@ -273,6 +274,18 @@ msgstr ""
msgid "Ducking level" msgid "Ducking level"
msgstr "回避级别" msgstr "回避级别"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr "自动检测 HDMI 设备的声道数量和位置(实验性)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "自动检测 HDMI 声道(实验性)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description #. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf #: wireplumber.conf
msgid "The camera discovery timeout in milliseconds" msgid "The camera discovery timeout in milliseconds"
@ -305,8 +318,8 @@ msgstr "监视器端口"
#. /wireplumber.settings.schema/node.features.audio.mono/description #. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf #: wireplumber.conf
msgid "Configure all audio nodes in MONO" msgid "Configure all audio device sink nodes in MONO"
msgstr "在单声道中配置所有音频节点" msgstr "在单声道中配置所有音频设备信宿节点"
#. /wireplumber.settings.schema/node.features.audio.mono/name #. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf #: wireplumber.conf

View file

@ -470,6 +470,9 @@ wireplumber.components = [
{ {
name = monitors/libcamera/create-node.lua, type = script/lua name = monitors/libcamera/create-node.lua, type = script/lua
provides = hooks.monitor.libcamera-create-node provides = hooks.monitor.libcamera-create-node
requires = [ support.export-core,
pw.client-node,
pw.node-factory.spa ]
} }
{ {
name = monitors/libcamera/enumerate-device.lua, type = script/lua name = monitors/libcamera/enumerate-device.lua, type = script/lua
@ -485,23 +488,43 @@ wireplumber.components = [
## Client access configuration hooks ## Client access configuration hooks
{ {
name = client/access-default.lua, type = script/lua name = client/select-access.lua, type = script/lua
provides = script.client.access-default provides = script.client.select-access
} }
{ {
name = client/access-portal.lua, type = script/lua name = client/find-config-access.lua, type = script/lua
provides = script.client.access-config
}
{
name = client/find-flatpak-access.lua, type = script/lua
provides = script.client.access-flatpak
}
{
name = client/find-snap-access.lua, type = script/lua
provides = script.client.access-snap
}
{
name = client/find-portal-access.lua, type = script/lua
provides = script.client.access-portal provides = script.client.access-portal
requires = [ support.portal-permissionstore ] requires = [ support.portal-permissionstore ]
} }
{ {
name = client/access-snap.lua, type = script/lua name = client/find-default-access.lua, type = script/lua
provides = script.client.access-snap provides = script.client.access-default
}
{
name = client/apply-access.lua, type = script/lua
provides = script.client.apply-access
} }
{ {
type = virtual, provides = policy.client.access type = virtual, provides = policy.client.access
wants = [ script.client.access-default, requires = [ script.client.select-access,
script.client.access-portal, script.client.access-config,
script.client.access-snap ] script.client.access-default,
script.client.apply-access ]
wants = [ script.client.access-flatpak,
script.client.access-snap,
script.client.access-portal ]
} }
## Device profile selection hooks ## Device profile selection hooks
@ -627,12 +650,17 @@ wireplumber.components = [
name = node/filter-forward-format.lua, type = script/lua name = node/filter-forward-format.lua, type = script/lua
provides = hooks.filter.forward-format provides = hooks.filter.forward-format
} }
{
name = node/filter-graph.lua, type = script/lua
provides = hooks.filter.graph
}
{ {
type = virtual, provides = policy.node type = virtual, provides = policy.node
requires = [ hooks.node.create-session-item ] requires = [ hooks.node.create-session-item ]
wants = [ hooks.node.suspend wants = [ hooks.node.suspend
hooks.stream.state hooks.stream.state
hooks.filter.forward-format ] hooks.filter.forward-format
hooks.filter.graph ]
} }
{ {
name = node/software-dsp.lua, type = script/lua name = node/software-dsp.lua, type = script/lua
@ -648,6 +676,11 @@ wireplumber.components = [
name = linking/rescan.lua, type = script/lua name = linking/rescan.lua, type = script/lua
provides = hooks.linking.rescan provides = hooks.linking.rescan
} }
{
name = linking/rescan-on-linkable.lua, type = script/lua
provides = hooks.linking.rescan-on-linkable
requires = [ hooks.linking.rescan ]
}
{ {
name = linking/find-media-role-target.lua, type = script/lua name = linking/find-media-role-target.lua, type = script/lua
provides = hooks.linking.target.find-media-role provides = hooks.linking.target.find-media-role
@ -701,7 +734,8 @@ wireplumber.components = [
requires = [ hooks.linking.rescan, requires = [ hooks.linking.rescan,
hooks.linking.target.prepare-link, hooks.linking.target.prepare-link,
hooks.linking.target.link ] hooks.linking.target.link ]
wants = [ hooks.linking.target.find-media-role, wants = [ hooks.linking.rescan-on-linkable,
hooks.linking.target.find-media-role,
hooks.linking.target.find-defined, hooks.linking.target.find-defined,
hooks.linking.target.find-audio-group, hooks.linking.target.find-audio-group,
hooks.linking.target.find-filter, hooks.linking.target.find-filter,
@ -717,10 +751,21 @@ wireplumber.components = [
provides = hooks.linking.role-based.rescan provides = hooks.linking.role-based.rescan
requires = [ api.mixer ] requires = [ api.mixer ]
} }
{
name = node/find-media-role-default-volume.lua, type = script/lua
provides = hooks.node.role-based.default-volume
requires = [ hooks.linking.role-based.rescan ]
}
{
name = linking/find-media-role-sink-target.lua, type = script/lua
provides = hooks.linking.target.find-media-role-sink
}
{ {
type = virtual, provides = policy.linking.role-based type = virtual, provides = policy.linking.role-based
requires = [ policy.linking.standard, requires = [ policy.linking.standard,
hooks.linking.role-based.rescan ] hooks.linking.role-based.rescan,
hooks.node.role-based.default-volume,
hooks.linking.target.find-media-role-sink ]
} }
## Standard policy definition ## Standard policy definition
@ -831,6 +876,12 @@ wireplumber.settings.schema = {
type = "bool" type = "bool"
default = true default = true
} }
bluetooth.profile-preference = {
name = "Bluetooth profile preference"
description = "Prefer better quality or better latency when auto-selecting profiles (only 'quality' or 'latency' values are accepted)"
type = "string"
default = "quality"
}
## Device ## Device
device.restore-profile = { device.restore-profile = {
@ -912,6 +963,12 @@ wireplumber.settings.schema = {
min = 0 min = 0
max = 60000 max = 60000
} }
monitor.alsa.autodetect-hdmi-channels = {
name = "Automatically detect HDMI channels (experimental)"
description = "Automatically detect channel count and positions for HDMI devices (experimental)"
type = "bool"
default = false
}
## Node ## Node
node.features.audio.no-dsp = { node.features.audio.no-dsp = {
@ -934,7 +991,7 @@ wireplumber.settings.schema = {
} }
node.features.audio.mono = { node.features.audio.mono = {
name = "Mono" name = "Mono"
description = "Configure all audio nodes in MONO" description = "Configure all audio device sink nodes in MONO"
type = "bool" type = "bool"
default = false default = false
} }

View file

@ -1,13 +1,80 @@
## The WirePlumber access configuration ## The WirePlumber access configuration
access.rules = [ access.permission-managers = [
# The list of access rules ## The list of access permission managers
# The following are the default rules applied if none overrides them. ## The following is an example of how to create a custom permission manager
## that removes all permissions on all Audio/Source nodes
# {
# ## The unique name of the permission manager. This is mandatory.
# name = "custom"
#
# ## The default permissions to apply on all objects that dont have a match
# default_permissions = "all"
#
# ## The permissions to apply specifically on the PipeWire core object
# ## (ID 0). This is useful to allow clients to interact with the core
# ## (e.g. enumerate objects) while restricting access to individual objects.
# ## If not set, the default_permissions value is used for the core as well.
# core_permissions = "rx"
#
# ## The rules to apply specific permissions to matched objects
# rules = [
# {
# matches = [
# {
# media.class = "Audio/Source"
# }
# ]
# actions = {
# set-permissions = "-"
# }
# }
# ]
# }
]
access.rules = [
## The list of access rules
## This rule attaches the 'custom' permission manager to paplay clients.
## Note: This is only used if there is no 'default_permissions' action.
# { # {
# matches = [ # matches = [
# { # {
# access = "flatpak" # application.name = "paplay"
# }
# ]
# actions = {
# update-props = {
# permission_manager_name = "custom"
# }
# }
# }
## This rule grants read-only permissions to paplay clients for all objects.
## Note: This has precedence over the 'permission_manager_name' action.
# {
# matches = [
# {
# application.name = "paplay"
# }
# ]
# actions = {
# update-props = {
# default_permissions = "r"
# }
# }
# }
## This rule sets the pipewire effective access to 'flatpak-manager' for
## clients with both 'flatpak' access and 'Manager' media category, and all
## grants all permissions.
## Note: WirePlumber already does this by default.
# {
# matches = [
# {
# pipewire.access = "flatpak"
# media.category = "Manager" # media.category = "Manager"
# } # }
# ] # ]
@ -19,10 +86,14 @@ access.rules = [
# } # }
# } # }
## This rule grants read-exec-only permissions to clients with 'flatpak'
## access and without 'Manager' media category.
## Note: WirePlumber already does this by default.
# { # {
# matches = [ # matches = [
# { # {
# access = "flatpak" # pipewire.access = "flatpak"
# media.category = "!\"Manager\""
# } # }
# ] # ]
# actions = { # actions = {
@ -32,10 +103,13 @@ access.rules = [
# } # }
# } # }
## This rule grants read-exec-only permissions to clients with 'restricted'
## access.
## Note: WirePlumber already does this by default.
# { # {
# matches = [ # matches = [
# { # {
# access = "restricted" # pipewire.access = "restricted"
# } # }
# ] # ]
# actions = { # actions = {
@ -45,10 +119,12 @@ access.rules = [
# } # }
# } # }
## This rule grants all permissions to clients with 'unrestricted' access.
## Note: WirePlumber already does this by default.
# { # {
# matches = [ # matches = [
# { # {
# access = "default" # pipewire.access = "unrestricted"
# } # }
# ] # ]
# actions = { # actions = {

View file

@ -0,0 +1,43 @@
node.filter-graph.rules = [
## The list of filter graph rules
## This rule example creates two filter graphs for each audio source node
# {
# matches = [
# {
# ## This matches all audio source nodes
# media.class = "Audio/Source"
# }
# ]
# actions = {
# create-filter-graph = [
# ## Multiple filter graphs can be defined here.
# ## The syntax is the same as the pipewire filter-chain conf files.
#
# ## This is an example of a bultin passthrough filter
# {
# nodes = [
# {
# type = builtin
# label = copy
# name = passthrough
# }
# ]
# }
#
# ## This is an example of a LADSPA rnnoise filter
# {
# nodes = [
# {
# type = ladspa
# name = rnnoise
# plugin = librnnoise_ladspa
# label = noise_suppressor_stereo
# }
# ]
# }
# ]
# }
# }
]

View file

@ -150,6 +150,7 @@ wireplumber.components = [
policy.role-based.priority = 100 policy.role-based.priority = 100
policy.role-based.action.same-priority = "mix" policy.role-based.action.same-priority = "mix"
policy.role-based.action.lower-priority = "cork" policy.role-based.action.lower-priority = "cork"
policy.role-based.preferred-target = "Speaker"
} }
} }
provides = loopback.sink.role.alert provides = loopback.sink.role.alert

View file

@ -19,5 +19,25 @@ monitor.alsa.rules = [
api.alsa.headroom = 2048 api.alsa.headroom = 2048
} }
} }
},
# VMware & VirtualBox on Windows hosts require more headroom to
# avoid stuttering.
{
matches = [
{
node.name = "~alsa_input.pci.*"
cpu.vm.name = "~^(vmware)|(oracle)$"
}
{
node.name = "~alsa_output.pci.*"
cpu.vm.name = "~^(vmware)|(oracle)$"
}
]
actions = {
update-props = {
api.alsa.period-size = 1024
api.alsa.headroom = 8192
}
}
} }
] ]

View file

@ -1,89 +0,0 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
log = Log.open_topic ("s-client")
config = {}
config.rules = Conf.get_section_as_json ("access.rules")
function getAccess (properties)
local access = properties["pipewire.access"]
local client_access = properties["pipewire.client.access"]
local is_flatpak = properties["pipewire.sec.flatpak"]
if is_flatpak then
client_access = "flatpak"
end
if client_access == nil then
return access
elseif access == "unrestricted" or access == "default" then
if client_access ~= "unrestricted" then
return client_access
end
end
return access
end
function getDefaultPermissions (properties)
local access = properties["access"]
local media_category = properties["media.category"]
if access == "flatpak" and media_category == "Manager" then
return "all", "flatpak-manager"
elseif access == "flatpak" or access == "restricted" then
return "rx", access
elseif access == "default" then
return "all", access
end
return nil, nil
end
function getPermissions (properties)
if config.rules then
local mprops, matched = JsonUtils.match_rules_update_properties (
config.rules, properties)
if (matched > 0 and mprops["default_permissions"]) then
return mprops["default_permissions"], mprops["access"]
end
end
return nil, nil
end
clients_om = ObjectManager {
Interest { type = "client" }
}
clients_om:connect("object-added", function (om, client)
local id = client["bound-id"]
local properties = client["properties"]
local access = getAccess (properties)
properties["access"] = access
local perms, effective_access = getPermissions (properties)
if perms == nil then
perms, effective_access = getDefaultPermissions (properties)
end
if effective_access == nil then
effective_access = access
end
if perms ~= nil then
log:info(client, "Granting permissions to client " .. id .. " (access " ..
effective_access .. "): " .. perms)
client:update_permissions { ["any"] = perms }
client:update_properties { ["pipewire.access.effective"] = effective_access }
else
log:debug(client, "No rule for client " .. id .. " (access " .. access .. ")")
end
end)
clients_om:activate()

View file

@ -1,143 +0,0 @@
MEDIA_ROLE_NONE = 0
MEDIA_ROLE_CAMERA = 1 << 0
log = Log.open_topic ("s-client")
function hasPermission (permissions, app_id, lookup)
if permissions then
for key, values in pairs(permissions) do
if key == app_id then
for _, v in pairs(values) do
if v == lookup then
return true
end
end
end
end
end
return false
end
function parseMediaRoles (media_roles_str)
local media_roles = MEDIA_ROLE_NONE
for role in media_roles_str:gmatch('[^,%s]+') do
if role == "Camera" then
media_roles = media_roles | MEDIA_ROLE_CAMERA
end
end
return media_roles
end
function setPermissions (client, allow_client, allow_nodes)
local client_id = client["bound-id"]
log:info(client, "Granting ALL access to client " .. client_id)
-- Update permissions on client
client:update_permissions { [client_id] = allow_client and "all" or "-" }
-- Update permissions on camera source nodes
for node in nodes_om:iterate() do
local node_id = node["bound-id"]
client:update_permissions { [node_id] = allow_nodes and "all" or "-" }
end
end
function updateClientPermissions (client, permissions)
local client_id = client["bound-id"]
local str_prop = nil
local app_id = nil
local media_roles = nil
local allowed = false
-- Make sure the client is not the portal itself
str_prop = client.properties["pipewire.access.portal.is_portal"]
if str_prop == "yes" then
log:info (client, "client is the portal itself")
return
end
-- Make sure the client has a portal app Id
str_prop = client.properties["pipewire.access.portal.app_id"]
if str_prop == nil then
log:info (client, "Portal managed client did not set app_id")
return
end
if str_prop == "" then
log:info (client, "Ignoring portal check for non-sandboxed client")
setPermissions (client, true, true)
return
end
app_id = str_prop
-- Make sure the client has portal media roles
str_prop = client.properties["pipewire.access.portal.media_roles"]
if str_prop == nil then
log:info (client, "Portal managed client did not set media_roles")
return
end
media_roles = parseMediaRoles (str_prop)
if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
log:info (client, "Ignoring portal check for clients without camera role")
return
end
-- Update permissions
allowed = hasPermission (permissions, app_id, "yes")
log:info (client, "setting permissions: " .. tostring(allowed))
setPermissions (client, allowed, allowed)
end
-- Create portal clients object manager
clients_om = ObjectManager {
Interest {
type = "client",
Constraint { "pipewire.access", "=", "portal" },
}
}
-- Set permissions to portal clients from the permission store if loaded
pps_plugin = Plugin.find("portal-permissionstore")
if pps_plugin then
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.role", "=", "Camera" },
Constraint { "media.class", "=", "Video/Source" },
}
}
nodes_om:activate()
clients_om:connect("object-added", function (om, client)
local new_perms = pps_plugin:call("lookup", "devices", "camera");
updateClientPermissions (client, new_perms)
end)
nodes_om:connect("object-added", function (om, node)
local new_perms = pps_plugin:call("lookup", "devices", "camera");
for client in clients_om:iterate() do
updateClientPermissions (client, new_perms)
end
end)
pps_plugin:connect("changed", function (p, table, id, deleted, permissions)
if table == "devices" or id == "camera" then
for app_id, _ in pairs(permissions) do
for client in clients_om:iterate {
Constraint { "pipewire.access.portal.app_id", "=", app_id }
} do
updateClientPermissions (client, permissions)
end
end
end
end)
else
-- Otherwise, just set all permissions to all portal clients
clients_om:connect("object-added", function (om, client)
local id = client["bound-id"]
log:info(client, "Granting ALL access to client " .. id)
client:update_permissions { ["any"] = "all" }
end)
end
clients_om:activate()

View file

@ -1,87 +0,0 @@
-- Manage snap audio permissions
--
-- Copyright © 2023 Canonical Ltd.
-- @author Sergio Costas Rodriguez <sergio.costas@canonical.com>
--
-- SPDX-License-Identifier: MIT
function removeClientPermissionsForOtherClients (client)
-- Remove access to any other clients, but allow all the process of the
-- same snap to access their elements
local client_id = client.properties["pipewire.snap.id"]
for snap_client in clients_snap:iterate() do
local snap_client_id = snap_client.properties["pipewire.snap.id"]
if snap_client_id ~= client_id then
client:update_permissions { [snap_client["bound-id"]] = "-" }
end
end
for no_snap_client in clients_no_snap:iterate() do
client:update_permissions { [no_snap_client["bound-id"]] = "-" }
end
end
function updateClientPermissions (client)
-- Remove access to Audio/Sources and Audio/Sinks based on snap permissions
for node in nodes_om:iterate() do
local node_id = node["bound-id"]
local property = "pipewire.snap.audio.playback"
if node.properties["media.class"] == "Audio/Source" then
property = "pipewire.snap.audio.record"
end
if client.properties[property] ~= "true" then
client:update_permissions { [node_id] = "-" }
end
end
end
clients_snap = ObjectManager {
Interest {
type = "client",
Constraint { "pipewire.snap.id", "+", type = "pw"},
}
}
clients_no_snap = ObjectManager {
Interest {
type = "client",
Constraint { "pipewire.snap.id", "-", type = "pw"},
}
}
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*"}
}
}
clients_snap:connect("object-added", function (om, client)
-- If a new snap client is added, adjust its permissions
updateClientPermissions (client)
removeClientPermissionsForOtherClients (client)
end)
clients_no_snap:connect("object-added", function (om, client)
-- If a new, non-snap client is added,
-- remove access to it from other snaps
client_id = client["bound-id"]
for snap_client in clients_snap:iterate() do
if client.properties["pipewire.snap.id"] ~= nil then
snap_client:update_permissions { [client_id] = "-" }
end
end
end)
nodes_om:connect("object-added", function (om, node)
-- If a new Audio/Sink or Audio/Source node is added,
-- adjust the permissions in the snap clients
for client in clients_snap:iterate() do
updateClientPermissions (client)
end
end)
clients_snap:activate()
clients_no_snap:activate()
nodes_om:activate()

View file

@ -0,0 +1,73 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Applies effective access and permissions to clients.
log = Log.open_topic ("s-client")
AsyncEventHook {
name = "client/apply-access",
after = { "client/find-config-access", "client/find-default-access" },
interests = {
EventInterest {
Constraint { "event.type", "=", "select-access" },
},
},
steps = {
start = {
next = "none",
execute = function (event, transition)
local client = event:get_subject ()
local app_name = client:get_property ("application.name")
local effective_access = event:get_data ("effective-access")
local default_permissions = event:get_data ("default-permissions")
local permission_manager = event:get_data ("permission-manager")
log:debug (client, string.format ("handling client '%s'", app_name))
-- Set effective access if any
if effective_access ~= nil then
client:update_properties {
["pipewire.access.effective"] = effective_access
}
log:info (client, string.format (
"Updated effective access on client '%s' to '%s'", app_name,
effective_access))
end
-- Set defaut permissions if any, otherwise check permission manager
if default_permissions ~= nil then
client:update_permissions { ["any"] = default_permissions }
log:info (client, string.format (
"Updated default permissions on client '%s' to '%s'", app_name,
default_permissions))
transition:advance ()
elseif permission_manager ~= nil then
-- Make sure the permission manager is activated
permission_manager:activate (Features.ALL, function (pm, e)
if e then
transition:return_error (string.format (
"failed to activate permission manager for client '%s': %s",
app_name, e))
return
end
-- Attach permission manager to client so permissions are applied
client:attach_permission_manager (permission_manager)
log:info (client, string.format (
"Attached permission manager to client '%s'", app_name))
transition:advance ()
end)
else
log:info (client, string.format (
"Handled client '%s' without updating permissions", app_name))
transition:advance ()
end
end,
},
},
}:register()

View file

@ -0,0 +1,120 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Evaluates whether the client is eligible for config access or not.
cutils = require ("common-utils")
log = Log.open_topic ("s-client")
config = {}
config.rules = Conf.get_section_as_json ("access.rules", Json.Array {})
config.permission_managers = Conf.get_section_as_json (
"access.permission-managers", Json.Array {})
-- Create the config permission managers
permission_managers = {}
config_pm_table = config.permission_managers:parse (2)
for _, pm_info in ipairs (config_pm_table) do
if pm_info.name == nil then
log:warning ("Config permission manager does not have a name, ignoring...")
goto skip_pm
end
local config_pm = PermissionManager ()
-- Set default permissions if defined
if pm_info.default_permissions ~= nil then
config_pm:set_default_permissions (pm_info.default_permissions)
end
-- Set core permissions if defined
if pm_info.core_permissions ~= nil then
config_pm:set_core_permissions (pm_info.core_permissions)
end
-- Set rules match if defined
if pm_info.rules ~= nil then
config_pm:add_rules_match (Json.Raw (pm_info.rules))
end
-- Add it to the table
permission_managers[pm_info.name] = config_pm
log:debug ("Added config permission manager: " .. pm_info.name)
::skip_pm::
end
SimpleEventHook {
name = "client/find-config-access",
before = { "client/find-default-access", "client/apply-access" },
interests = {
EventInterest {
Constraint { "event.type", "=", "select-access" },
},
},
execute = function (event)
local client = event:get_subject ()
local app_name = client:get_property ("application.name")
local effective_access = event:get_data ("effective-access")
local default_permissions = event:get_data ("default-permissions")
local permission_manager = event:get_data ("permission-manager")
log:debug (client, string.format ("handling client '%s'", app_name))
-- We keep backward compatibility to allow matching on 'access' property
local client_properties = client.properties
local access = cutils.get_client_access (client_properties)
client_properties["access"] = access
-- Update the client propst to get the config access, perms and PM
local updated_props = JsonUtils.match_rules_update_properties (
config.rules, client_properties)
local config_access = updated_props["access"]
local config_default_perms = updated_props["default_permissions"]
local config_pm_name = updated_props["permission_manager_name"]
-- Show warning if both config_default_perms and config_pm_name are defined
if config_default_perms ~= nil and config_pm_name ~= nil then
log:warning (client, string.format (
"Ignoring 'permission_manager_name' property for client '%s'",
app_name))
end
-- Check effective access if never set before
if effective_access == nil and config_access ~= nil then
log:info (client, string.format (
"Found config %s effective-access for client '%s'",
config_access, app_name))
event:set_data ("effective-access", config_access)
end
-- Check default permissions if never set before
if default_permissions == nil and config_default_perms ~= nil then
log:info (client, string.format (
"Found config '%s' default-permissions for client '%s'",
config_default_perms, app_name))
event:set_data ("default-permissions", config_default_perms)
end
-- check permission manager if never set before
if permission_manager == nil and config_default_perms == nil
and config_pm_name ~= nil then
local config_pm = permission_managers [config_pm_name]
if config_pm ~= nil then
log:info (client, string.format (
"Found config '%s' PM for client '%s'",
config_pm_name, app_name))
event:set_data ("permission-manager", config_pm)
else
log:warning (client, string.format (
"Could not find config '%s' PM for client '%s'",
config_pm_name, app_name))
end
end
end
}:register()

View file

@ -0,0 +1,61 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Evaluates whether the client is eligible for default access or not.
cutils = require ("common-utils")
log = Log.open_topic ("s-client")
-- The default permission manager
default_pm = PermissionManager ()
default_pm:set_default_permissions (Perm.ALL)
-- The default-restricted permission manager
default_restricted_pm = PermissionManager ()
default_restricted_pm:set_default_permissions (Perm.RX)
SimpleEventHook {
name = "client/find-default-access",
before = "client/apply-access",
after = "client/find-config-access",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-access" },
},
},
execute = function (event)
local client = event:get_subject ()
local app_name = client:get_property ("application.name")
local permission_manager = event:get_data ("permission-manager")
local effective_access = event:get_data ("effective-access")
log:debug (client, string.format ("handling client '%s'", app_name))
-- Check effective access if never set before
if effective_access == nil then
local access = cutils.get_client_access (client.properties)
if access ~= nil then
log:info (client, string.format (
"Found default %s effective-access for client '%s'", access, app_name))
event:set_data ("effective-access", access)
end
end
-- Check permission manager if never set before
if permission_manager == nil then
if access == "restricted" then
log:info (client, string.format (
"Found default-restricted PM for client '%s'", app_name))
event:set_data ("permission-manager", default_restricted_pm)
else
log:info (client, string.format (
"Found default PM for client '%s'", app_name))
event:set_data ("permission-manager", default_pm)
end
end
end
}:register()

View file

@ -0,0 +1,69 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Evaluates whether the client is eligible for flatpak access or not.
cutils = require ("common-utils")
log = Log.open_topic ("s-client")
-- The flatpack-manager permission manager
flatpack_manager_pm = PermissionManager ()
flatpack_manager_pm:set_default_permissions (Perm.ALL)
-- The flatpack permission manager
flatpack_pm = PermissionManager ()
flatpack_pm:set_default_permissions (Perm.RX)
SimpleEventHook {
name = "client/find-flatpak-access",
before = "client/find-default-access",
after = "client/find-config-access",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-access" },
},
},
execute = function (event)
local client = event:get_subject ()
local app_name = client:get_property ("application.name")
local access = cutils.get_client_access (client.properties)
local media_category = client:get_property ("media.category")
local permission_manager = event:get_data ("permission-manager")
local effective_access = event:get_data ("effective-access")
log:debug (client, string.format ("handling client '%s'", app_name))
-- Check effective access if never set before
if effective_access == nil then
if access == "flatpak" and media_category == "Manager" then
effective_access = "flatpak-manager"
elseif access == "flatpak" then
effective_access = "flatpak"
end
if effective_access ~= nil then
log:info (client, string.format (
"Found %s effective-access for client '%s'",
effective_access, app_name))
event:set_data ("effective-access", effective_access)
end
end
-- Check permission manager if never set before
if permission_manager == nil then
if access == "flatpak" and media_category == "Manager" then
log:info (client, string.format (
"Found flatpak-manager PM for client '%s'", app_name))
event:set_data ("permission-manager", flatpack_manager_pm)
elseif access == "flatpak" then
log:info (client, string.format (
"Found flatpak PM for client '%s'", app_name))
event:set_data ("permission-manager", flatpack_pm)
end
end
end
}:register()

View file

@ -0,0 +1,161 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Evaluates whether the client is eligible for portal access or not.
log = Log.open_topic ("s-client")
pps_plugin = Plugin.find("portal-permissionstore")
cached_camera_permissions = nil
camera_permissions_loaded = false
MEDIA_ROLE_NONE = 0
MEDIA_ROLE_CAMERA = 1 << 0
function hasPermission (permissions, app_id, lookup)
if permissions then
for key, values in pairs(permissions) do
if key == app_id then
for _, v in pairs(values) do
if v == lookup then
return true
end
end
end
end
end
return false
end
function parseMediaRoles (media_roles_str)
local media_roles = MEDIA_ROLE_NONE
for role in media_roles_str:gmatch('[^,%s]+') do
if role == "Camera" then
media_roles = media_roles | MEDIA_ROLE_CAMERA
end
end
return media_roles
end
function getCameraPermissions ()
if not camera_permissions_loaded then
cached_camera_permissions = pps_plugin:call("lookup", "devices", "camera")
camera_permissions_loaded = true
end
return cached_camera_permissions
end
-- The portal permission manager
portal_pm = PermissionManager ()
portal_pm:set_default_permissions (Perm.ALL)
-- Add interest in camera video source nodes
portal_pm:add_interest_match (
function (_, client, _)
local client_id = client["bound-id"]
local str_prop = nil
local app_id = nil
local media_roles = nil
local allowed = false
-- Give all permissions if portal-permissionstore plugin is not loaded
if pps_plugin == nil then
log:info (client, "Portal permission store plugin not loaded")
return Perm.ALL
end
-- Give all permissions to the portal itself
str_prop = client:get_property ("pipewire.access.portal.is_portal")
if str_prop == "yes" then
log:info (client, "client is the portal itself")
return Perm.ALL
end
-- Give all permissions to clients without portal App ID
str_prop = client:get_property ("pipewire.access.portal.app_id")
if str_prop == nil then
log:info (client, "Portal managed client did not set app_id")
return Perm.ALL
end
-- Ignore portal check for non-sandboxed client
if str_prop == "" then
log:info (client, "Ignoring portal check for non-sandboxed client")
return Perm.ALL
end
app_id = str_prop
-- Make sure the client has portal media roles
str_prop = client:get_property ("pipewire.access.portal.media_roles")
if str_prop == nil then
log:info (client, "Portal managed client did not set media_roles")
return Perm.ALL
end
-- Give all permissions to clients without camera role
media_roles = parseMediaRoles (str_prop)
if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
log:info (client, "Ignoring portal check for clients without camera role")
return Perm.ALL
end
-- Check whether the client has allowed access or not
local permissions = getCameraPermissions ()
allowed = hasPermission (permissions, app_id, "yes")
-- Return the allowed or not allowed permissions
log:info (client, "Setting portal camera permissions to " ..
(allowed and "all" or "none"))
return allowed and Perm.ALL or Perm.NONE
end,
Interest {
type = "node",
Constraint { "media.role", "=", "Camera" },
Constraint { "media.class", "=", "Video/Source" },
}
)
-- Listen for changes and update permissions when that happens
if pps_plugin ~= nil then
pps_plugin:connect("changed", function (p, table, id, deleted, permissions)
if table == "devices" or id == "camera" then
cached_camera_permissions = permissions
camera_permissions_loaded = true
portal_pm:update_permissions ()
end
end)
end
SimpleEventHook {
name = "client/find-portal-access",
before = "client/find-default-access",
after = "client/find-config-access",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-access" },
},
},
execute = function (event)
local client = event:get_subject ()
local app_name = client:get_property ("application.name")
local permission_manager = event:get_data ("permission-manager")
log:debug (client, string.format ("handling client '%s'", app_name))
-- Bypass the hook if the permission manager is already picked up
if permission_manager ~= nil then
return
end
local access = client:get_property ("pipewire.access")
if access == "portal" then
log:info (client, string.format (
"Found portal PM for client '%s'", app_name))
event:set_data ("permission-manager", portal_pm)
end
end
}:register()

View file

@ -0,0 +1,89 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Evaluates whether the client is eligible for snap access or not.
log = Log.open_topic ("s-client")
-- The snap permission manager
snap_pm = PermissionManager ()
snap_pm:set_default_permissions (Perm.ALL)
-- Always remove permissions for all non-snap clients
snap_pm:add_interest_match_simple (Perm.NONE,
Interest {
type = "client",
Constraint { "pipewire.snap.id", "-", type = "pw"},
}
)
-- Always remove permissions for all snap clients with different snap_id
snap_pm:add_interest_match (
function (_, client, object)
client_snap_id = client:get_property ("pipewire.snap.id")
object_snap_id = object:get_property ("pipewire.snap.id")
return client_snap_id == object_snap_id and Perm.ALL or Perm.NONE
end,
Interest {
type = "client",
Constraint { "pipewire.snap.id", "+", type = "pw"},
}
)
-- Check playback node permissions
snap_pm:add_interest_match (
function (_, client, _)
local allowed = client.properties:get_boolean ("pipewire.snap.audio.playback")
return allowed and Perm.ALL or Perm.NONE
end,
Interest {
type = "node",
Constraint { "media.class", "=", "Audio/Sink"}
}
)
-- Check record node permissions
snap_pm:add_interest_match (
function (_, client, _)
local allowed = client.properties:get_boolean ("pipewire.snap.audio.record")
return allowed and Perm.ALL or Perm.NONE
end,
Interest {
type = "node",
Constraint { "media.class", "=", "Audio/Source"}
}
)
SimpleEventHook {
name = "client/find-snap-access",
before = "client/find-default-access",
after = "client/find-config-access",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-access" },
},
},
execute = function (event)
local client = event:get_subject ()
local app_name = client:get_property ("application.name")
local permission_manager = event:get_data ("permission-manager")
log:debug (client, string.format ("handling client '%s'", app_name))
-- Bypass the hook if the permission manager is already picked up
if permission_manager ~= nil then
return
end
local snap_id = client:get_property ("pipewire.snap.id")
if snap_id ~= nil then
log:info (client, string.format (
"Found snap PM for client '%s'", app_name))
event:set_data ("permission-manager", snap_pm)
end
end
}:register()

View file

@ -0,0 +1,21 @@
-- WirePlumber
--
-- Copyright © 2026 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Triggers select-access event for added clients.
SimpleEventHook {
name = "client/select-access-trigger",
interests = {
EventInterest {
Constraint { "event.type", "=", "client-added" },
},
},
execute = function(event)
local source = event:get_source ()
local client = event:get_subject ()
source:call ("push-event", "select-access", client, nil)
end
}:register()

View file

@ -24,6 +24,9 @@ SimpleEventHook {
local om = source:call ("get-object-manager", "metadata") local om = source:call ("get-object-manager", "metadata")
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } } local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
if metadata == nil then
return
end
if selected_node then if selected_node then
local key = "default." .. def_node_type local key = "default." .. def_node_type

View file

@ -18,6 +18,8 @@ SimpleEventHook {
}, },
}, },
execute = function (event) execute = function (event)
local props = event:get_properties ()
local def_node_type = props ["default-node.type"]
local available_nodes = event:get_data ("available-nodes") local available_nodes = event:get_data ("available-nodes")
local selected_prio = event:get_data ("selected-node-priority") or 0 local selected_prio = event:get_data ("selected-node-priority") or 0
local selected_route_prio = event:get_data ("selected-route-priority") or 0 local selected_route_prio = event:get_data ("selected-route-priority") or 0
@ -37,6 +39,12 @@ SimpleEventHook {
-- Highest priority node wins -- Highest priority node wins
local priority = nutils.get_session_priority (node_props) local priority = nutils.get_session_priority (node_props)
local route_priority = nutils.get_route_priority (node_props) local route_priority = nutils.get_route_priority (node_props)
local media_class = node_props ["media.class"]
-- Never consider sink nodes as best if audio.source is the def node type
if media_class == "Audio/Sink" and def_node_type == "audio.source" then
goto skip_node
end
if selected_node == nil or if selected_node == nil or
priority > selected_prio or priority > selected_prio or
@ -46,6 +54,8 @@ SimpleEventHook {
selected_route_prio = route_priority selected_route_prio = route_priority
selected_node = node_props ["node.name"] selected_node = node_props ["node.name"]
end end
::skip_node::
end end
event:set_data ("selected-node-priority", selected_prio) event:set_data ("selected-node-priority", selected_prio)

View file

@ -55,9 +55,14 @@ AsyncEventHook {
-- ensure default values -- ensure default values
local is_input = (route_info.direction == "Input") local is_input = (route_info.direction == "Input")
props.mute = props.mute or false props.mute = props.mute or false
props.channelVolumes = props.channelVolumes or props.channelVolumes = props.channelVolumes or {
{ is_input and Settings.get_float ("device.routes.default-source-volume") -- See if we have a per-device override
or Settings.get_float ("device.routes.default-sink-volume") } (is_input and tonumber(device.properties["device.routes.default-source-volume"]))
or tonumber(device.properties["device.routes.default-sink-volume"])
-- Otherwise we use the global default
or (is_input and Settings.get_float ("device.routes.default-source-volume"))
or Settings.get_float ("device.routes.default-sink-volume")
}
-- prefix the props with correct IDs to create a Pod.Object -- prefix the props with correct IDs to create a Pod.Object
table.insert (props, 1, "Spa:Pod:Object:Param:Props") table.insert (props, 1, "Spa:Pod:Object:Param:Props")

View file

@ -7,6 +7,7 @@
cutils = require ("common-utils") cutils = require ("common-utils")
log = Log.open_topic ("s-automute-alsa-routes") log = Log.open_topic ("s-automute-alsa-routes")
hooks_registered = false
function setRoute (device, route, mute) function setRoute (device, route, mute)
local param = Pod.Object { local param = Pod.Object {
@ -194,17 +195,19 @@ evaluate_mute_on_node_removed_hook = SimpleEventHook {
function toggleState () function toggleState ()
local mute_alsa = Settings.get_boolean ("device.routes.mute-on-alsa-playback-removed") local mute_alsa = Settings.get_boolean ("device.routes.mute-on-alsa-playback-removed")
local mute_bluez = Settings.get_boolean ("device.routes.mute-on-bluetooth-playback-removed") local mute_bluez = Settings.get_boolean ("device.routes.mute-on-bluetooth-playback-removed")
if mute_alsa or mute_bluez then if (mute_alsa or mute_bluez) and not hooks_registered then
nodes_info = {} nodes_info = {}
mute_alsa_devices_hook:register () mute_alsa_devices_hook:register ()
update_nodes_info_hook:register () update_nodes_info_hook:register ()
evaluate_mute_on_device_route_changed_hook:register () evaluate_mute_on_device_route_changed_hook:register ()
evaluate_mute_on_node_removed_hook:register () evaluate_mute_on_node_removed_hook:register ()
else hooks_registered = true
elseif not mute_alsa and not mute_bluez and hooks_registered then
mute_alsa_devices_hook:remove () mute_alsa_devices_hook:remove ()
update_nodes_info_hook:remove () update_nodes_info_hook:remove ()
evaluate_mute_on_device_route_changed_hook:remove () evaluate_mute_on_device_route_changed_hook:remove ()
evaluate_mute_on_node_removed_hook:remove () evaluate_mute_on_node_removed_hook:remove ()
hooks_registered = false
end end
end end

View file

@ -28,43 +28,25 @@
lutils = require ("linking-utils") lutils = require ("linking-utils")
cutils = require ("common-utils") cutils = require ("common-utils")
log = Log.open_topic ("s-device") log = Log.open_topic ("s-device")
persistent_storage_hooks_registered = false
autoswitch_hooks_registered = false
state = nil local PROFILE_RESTORE_TIMEOUT_MSEC = 2000
headset_profiles = nil local PROFILE_SWITCH_TIMEOUT_MSEC = 500
local profile_restore_timeout_msec = 2000 local state = nil
local profile_switch_timeout_msec = 500 local headset_profiles = {}
local non_headset_profiles = {}
local INVALID = -1 local capture_stream_links = {}
local restore_timeout_source = {} local restore_timeout_source = {}
local switch_timeout_source = {} local switch_timeout_source = {}
local last_profiles = {} function saveHeadsetProfile (device, profile_name, persistent)
local active_streams = {}
local previous_streams = {}
function handlePersistentSetting (enable)
if enable and state == nil then
-- the state storage
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
and State ("bluetooth-autoswitch") or nil
headset_profiles = state and state:load () or {}
else
state = nil
headset_profiles = nil
end
end
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
end)
function saveHeadsetProfile (device, profile_name)
local key = "saved-headset-profile:" .. device.properties ["device.name"] local key = "saved-headset-profile:" .. device.properties ["device.name"]
headset_profiles [key] = profile_name headset_profiles [key] = profile_name
state:save_after_timeout (headset_profiles) if state ~= nil and persistent then
state:save_after_timeout (headset_profiles)
end
end end
function getSavedHeadsetProfile (device) function getSavedHeadsetProfile (device)
@ -72,85 +54,36 @@ function getSavedHeadsetProfile (device)
return headset_profiles [key] return headset_profiles [key]
end end
function saveLastProfile (device, profile_name) function saveNonHeadsetProfile (device, profile_name)
last_profiles [device.properties ["device.name"]] = profile_name non_headset_profiles [device.properties ["device.name"]] = profile_name
end end
function getSavedLastProfile (device) function getSavedNonHeadsetProfile (device)
return last_profiles [device.properties ["device.name"]] return non_headset_profiles [device.properties ["device.name"]]
end
function isSwitchedToHeadsetProfile (device)
return getSavedLastProfile (device) ~= nil
end end
function findProfile (device, index, name) function findProfile (device, index, name)
for p in device:iterate_params ("EnumProfile") do for p in device:iterate_params ("EnumProfile") do
local profile = cutils.parseParam (p, "EnumProfile") local profile = cutils.parseParam (p, "EnumProfile")
if not profile then if profile ~= nil then
goto skip_enum_profile if (index ~= nil and profile.index == index) or
end (name ~= nil and profile.name == name) then
return profile
log:debug ("Profile name: " .. profile.name .. ", priority: " end
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile.priority, profile.index, profile.name
end
::skip_enum_profile::
end
return INVALID, INVALID, nil
end
function getCurrentProfile (device)
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
if profile then
return profile.name
end end
end end
return nil return nil
end end
function highestPrioProfileWithInputRoute (device) function getCurrentProfile (device)
local profile_priority = INVALID for p in device:iterate_params ("Profile") do
local profile_index = INVALID local profile = cutils.parseParam (p, "Profile")
local profile_name = nil if profile then
return profile
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
-- Parse pod
if not route then
goto skip_enum_route
end end
if route.direction ~= "Input" then
goto skip_enum_route
end
log:debug ("Route with index: " .. tostring (route.index) .. ", direction: "
.. route.direction .. ", name: " .. route.name .. ", description: "
.. route.description .. ", priority: " .. route.priority)
if route.profiles then
for _, v in pairs (route.profiles) do
local priority, index, name = findProfile (device, v)
if priority ~= INVALID then
if profile_priority < priority then
profile_priority = priority
profile_index = index
profile_name = name
end
end
end
end
::skip_enum_route::
end end
return nil
return profile_priority, profile_index, profile_name
end end
function hasProfileInputRoute (device, profile_index) function hasProfileInputRoute (device, profile_index)
@ -167,6 +100,51 @@ function hasProfileInputRoute (device, profile_index)
return false return false
end end
function isHeadsetProfile (device, profile)
if hasProfileInputRoute (device, profile.index) and
(string.find (profile.name, "^headset%-head%-unit") or profile.name == "bap-duplex") then
return true
else
return false
end
end
function highestPrioHeadsetProfile (device)
local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
if route ~= nil and route.profiles ~= nil and route.direction == "Input" then
for _, v in pairs (route.profiles) do
local p = findProfile (device, v)
if p ~= nil and isHeadsetProfile (device, p) then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
end
end
end
end
end
return found_profile
end
function highestPrioNonHeadsetProfile (device)
local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
for _, v in pairs (route.profiles) do
local p = findProfile (device, v)
if p ~= nil and not isHeadsetProfile (device, p) then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
end
end
end
end
end
return found_profile
end
function switchDeviceToHeadsetProfile (dev_id, device_om) function switchDeviceToHeadsetProfile (dev_id, device_om)
-- Find the actual device -- Find the actual device
local device = device_om:lookup { local device = device_om:lookup {
@ -177,49 +155,43 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
return return
end end
local cur_profile_name = getCurrentProfile (device) -- Do not switch if the current profile is already a headset profile
local priority, index, name = findProfile (device, nil, cur_profile_name) local cur_profile = getCurrentProfile (device)
if hasProfileInputRoute (device, index) then if cur_profile ~= nil and isHeadsetProfile (device, cur_profile) then
log:info ("Current profile has input route, not switching") log:info (device,
"Current profile is already a headset profile, no need to switch")
return
elseif cur_profile == nil then
log:info (device, "Could not get current profile, not switching")
return return
end end
if isSwitchedToHeadsetProfile (device) then -- Get saved headset profile if any, otherwise find the highest priority one
log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP") local profile = nil
return local profile_name = getSavedHeadsetProfile (device)
end if profile_name ~= nil then
profile = findProfile (device, nil, profile_name)
local saved_headset_profile = getSavedHeadsetProfile (device) if profile ~= nil and not isHeadsetProfile (device, profile) then
saveHeadsetProfile (device, nil, false)
index = INVALID profile = nil
if saved_headset_profile then
priority, index, name = findProfile (device, nil, saved_headset_profile)
if index ~= INVALID and not hasProfileInputRoute (device, index) then
index = INVALID
saveHeadsetProfile (device, nil)
end end
end end
if index == INVALID then if profile == nil then
priority, index, name = highestPrioProfileWithInputRoute (device) profile = highestPrioHeadsetProfile (device)
end end
if index ~= INVALID then -- Switch if headset profile was found
if profile ~= nil then
local pod = Pod.Object { local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile", "Spa:Pod:Object:Param:Profile", "Profile",
index = index index = profile.index,
save = false
} }
log:info (device, "Switching profile from: " .. cur_profile.name
-- store the current profile (needed when restoring) .. " to: " .. profile.name)
saveLastProfile (device, cur_profile_name)
-- switch to headset profile
log:info ("Setting profile of '"
.. device.properties ["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params ("Profile", pod) device:set_params ("Profile", pod)
else else
log:warning ("Got invalid index when switching profile") log:warning ("Could not find valid headset profile, not switching")
end end
end end
@ -233,142 +205,139 @@ function restoreProfile (dev_id, device_om)
return return
end end
if not isSwitchedToHeadsetProfile (device) then -- Do not restore if the current profile is already a non-headset profile
log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP") local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil and not isHeadsetProfile (device, cur_profile) then
log:info (device,
"Current profile is already a non-headset profile, no need to restore")
return
elseif cur_profile == nil then
log:info (device, "Could not get current profile, not switching")
return return
end end
local profile_name = getSavedLastProfile (device) -- Get saved non-headset profile if any, otherwise find the highest priority one
local cur_profile_name = getCurrentProfile (device) local profile = nil
local priority, index, name local profile_name = getSavedNonHeadsetProfile (device)
if profile_name ~= nil then
if cur_profile_name then profile = findProfile (device, nil, profile_name)
priority, index, name = findProfile (device, nil, cur_profile_name) if profile ~= nil and isHeadsetProfile (device, profile) then
saveNonHeadsetProfile (device, nil)
if index ~= INVALID and hasProfileInputRoute (device, index) then profile = nil
log:info ("Setting saved headset profile to: " .. cur_profile_name)
saveHeadsetProfile (device, cur_profile_name)
end end
end end
if profile == nil then
profile = highestPrioNonHeadsetProfile (device)
end
if profile_name then -- Restore if non-headset profile was found
priority, index, name = findProfile (device, nil, profile_name) if profile ~= nil then
local pod = Pod.Object {
if index ~= INVALID then "Spa:Pod:Object:Param:Profile", "Profile",
local pod = Pod.Object { index = profile.index,
"Spa:Pod:Object:Param:Profile", "Profile", save = false
index = index }
} log:info (device, "Restoring profile from: " .. cur_profile.name
.. " to: " .. profile.name)
-- clear last profile as we will restore it now device:set_params ("Profile", pod)
saveLastProfile (device, nil) else
log:warning ("Could not find valid non-headset profile, not switching")
-- restore previous profile
log:info ("Restoring profile of '"
.. device.properties ["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params ("Profile", pod)
else
log:warning ("Failed to restore profile")
end
end end
end end
function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om) function triggerSwitchDeviceToHeadsetProfile (source, dev_id)
-- Always clear any pending restore/switch callbacks when triggering a new switch -- Always clear any pending restore/switch callbacks when triggering a new switch
if restore_timeout_source[dev_id] ~= nil then if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy () restore_timeout_source[dev_id]:destroy ()
restore_timeout_source[dev_id] = nil restore_timeout_source[dev_id] = nil
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
end end
if switch_timeout_source[dev_id] ~= nil then if switch_timeout_source[dev_id] ~= nil then
switch_timeout_source[dev_id]:destroy () switch_timeout_source[dev_id]:destroy ()
switch_timeout_source[dev_id] = nil switch_timeout_source[dev_id] = nil
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
end end
-- create new switch callback -- create new switch callback
switch_timeout_source[dev_id] = Core.timeout_add (profile_switch_timeout_msec, function () log:info ("Triggering profile switch on device " .. tostring (dev_id))
switch_timeout_source[dev_id] = Core.timeout_add (PROFILE_SWITCH_TIMEOUT_MSEC, function ()
switch_timeout_source[dev_id] = nil switch_timeout_source[dev_id] = nil
switchDeviceToHeadsetProfile (dev_id, device_om)
local e = source:call ("create-event", "autoswitch-bluez-headset-profile", nil, nil)
e:set_data ("device-id", dev_id)
EventDispatcher.push_event (e)
end) end)
end end
function triggerRestoreProfile (dev_id, device_om) function triggerRestoreProfile (source, dev_id)
-- we never restore the device profiles if there are active streams
for _, v in pairs (active_streams) do
if v == dev_id then
return
end
end
-- Always clear any pending restore/switch callbacks when triggering a new restore -- Always clear any pending restore/switch callbacks when triggering a new restore
if switch_timeout_source[dev_id] ~= nil then if switch_timeout_source[dev_id] ~= nil then
switch_timeout_source[dev_id]:destroy () switch_timeout_source[dev_id]:destroy ()
switch_timeout_source[dev_id] = nil switch_timeout_source[dev_id] = nil
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
end end
if restore_timeout_source[dev_id] ~= nil then if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy () restore_timeout_source[dev_id]:destroy ()
restore_timeout_source[dev_id] = nil restore_timeout_source[dev_id] = nil
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
end end
-- create new restore callback -- create new restore callback
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function () log:info ("Triggering profile restore on device " .. tostring (dev_id))
restore_timeout_source[dev_id] = Core.timeout_add (PROFILE_RESTORE_TIMEOUT_MSEC, function ()
restore_timeout_source[dev_id] = nil restore_timeout_source[dev_id] = nil
restoreProfile (dev_id, device_om)
local e = source:call ("create-event", "autoswitch-bluez-a2dp-profile", nil, nil)
e:set_data ("device-id", dev_id)
EventDispatcher.push_event (e)
end) end)
end end
-- We consider a Stream of interest if it is linked to a bluetooth loopback function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups)
-- source filter local stream_id = stream["bound-id"]
function checkStreamStatus (stream, node_om, visited_link_groups)
-- check if the stream is linked to a bluetooth loopback source
local stream_id = tonumber(stream["bound-id"])
local peer_id = lutils.getNodePeerId (stream_id)
if peer_id ~= nil then
local bt_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
}
if bt_node ~= nil then
local dev_id = bt_node.properties["device.id"]
if dev_id ~= nil then
-- If a stream we previously saw stops running, we consider it
-- inactive, because some applications (Teams) just cork input
-- streams, but don't close them.
if previous_streams [stream.id] == dev_id and
stream.state ~= "running" then
return nil
end
return dev_id -- Make sure the node is linked
local link = link_om:lookup {
Constraint { "link.input.node", "=", stream_id, type = "pw-global"}
}
if link == nil then
return nil
end
local peer_id = link.properties["link.output.node"]
-- If the peer node is the BT loopback source node, return its Id.
-- Otherwise recursively advance in the graph if it is linked to a filter.
local bt_node = node_om:lookup {
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
}
if bt_node ~= nil then
return bt_node
else
local filter_main_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "node.link-group", "+", type = "pw" }
}
if filter_main_node ~= nil then
local filter_link_group = filter_main_node.properties ["node.link-group"]
if visited_link_groups == nil then
visited_link_groups = {}
end end
else if visited_link_groups [filter_link_group] then
-- Check if it is linked to a filter main node, and recursively advance if so return nil
local filter_main_node = node_om:lookup { else
Constraint { "bound-id", "=", peer_id, type = "gobject" }, visited_link_groups [filter_link_group] = true
Constraint { "node.link-group", "+", type = "pw" } end
} for filter_stream_node in node_om:iterate {
if filter_main_node ~= nil then Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
-- Now check all stream nodes for this filter Constraint { "stream.monitor", "!", "true", type = "pw" },
local filter_link_group = filter_main_node.properties ["node.link-group"] Constraint { "bluez5.loopback", "!", "true", type = "pw" },
if visited_link_groups == nil then Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
visited_link_groups = {} } do
end local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_node, node_om, link_om, visited_link_groups)
if visited_link_groups [filter_link_group] then if bt_node ~= nil then
return nil return bt_node
else
visited_link_groups [filter_link_group] = true
end
for filter_stream_node in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
} do
local dev_id = checkStreamStatus (filter_stream_node, node_om, visited_link_groups)
if dev_id ~= nil then
return dev_id
end
end end
end end
end end
@ -377,60 +346,118 @@ function checkStreamStatus (stream, node_om, visited_link_groups)
return nil return nil
end end
function handleStream (stream, node_om, device_om) function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then local bt_node_id = bt_node["bound-id"]
return
end
local dev_id = checkStreamStatus (stream, node_om)
if dev_id ~= nil then
active_streams [stream.id] = dev_id
previous_streams [stream.id] = dev_id
triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
else
dev_id = active_streams [stream.id]
active_streams [stream.id] = nil
if dev_id ~= nil then
triggerRestoreProfile (dev_id, device_om)
end
end
end
function handleAllStreams (node_om, device_om)
for stream in node_om:iterate { for stream in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" }, Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "node.link-group", "-", type = "pw" }, Constraint { "node.link-group", "-", type = "pw" },
Constraint { "stream.monitor", "!", "true", type = "pw" }, Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" } Constraint { "bluez5.loopback", "!", "true", type = "pw" }
} do } do
handleStream (stream, node_om, device_om) local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om)
if linked_bt_node ~= nil then
local linked_bt_node_id = linked_bt_node ["bound-id"]
if tonumber (linked_bt_node_id) == tonumber (bt_node_id) then
return true
end
end
end end
return false
end end
SimpleEventHook { local switch_profile_hook = AsyncEventHook {
name = "node-removed@autoswitch-bluetooth-profile", name = "switch-profile@autoswitch-bluetooth-profile",
interests = { interests = {
EventInterest { EventInterest {
Constraint { "event.type", "=", "node-removed" }, Constraint { "event.type", "=", "autoswitch-bluez-headset-profile" },
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" }, },
Constraint { "bluez5.loopback", "!", "true", type = "pw" }, },
steps = {
start = {
next = "none",
execute = function (event, transition)
local source = event:get_source ()
local device_om = source:call ("get-object-manager", "device")
local device_id = event:get_data ("device-id")
-- Switch profile
switchDeviceToHeadsetProfile (device_id, device_om)
-- Wait until the profile is applied
Core.sync (function ()
transition:advance ()
end)
end
},
}
}
local restore_profile_hook = AsyncEventHook {
name = "restore-profile@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "autoswitch-bluez-a2dp-profile" },
},
},
steps = {
start = {
next = "none",
execute = function (event, transition)
local source = event:get_source ()
local device_om = source:call ("get-object-manager", "device")
local device_id = event:get_data ("device-id")
-- Restore profile
restoreProfile (device_id, device_om)
-- Wait until the profile is applied
Core.sync (function ()
transition:advance ()
end)
end
},
}
}
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "evaluate-bluetooth-profiles" },
}, },
}, },
execute = function (event) execute = function (event)
local stream = event:get_subject ()
local source = event:get_source () local source = event:get_source ()
local device_om = source:call ("get-object-manager", "device") local node_om = source:call ("get-object-manager", "node")
local link_om = source:call ("get-object-manager", "link")
local dev_id = active_streams[stream.id] -- Evaluate all bluetooth loopback source nodes, and switch to headset
active_streams[stream.id] = nil -- profile only if the node is running and linked to a stream that is not a
previous_streams[stream.id] = nil -- monitor, otherwise just restore the profile.
if dev_id ~= nil then --
triggerRestoreProfile (dev_id, device_om) -- If the bluetooth node is linked to a stream that is a monitor, its state
-- will be 'running', so we cannot just rely on the state to know if we
-- have to switch or not, we also need to check if the node is linked to
-- a stream that is not a monitor.
for bt_node in node_om:iterate {
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
} do
local bt_node_state = bt_node["state"]
local bt_dev_id = bt_node.properties ["device.id"]
if bt_node_state == "running" and
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
triggerSwitchDeviceToHeadsetProfile (source, bt_dev_id)
else
triggerRestoreProfile (source, bt_dev_id)
end
end end
end end
}:register () }
SimpleEventHook { local link_added_hook = SimpleEventHook {
name = "link-added@autoswitch-bluetooth-profile", name = "link-added@autoswitch-bluetooth-profile",
interests = { interests = {
EventInterest { EventInterest {
@ -438,46 +465,164 @@ SimpleEventHook {
}, },
}, },
execute = function (event) execute = function (event)
local link = event:get_subject ()
local source = event:get_source () local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node") local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device") local link = event:get_subject ()
local link_props = link.properties local in_stream_id = link.properties["link.input.node"]
for stream in node_om:iterate { -- Only evaluate bluetooth profiles if a capture stream was linked
local stream = node_om:lookup {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" }, Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "node.link-group", "-", type = "pw" }, Constraint { "node.link-group", "-", type = "pw" },
Constraint { "stream.monitor", "!", "true", type = "pw" }, Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" } Constraint { "bluez5.loopback", "!", "true", type = "pw" },
} do Constraint { "bound-id", "=", in_stream_id, type = "gobject" },
local in_id = tonumber(link_props["link.input.node"]) }
local stream_id = tonumber(stream["bound-id"]) if stream ~= nil then
if in_id == stream_id then capture_stream_links [link.id] = true
handleStream (stream, node_om, device_om) source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
end end
end end
}:register () }
SimpleEventHook { local link_removed_hook = SimpleEventHook {
name = "bluez-device-added@autoswitch-bluetooth-profile", name = "link-removed@autoswitch-bluetooth-profile",
interests = { interests = {
EventInterest { EventInterest {
Constraint { "event.type", "=", "device-added" }, Constraint { "event.type", "=", "link-removed" },
},
},
execute = function (event)
local source = event:get_source ()
local link = event:get_subject ()
-- Only evaluate bluetooth profiles if a capture stream was unlinked
if capture_stream_links [link.id] then
capture_stream_links [link.id] = nil
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
end
}
local state_changed_hook = SimpleEventHook {
name = "bluez-loopback-state-changed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-state-changed" },
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
},
},
execute = function (event)
local source = event:get_source ()
local node = event:get_subject ()
local old_state = event:get_properties ()["event.subject.old-state"]
local new_state = event:get_properties ()["event.subject.new-state"]
log:info (node, "state changed from '" .. old_state .. "' to '" .. new_state .. "'")
-- Dont evaluate if the state changed from idle to suspended
if old_state == "idle" and new_state == "suspended" then
return
end
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
local node_added_hook = SimpleEventHook {
name = "bluez-loopback-added@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
},
},
execute = function (event)
local source = event:get_source ()
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
local device_profile_changed_hook = SimpleEventHook {
name = "bluez-profile-changed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
Constraint { "event.subject.param-id", "=", "Profile" },
Constraint { "device.api", "=", "bluez5" }, Constraint { "device.api", "=", "bluez5" },
}, },
}, },
execute = function (event) execute = function (event)
local device = event:get_subject () local device = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device")
-- Devices are unswitched initially -- Always save the current profile when it changes
saveLastProfile (device, nil) local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil then
-- Handle all streams when BT device is added if isHeadsetProfile (device, cur_profile) then
handleAllStreams (node_om, device_om) log:info (device, "Saving headset profile " .. cur_profile.name)
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
else
log:info (device, "Saving non-headset profile " .. cur_profile.name)
saveNonHeadsetProfile (device, cur_profile.name)
end
end
end end
}:register () }
function evaluatePersistentStorage ()
if Settings.get_boolean ("bluetooth.use-persistent-storage") and
not persistent_storage_hooks_registered then
state = State ("bluetooth-autoswitch")
headset_profiles = state:load ()
persistent_storage_hooks_registered = true
elseif persistent_storage_hooks_registered then
state = nil
headset_profiles = {}
persistent_storage_hooks_registered = false
end
end
function evaluateAutoswitch ()
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") and
not autoswitch_hooks_registered then
capture_stream_links = {}
restore_timeout_source = {}
switch_timeout_source = {}
switch_profile_hook:register ()
restore_profile_hook:register ()
evaluate_bluetooth_profiles_hook:register ()
link_added_hook:register ()
link_removed_hook:register ()
state_changed_hook:register ()
node_added_hook:register ()
device_profile_changed_hook:register ()
autoswitch_hooks_registered = true
elseif autoswitch_hooks_registered then
capture_stream_links = nil
restore_timeout_source = nil
switch_timeout_source = nil
switch_profile_hook:remove ()
restore_profile_hook:remove ()
evaluate_bluetooth_profiles_hook:remove ()
link_added_hook:remove ()
link_removed_hook:remove ()
state_changed_hook:remove ()
node_added_hook:remove ()
device_profile_changed_hook:remove ()
autoswitch_hooks_registered = false
end
end
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
evaluatePersistentStorage ()
end)
evaluatePersistentStorage ()
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
evaluateAutoswitch ()
end)
evaluateAutoswitch ()

View file

@ -27,7 +27,7 @@ SimpleEventHook {
local device = event:get_subject () local device = event:get_subject ()
local event_properties = event:get_properties () local event_properties = event:get_properties ()
local active_ids = event_properties ["profile.active-device-ids"] local active_ids = event_properties ["profile.active-device-ids"]
local selected_routes = event:get_data ("selected-routes") or {} local selected_routes = event:get_data ("selected-routes") or Properties()
local dev_info = devinfo:get_device_info (device) local dev_info = devinfo:get_device_info (device)
assert (dev_info) assert (dev_info)

View file

@ -13,6 +13,37 @@ log = Log.open_topic ("s-device")
config = {} config = {}
config.rules = Conf.get_section_as_json ("device.profile.priority.rules", Json.Array {}) config.rules = Conf.get_section_as_json ("device.profile.priority.rules", Json.Array {})
function getRulesProfilePriorities (device)
local props = JsonUtils.match_rules_update_properties (config.rules,
device.properties)
local p_array = props["priorities"]
if not p_array then
return nil
end
local p_json = Json.Raw (p_array)
return p_json:parse ()
end
function getPreferredBluetoothProfilePriorities (device)
if device.properties["device.api"] ~= "bluez5" then
return nil
end
local preference = Settings.get_string ("bluetooth.profile-preference")
if preference == "latency" then
log:info (device, "using best latency profile")
return { "a2dp-auto-prefer-latency" }
elseif preference == "quality" then
log:info (device, "using best quality profile")
return { "a2dp-auto-prefer-quality" }
end
log:warning (device, "invalid preference value '" .. preference ..
"'. Defaulting to best quality profile")
return { "a2dp-auto-prefer-quality" }
end
SimpleEventHook { SimpleEventHook {
name = "device/find-preferred-profile", name = "device/find-preferred-profile",
after = "device/find-stored-profile", after = "device/find-stored-profile",
@ -31,19 +62,22 @@ SimpleEventHook {
end end
local device = event:get_subject () local device = event:get_subject ()
local props = JsonUtils.match_rules_update_properties (
config.rules, device.properties)
local p_array = props["priorities"]
-- skip hook if the profile priorities are NOT defined for this device.
if not p_array then
return nil
end
local p_json = Json.Raw(p_array)
local priorities = p_json:parse()
local device_name = device.properties["device.name"] or "" local device_name = device.properties["device.name"] or ""
-- Use device priority rules if any. Otherwise, get the prefered quality or
-- latency priorities if BT device.
local priorities = getRulesProfilePriorities (device)
if priorities == nil then
priorities = getPreferredBluetoothProfilePriorities (device)
end
if priorities == nil then
log:info (device, string.format (
"Preferred profile priorities not available for device '%s'",
device_name))
return
end
-- Find the prefered profile
for _, priority_profile in ipairs(priorities) do for _, priority_profile in ipairs(priorities) do
for p in device:iterate_params("EnumProfile") do for p in device:iterate_params("EnumProfile") do
local device_profile = cutils.parseParam(p, "EnumProfile") local device_profile = cutils.parseParam(p, "EnumProfile")
@ -61,8 +95,8 @@ SimpleEventHook {
selected_profile.name, selected_profile.index, device_name)) selected_profile.name, selected_profile.index, device_name))
event:set_data ("selected-profile", selected_profile) event:set_data ("selected-profile", selected_profile)
else else
log:info (device, "Profiles listed in 'device.profile.priority.rules'" log:info (device, string.format (
.. " do not match the available ones of device: " .. device_name) "Could not find preferred profile for device '%s'", device_name))
end end
end end

View file

@ -26,3 +26,11 @@ SimpleEventHook {
source:call ("push-event", "select-profile", device, nil) source:call ("push-event", "select-profile", device, nil)
end end
}:register() }:register()
Settings.subscribe ("bluetooth.profile-preference", function ()
source = source or Plugin.find ("standard-event-source")
local device_om = source:call ("get-object-manager", "device")
for device in device_om:iterate () do
source:call ("push-event", "select-profile", device, nil)
end
end)

View file

@ -34,9 +34,12 @@ find_stored_profile_hook = SimpleEventHook {
end end
local device = event:get_subject () local device = event:get_subject ()
local dev_name = device.properties["device.name"] local device_props = device.properties
local dev_name = device_props["device.name"]
local dont_restore_off_profile = cutils.parseBool (
device_props["session.dont-restore-off-profile"])
if not dev_name then if not dev_name then
log:critical (device, "invalid device.name") log:warning (device, "invalid device.name")
return return
end end
@ -45,7 +48,8 @@ find_stored_profile_hook = SimpleEventHook {
if profile_name then if profile_name then
for p in device:iterate_params ("EnumProfile") do for p in device:iterate_params ("EnumProfile") do
local profile = cutils.parseParam (p, "EnumProfile") local profile = cutils.parseParam (p, "EnumProfile")
if profile.name == profile_name and profile.available ~= "no" then if profile.name == profile_name and profile.available ~= "no" and
(not dont_restore_off_profile or profile.index ~= 0) then
selected_profile = profile selected_profile = profile
break break
end end
@ -87,7 +91,7 @@ function updateStoredProfile (device, profile)
local index = nil local index = nil
if not dev_name then if not dev_name then
log:critical (device, "invalid device.name") log:warning (device, "invalid device.name")
return return
end end

View file

@ -36,7 +36,7 @@ find_stored_routes_hook = SimpleEventHook {
local event_properties = event:get_properties () local event_properties = event:get_properties ()
local profile_name = event_properties ["profile.name"] local profile_name = event_properties ["profile.name"]
local active_ids = event_properties ["profile.active-device-ids"] local active_ids = event_properties ["profile.active-device-ids"]
local selected_routes = event:get_data ("selected-routes") or {} local selected_routes = event:get_data ("selected-routes") or Properties()
local dev_info = devinfo:get_device_info (device) local dev_info = devinfo:get_device_info (device)
assert (dev_info) assert (dev_info)
@ -108,13 +108,13 @@ apply_route_props_hook = SimpleEventHook {
}, },
execute = function (event) execute = function (event)
local device = event:get_subject () local device = event:get_subject ()
local selected_routes = event:get_data ("selected-routes") or {} local selected_routes = event:get_data ("selected-routes") or Properties()
local new_selected_routes = {} local new_selected_routes = {}
local dev_info = devinfo:get_device_info (device) local dev_info = devinfo:get_device_info (device)
assert (dev_info) assert (dev_info)
if next (selected_routes) == nil then if selected_routes:get_count () == 0 then
log:info (device, "No routes selected to set on " .. dev_info.name) log:info (device, "No routes selected to set on " .. dev_info.name)
return return
end end
@ -159,129 +159,125 @@ store_or_restore_routes_hook = AsyncEventHook {
}, },
steps = { steps = {
start = { start = {
next = "evaluate", next = "none",
execute = function (event, transition) execute = function (event, transition)
local source = event:get_source ()
local device = event:get_subject ()
-- Make sure the routes are always updated before evaluating them. -- Make sure the routes are always updated before evaluating them.
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762 -- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762
local device = event:get_subject () device:enum_params ("EnumRoute", function (enum_route_it, e)
device:enum_params ("EnumRoute", function (_, e) local selected_routes = {}
local push_select_routes = false
-- check for error
if e then if e then
transition:return_error ("failed to enum routes: " transition:return_error ("failed to enum routes: "
.. tostring (e)); .. tostring (e));
else return
end
-- Make sure the device is still valid
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
transition:advance () transition:advance ()
end return
end)
end
},
evaluate = {
next = "none",
execute = function (event, transition)
local device = event:get_subject ()
local source = event:get_source ()
local selected_routes = {}
local push_select_routes = false
-- Make sure the device is still valid
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
transition:advance ()
return
end
local dev_info = devinfo:get_device_info (device)
if not dev_info then
transition:advance ()
return
end
local new_route_infos = {}
-- look at all the routes and update/reset cached information
for p in device:iterate_params ("EnumRoute") do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end end
-- find cached route information local dev_info = devinfo:get_device_info (device)
local route_info = devinfo.find_route_info (dev_info, route, true) if not dev_info then
if not route_info then transition:advance ()
goto skip_enum_route return
end end
-- update properties local new_route_infos = {}
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store -- look at all the routes and update/reset cached information
new_route_infos [route.index] = route_info for p in enum_route_it:iterate() do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
::skip_enum_route:: -- find cached route information
end local route_info = devinfo.find_route_info (dev_info, route, true)
if not route_info then
goto skip_enum_route
end
-- update route_infos with new prev_active, active and save changes -- update properties
dev_info.route_infos = new_route_infos route_info.prev_active = route_info.active
new_route_infos = nil route_info.active = false
route_info.save = false
-- check for changes in the active routes -- store
for p in device:iterate_params ("Route") do new_route_infos [route.index] = route_info
local route = cutils.parseParam (p, "Route")
if not route then ::skip_enum_route::
goto skip_route
end end
-- get cached route info and at the same time -- update route_infos with new prev_active, active and save changes
-- ensure that the route is also in EnumRoute dev_info.route_infos = new_route_infos
local route_info = devinfo.find_route_info (dev_info, route, false) new_route_infos = nil
if not route_info then
goto skip_route
end
-- update route_info state -- check for changes in the active routes
route_info.active = true for p in device:iterate_params ("Route") do
route_info.save = route.save local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
if not route_info.prev_active then -- get cached route info and at the same time
-- a new route is now active, restore the volume and -- ensure that the route is also in EnumRoute
-- make sure we save this as a preferred route local route_info = devinfo.find_route_info (dev_info, route, false)
log:info (device, if not route_info then
string.format ("new active route(%s) found of device(%s)", goto skip_route
route.name, dev_info.name)) end
route_info.prev_active = true
-- update route_info state
route_info.active = true route_info.active = true
route_info.save = route.save
selected_routes [tostring (route.device)] = if not route_info.prev_active then
Json.Object { index = route_info.index }:to_string () -- a new route is now active, restore the volume and
push_select_routes = true -- make sure we save this as a preferred route
log:info (device,
string.format ("new active route(%s) found of device(%s)",
route.name, dev_info.name))
route_info.prev_active = true
route_info.active = true
elseif route.available ~= "no" and route.save and route.props then selected_routes [tostring (route.device)] =
-- just save route properties Json.Object { index = route_info.index }:to_string ()
log:info (device, push_select_routes = true
string.format ("storing route(%s) props of device(%s)",
route.name, dev_info.name))
saveRouteProps (dev_info, route) elseif route.available ~= "no" and route.save and route.props then
-- just save route properties
log:info (device,
string.format ("storing route(%s) props of device(%s)",
route.name, dev_info.name))
saveRouteProps (dev_info, route)
end
::skip_route::
end end
::skip_route:: -- save selected routes for the active profile
end for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
saveProfileRoutes (dev_info, profile.name)
end
-- save selected routes for the active profile -- push a select-routes event to re-apply the routes with new properties
for p in device:iterate_params ("Profile") do if push_select_routes then
local profile = cutils.parseParam (p, "Profile") local e = source:call ("create-event", "select-routes", device, nil)
saveProfileRoutes (dev_info, profile.name) e:set_data ("selected-routes", selected_routes)
end EventDispatcher.push_event (e)
end
-- push a select-routes event to re-apply the routes with new properties transition:advance ()
if push_select_routes then end)
local e = source:call ("create-event", "select-routes", device, nil)
e:set_data ("selected-routes", selected_routes)
EventDispatcher.push_event (e)
end
transition:advance ()
end end
} }
} }

View file

@ -91,4 +91,24 @@ function cutils.get_application_name ()
return Core.get_properties()["application.name"] or "WirePlumber" return Core.get_properties()["application.name"] or "WirePlumber"
end end
function cutils.get_client_access (client_properties)
local access = client_properties["pipewire.access"]
local client_access = client_properties["pipewire.client.access"]
local is_flatpak = client_properties:get_boolean ("pipewire.sec.flatpak")
if is_flatpak then
client_access = "flatpak"
end
if client_access == nil then
return access
elseif access == "unrestricted" or access == "default" then
if client_access ~= "unrestricted" then
return client_access
end
end
return access
end
return cutils return cutils

View file

@ -9,7 +9,7 @@ Hooks
The hooks in this section are organized in 3 sub-categories. The first category The hooks in this section are organized in 3 sub-categories. The first category
includes hooks that are triggered by changes in the graph. Some of them are tasked includes hooks that are triggered by changes in the graph. Some of them are tasked
to schedule a "rescan-for-linking" event, which is the lowest priority event and to schedule a "rescan-for-linking" event, which is the lowest priority linking event and
its purpose is to scan through all the linkable session items and link them its purpose is to scan through all the linkable session items and link them
to a particular target. The "rescan-for-linking" event is always scheduled to run to a particular target. The "rescan-for-linking" event is always scheduled to run
once for all the graph changes in a cycle. This is achieved by flagging the event once for all the graph changes in a cycle. This is achieved by flagging the event

View file

@ -13,7 +13,8 @@ SimpleEventHook {
name = "linking/find-default-target", name = "linking/find-default-target",
after = { "linking/find-defined-target", after = { "linking/find-defined-target",
"linking/find-filter-target", "linking/find-filter-target",
"linking/find-media-role-target" }, "linking/find-media-role-target",
"linking/find-media-role-sink-target" },
before = "linking/prepare-link", before = "linking/prepare-link",
interests = { interests = {
EventInterest { EventInterest {

View file

@ -0,0 +1,81 @@
-- WirePlumber
--
-- Copyright © 2025 Phosh.mobi e.V.
--
-- SPDX-License-Identifier: MIT
--
-- Pick up a preferred target node for the output stream of role-based loopbacks
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/find-media-role-sink-target",
after = { "linking/find-defined-target",
"linking/find-media-role-target" },
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local _, om, si, si_props, _, target =
lutils:unwrap_select_target_event (event)
local node_name = si_props["node.name"]
local target_direction = cutils.getTargetDirection (si_props)
local media_class = si_props["media.class"]
local link_group = si_props["node.link-group"]
local is_virtual = si_props["node.virtual"]
log:info (si, string.format ("Lookup for '%s' (%s) / '%s' / '%s'",
node_name, tostring (si_props ["node.id"]), media_class, link_group))
--- bypass the hook if the target is already set or there's no link group
if target or media_class ~= "Stream/Output/Audio" or not is_virtual or link_group == nil then
return
end
--- We link the output node but the relevant properties are on the input node
--- of the link group
local input_node = om:lookup {
type = "SiLinkable",
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.link-group", "=", link_group },
}
if input_node == nil then
log:warning (si, string.format("No input node for %s found", link_group))
return
end
local target_name = input_node.properties["policy.role-based.preferred-target"]
--- no preferred target
if target_name == nil then
return
end
local si_target = om:lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "node.name", "=", target_name },
}
if si_target == nil then
si_target = om:lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "node.nick", "=", target_name },
}
end
if si_target then
log:info (si,
string.format ("... role based sink target picked: %s (%s)",
tostring (si_target.properties ["node.name"]),
tostring (si_target.properties ["node.id"])))
event:set_data ("target", si_target)
end
end
}:register ()

View file

@ -0,0 +1,25 @@
-- WirePlumber
--
-- Copyright © 2026 Axis Communications AB.
--
-- SPDX-License-Identifier: MIT
--
-- Trigger a full rescan when linkable session items are added or removed.
-- This can be disabled by setting hooks.linking.rescan-on-linkable = disabled
-- in wireplumber.profiles.
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/rescan-trigger-on-linkable-added-removed",
interests = {
EventInterest {
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
Constraint { "event.session-item.interface", "=", "linkable" },
},
},
execute = function (event)
local source = event:get_source ()
source:call ("schedule-rescan", "linking")
end
}:register ()

View file

@ -26,7 +26,7 @@ function checkFilter (si, om, handle_nonstreams)
-- always return true if this is not a filter -- always return true if this is not a filter
local node = si:get_associated_proxy ("node") local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"] local link_group = node:get_property ("node.link-group")
if link_group == nil then if link_group == nil then
return true return true
end end
@ -43,36 +43,34 @@ function checkFilter (si, om, handle_nonstreams)
end end
function checkLinkable (si, om, handle_nonstreams) function checkLinkable (si, om, handle_nonstreams)
local si_props = si.properties
-- For the rest of them, only handle stream session items -- For the rest of them, only handle stream session items
if not si_props or (si_props ["item.node.type"] ~= "stream" if si:get_property ("item.node.type") ~= "stream" and
and not handle_nonstreams) then not handle_nonstreams then
return false, si_props return false
end end
-- check filters -- check filters
if not checkFilter (si, om, handle_nonstreams) then if not checkFilter (si, om, handle_nonstreams) then
return false, si_props return false
end end
return true, si_props return true
end end
function unhandleLinkable (si, om) function unhandleLinkable (si, om)
local si_id = si.id if not checkLinkable (si, om, true) then
local valid, si_props = checkLinkable (si, om, true)
if not valid then
return return
end end
local si_id = si.id
log:info (si, string.format ("unhandling item %d", si_id)) log:info (si, string.format ("unhandling item %d", si_id))
-- iterate over all the links in the graph and -- iterate over all the links in the graph and
-- remove any links associated with this item -- remove any links associated with this item
for silink in om:iterate { type = "SiLink" } do for silink in om:iterate { type = "SiLink" } do
local out_id = tonumber (silink.properties ["out.item.id"]) local silink_props = silink.properties
local in_id = tonumber (silink.properties ["in.item.id"]) local out_id = silink_props:get_int ("out.item.id")
local in_id = silink_props:get_int ("in.item.id")
if out_id == si_id or in_id == si_id then if out_id == si_id or in_id == si_id then
local in_flags = lutils:get_flags (in_id) local in_flags = lutils:get_flags (in_id)
@ -84,7 +82,7 @@ function unhandleLinkable (si, om)
out_flags.peer_id = nil out_flags.peer_id = nil
end end
if cutils.parseBool (silink.properties["is.role.policy.link"]) then if silink_props:get_boolean ("is.role.policy.link") then
lutils.clearPriorityMediaRoleLink(silink) lutils.clearPriorityMediaRoleLink(silink)
end end
@ -113,17 +111,67 @@ SimpleEventHook {
end end
}:register () }:register ()
-- Handle newly added linkable immediately without waiting for full rescan
-- Only for simple cases where we know it won't affect other parts of the graph
SimpleEventHook {
name = "linking/linkable-added-immediate",
before = "linking/rescan-trigger",
interests = {
EventInterest {
Constraint { "event.type", "=", "session-item-added" },
Constraint { "event.session-item.interface", "=", "linkable" },
},
},
execute = function (event)
local si = event:get_subject ()
local source = event:get_source ()
local om = source:call ("get-object-manager", "session-item")
if not checkLinkable (si, om, false) then
return
end
-- Don't handle immediately if this is a smart filter that could affect other nodes
local node = si:get_associated_proxy ("node")
local link_group = node:get_property ("node.link-group")
if link_group then
local direction = cutils.getTargetDirection (si.properties)
if futils.is_filter_smart (direction, link_group) then
-- Smart filters need full rescan to handle cascading effects
return
end
end
-- Only handle if autoconnect is enabled
local autoconnect = si:get_property ("node.autoconnect")
if autoconnect ~= "true" then
return
end
-- Check if this is a simple stream (most common case)
-- Don't handle device nodes or special nodes that might become default targets
if si:get_property ("item.node.type") ~= "stream" then
return
end
-- Push select-target event immediately for simple stream case
source:call ("push-event", "select-target", si, nil)
end
}:register ()
function handleLinkables (source) function handleLinkables (source)
local om = source:call ("get-object-manager", "session-item") local om = source:call ("get-object-manager", "session-item")
for si in om:iterate { type = "SiLinkable" } do for si in om:iterate { type = "SiLinkable" } do
local valid, si_props = checkLinkable (si, om) if not checkLinkable (si, om) then
if not valid then
goto skip_linkable goto skip_linkable
end end
-- Get properties
local si_props = si.properties
-- check if we need to link this node at all -- check if we need to link this node at all
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"]) local autoconnect = si_props:get_boolean ("node.autoconnect")
if not autoconnect then if not autoconnect then
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected") log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
goto skip_linkable goto skip_linkable
@ -155,7 +203,7 @@ SimpleEventHook {
Constraint { "node.link-group", "+" }, Constraint { "node.link-group", "+" },
} do } do
local node = si:get_associated_proxy ("node") local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"] local link_group = node:get_property ("node.link-group")
local direction = cutils.getTargetDirection (si.properties) local direction = cutils.getTargetDirection (si.properties)
if futils.is_filter_smart (direction, link_group) and if futils.is_filter_smart (direction, link_group) and
futils.is_filter_disabled (direction, link_group) then futils.is_filter_disabled (direction, link_group) then
@ -170,11 +218,6 @@ SimpleEventHook {
SimpleEventHook { SimpleEventHook {
name = "linking/rescan-trigger", name = "linking/rescan-trigger",
interests = { interests = {
-- on linkable added or removed, where linkable is adapter or plain node
EventInterest {
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
Constraint { "event.session-item.interface", "=", "linkable" },
},
-- on device Routes changed -- on device Routes changed
EventInterest { EventInterest {
Constraint { "event.type", "=", "device-params-changed" }, Constraint { "event.type", "=", "device-params-changed" },
@ -235,7 +278,6 @@ SimpleEventHook {
}, },
execute = function (event) execute = function (event)
local si = event:get_subject () local si = event:get_subject ()
local si_props = si.properties
local source = event:get_source () local source = event:get_source ()
-- clear timeout source, if any -- clear timeout source, if any

View file

@ -297,9 +297,9 @@ function createNode(parent, id, obj_type, factory, properties)
properties["node.description"] = desc:gsub("(:)", " ") properties["node.description"] = desc:gsub("(:)", " ")
end end
-- add api.alsa.card.* properties for rule matching purposes -- add api.alsa.card.* and alsa.* properties for rule matching purposes
for k, v in pairs(dev_props) do for k, v in pairs(dev_props) do
if k:find("^api%.alsa%.card%..*") then if k:find("^api%.alsa%.card%..*") or k:find("^alsa%..*") then
properties[k] = v properties[k] = v
end end
end end
@ -494,6 +494,11 @@ function prepareDevice(parent, id, obj_type, factory, properties)
factory = "api.alsa.acp.device" factory = "api.alsa.acp.device"
end end
-- use HDMI channel detection if enabled in settings
if Settings.get_boolean ("monitor.alsa.autodetect-hdmi-channels") then
properties["api.acp.use-eld-channels"] = true
end
-- use device reservation, if available -- use device reservation, if available
if rd_plugin and properties["api.alsa.card"] then if rd_plugin and properties["api.alsa.card"] then
local rd_name = "Audio" .. properties["api.alsa.card"] local rd_name = "Audio" .. properties["api.alsa.card"]

View file

@ -78,7 +78,7 @@ end
function createMonitor() function createMonitor()
local monitor_props = {} local monitor_props = {}
for k, v in pairs(config.properties or {}) do for k, v in pairs(config.properties or Properties()) do
monitor_props[k] = v monitor_props[k] = v
end end

View file

@ -8,6 +8,7 @@
COMBINE_OFFSET = 64 COMBINE_OFFSET = 64
LOOPBACK_SOURCE_ID = 128 LOOPBACK_SOURCE_ID = 128
DEVICE_SOURCE_ID = 0 DEVICE_SOURCE_ID = 0
DEVICE_SINK_ID = 1
cutils = require ("common-utils") cutils = require ("common-utils")
log = Log.open_topic ("s-monitors") log = Log.open_topic ("s-monitors")
@ -20,13 +21,10 @@ config.rules = Conf.get_section_as_json ("monitor.bluez.rules", Json.Array {})
-- This is not a setting, it must always be enabled -- This is not a setting, it must always be enabled
config.properties["api.bluez5.connection-info"] = true config.properties["api.bluez5.connection-info"] = true
-- Properties used for previously creating a SCO source node. key: SPA device id
sco_source_node_properties = {}
devices_om = ObjectManager { devices_om = ObjectManager {
Interest { Interest {
type = "device", type = "device",
Constraint { "device.api", "=", "bluez5" },
} }
} }
@ -213,13 +211,15 @@ function createSetNode(parent, id, type, factory, properties)
) )
end end
properties["node.virtual"] = false local combine_props = properties:parse ()
properties["device.api"] = "bluez5" combine_props["node.virtual"] = false
properties["api.bluez5.set.members"] = nil combine_props["device.api"] = "bluez5"
properties["api.bluez5.set.channels"] = nil combine_props["api.bluez5.set.members"] = nil
properties["api.bluez5.set.leader"] = true combine_props["api.bluez5.set.channels"] = nil
properties["audio.position"] = Json.Array (channels) combine_props["api.bluez5.set.leader"] = true
args["combine.props"] = Json.Object (properties) combine_props["audio.position"] = Json.Array (channels)
args["combine.props"] = Json.Object (combine_props)
args["stream.props"] = Json.Object {} args["stream.props"] = Json.Object {}
args["stream.rules"] = Json.Array (rules) args["stream.rules"] = Json.Array (rules)
@ -230,6 +230,12 @@ function createSetNode(parent, id, type, factory, properties)
return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties) return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties)
end end
function getNodeName (prefix, bt_address, dev_name, node_id)
local name = prefix .. "." .. (bt_address or dev_name) .. "." .. tostring(node_id)
-- sanitize name
return name:gsub("([^%w_%-%.])", "_")
end
function createNode(parent, id, type, factory, properties) function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties local dev_props = parent.properties
local parent_id = parent["bound-id"] local parent_id = parent["bound-id"]
@ -258,25 +264,11 @@ function createNode(parent, id, type, factory, properties)
-- sanitize description, replace ':' with ' ' -- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ") properties["node.description"] = desc:gsub("(:)", " ")
-- set the node name
local name_prefix = ((factory:find("sink") and "bluez_output") or local name_prefix = ((factory:find("sink") and "bluez_output") or
(factory:find("source") and "bluez_input" or factory)) (factory:find("source") and "bluez_input" or factory))
properties["node.name"] = getNodeName (name_prefix,
-- hide the source node because we use the loopback source instead properties["api.bluez5.address"], dev_props["device.name"], id)
if parent:get_managed_object (LOOPBACK_SOURCE_ID) ~= nil and
(factory == "api.bluez5.sco.source" or
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"]))) then
properties["bluez5.loopback-target"] = true
properties["api.bluez5.internal"] = true
-- add 'internal' to name prefix to not be confused with loopback node
name_prefix = name_prefix .. "_internal"
end
-- set the node name
local name = name_prefix .. "." ..
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
tostring(id)
-- sanitize name
properties["node.name"] = name:gsub("([^%w_%-%.])", "_")
-- set priority -- set priority
if not properties["priority.driver"] then if not properties["priority.driver"] then
@ -304,10 +296,16 @@ function createNode(parent, id, type, factory, properties)
parent:set_managed_pending(id) parent:set_managed_pending(id)
else else
log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id)) log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id))
properties["bluez5.loopback"] = false
if factory == "api.bluez5.sco.source" then -- Set sink/source specific properties
sco_source_node_properties[parent_spa_id] = properties if factory == "api.bluez5.sco.source" or
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"])) then
properties["bluez5.loopback"] = false
if properties["api.bluez5.profile"] ~= "headset-audio-gateway" then
properties["api.bluez5.internal"] = true
end
end end
local node = LocalNode("adapter", properties) local node = LocalNode("adapter", properties)
node:activate(Feature.Proxy.BOUND) node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node) parent:store_managed_object(id, node)
@ -317,15 +315,9 @@ end
function removeNode(parent, id) function removeNode(parent, id)
local dev_props = parent.properties local dev_props = parent.properties
local parent_spa_id = tonumber(dev_props["spa.object.id"]) local parent_spa_id = tonumber(dev_props["spa.object.id"])
local src_properties = sco_source_node_properties[parent_spa_id]
log:debug("Remove node: " .. tostring (id)) log:debug("Remove node: " .. tostring (id))
if src_properties ~= nil and id == tonumber(src_properties["spa.object.id"]) then
log:debug("Clear old SCO properties")
sco_source_node_properties[parent_spa_id] = nil
end
-- Clear also the device set module, if any -- Clear also the device set module, if any
parent:store_managed_object(id + COMBINE_OFFSET, nil) parent:store_managed_object(id + COMBINE_OFFSET, nil)
end end
@ -399,7 +391,7 @@ function createDevice(parent, id, type, factory, properties)
end end
function removeDevice(parent, id) function removeDevice(parent, id)
sco_source_node_properties[id] = nil log:debug("Remove device: " .. tostring (id))
end end
function createMonitor() function createMonitor()
@ -417,7 +409,18 @@ function createMonitor()
return monitor return monitor
end end
function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id) function CreateDeviceLoopbackSource (dev_props, dev_id)
local dev_name = dev_props["api.bluez5.address"] or dev_props["device.name"]
local dec_desc = dev_props["device.description"] or dev_props["device.name"]
or dev_props["device.nick"] or dev_props["device.alias"] or "bluetooth-device"
local target_object = getNodeName ("bluez_input",
dev_props["api.bluez5.address"], dev_props["device.name"], DEVICE_SOURCE_ID)
-- sanitize description, replace ':' with ' '
dec_desc = dec_desc:gsub("(:)", " ")
log:info("create SCO source loopback node: " .. dev_name)
local args = Json.Object { local args = Json.Object {
["capture.props"] = Json.Object { ["capture.props"] = Json.Object {
["node.name"] = string.format ("bluez_capture_internal.%s", dev_name), ["node.name"] = string.format ("bluez_capture_internal.%s", dev_name),
@ -432,6 +435,7 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
["node.dont-fallback"] = true, ["node.dont-fallback"] = true,
["node.linger"] = true, ["node.linger"] = true,
["state.restore-props"] = false, ["state.restore-props"] = false,
["target.object"] = target_object,
}, },
["playback.props"] = Json.Object { ["playback.props"] = Json.Object {
["node.name"] = string.format ("bluez_input.%s", dev_name), ["node.name"] = string.format ("bluez_input.%s", dev_name),
@ -442,15 +446,8 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
["device.id"] = dev_id, ["device.id"] = dev_id,
["card.profile.device"] = DEVICE_SOURCE_ID, ["card.profile.device"] = DEVICE_SOURCE_ID,
["device.routes"] = "1", ["device.routes"] = "1",
["priority.driver"] = 2010,
["priority.session"] = 2010, ["priority.session"] = 2010,
["bluez5.loopback"] = true, ["bluez5.loopback"] = true,
["filter.smart"] = true,
["filter.smart.target"] = Json.Object {
["bluez5.loopback-target"] = true,
["bluez5.loopback"] = false,
["device.id"] = dev_id
}
} }
} }
return LocalModule("libpipewire-module-loopback", args:get_data(), {}) return LocalModule("libpipewire-module-loopback", args:get_data(), {})
@ -461,11 +458,6 @@ function checkProfiles (dev)
local props = dev.properties local props = dev.properties
local device_spa_id = tonumber(props["spa.object.id"]) local device_spa_id = tonumber(props["spa.object.id"])
-- Don't create loopback source device if autoswitch is disabled
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
return
end
-- Get the associated BT SpaDevice -- Get the associated BT SpaDevice
local internal_id = tostring (props["spa.object.id"]) local internal_id = tostring (props["spa.object.id"])
local spa_device = monitor:get_managed_object (internal_id) local spa_device = monitor:get_managed_object (internal_id)
@ -473,51 +465,48 @@ function checkProfiles (dev)
return return
end end
-- Ignore devices that don't support both A2DP sink and HSP/HFP profiles -- Check if the device supports headset profile
local has_a2dpsink_profile = false
local has_headset_profile = false local has_headset_profile = false
for p in dev:iterate_params("EnumProfile") do for p in dev:iterate_params("EnumProfile") do
local profile = cutils.parseParam (p, "EnumProfile") local profile = cutils.parseParam (p, "EnumProfile")
if profile.name:find ("a2dp") and profile.name:find ("sink") then if profile.name:find ("headset") then
has_a2dpsink_profile = true
elseif profile.name:find ("headset") then
has_headset_profile = true has_headset_profile = true
end end
end end
if not has_a2dpsink_profile or not has_headset_profile then
return
end
-- Create the loopback device if never created before if has_headset_profile then
local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID) -- Always create the source loopback device if autoswitch is enabled.
if loopback == nil then -- Otherwise, only create the source loopback device if the current profile
local dev_name = props["api.bluez5.address"] or props["device.name"] -- is headset, and destroy the source loopback deivce if the current profile
local dec_desc = props["device.description"] or props["device.name"] -- is A2DP.
or props["device.nick"] or props["device.alias"] or "bluetooth-device" if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
-- Create source loopback
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
if source_loopback == nil and has_headset_profile then
source_loopback = CreateDeviceLoopbackSource (props, device_id)
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
end
else
-- Check if current profile is headset
local is_current_profile_headset = false
for p in dev:iterate_params("Profile") do
local profile = cutils.parseParam (p, "Profile")
if profile.name:find ("headset") then
is_current_profile_headset = true
end
break
end
log:info("create SCO loopback node: " .. dev_name) if is_current_profile_headset then
-- Create source loopback
-- sanitize description, replace ':' with ' ' local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
dec_desc = dec_desc:gsub("(:)", " ") if source_loopback == nil and has_headset_profile then
loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id) source_loopback = CreateDeviceLoopbackSource (props, device_id)
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback) spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
end
-- recreate any sco source node else
local properties = sco_source_node_properties[device_spa_id] -- Destroy source loopback
if properties ~= nil then spa_device:store_managed_object(LOOPBACK_SOURCE_ID, nil)
local node_id = tonumber(properties["spa.object.id"])
local node = spa_device:get_managed_object (node_id)
if node ~= nil then
log:info("Recreate node: " .. properties["node.name"] .. ": " ..
properties["factory.name"] .. " " .. tostring (node_id))
spa_device:store_managed_object(node_id, nil)
properties["bluez5.loopback-target"] = true
properties["api.bluez5.internal"] = true
node = LocalNode("adapter", properties)
node:activate(Feature.Proxy.BOUND)
spa_device:store_managed_object(node_id, node)
end end
end end
end end
@ -526,16 +515,12 @@ end
function onDeviceParamsChanged (dev, param_name) function onDeviceParamsChanged (dev, param_name)
if param_name == "EnumProfile" then if param_name == "EnumProfile" then
checkProfiles (dev) checkProfiles (dev)
elseif param_name == "Profile" then
checkProfiles (dev)
end end
end end
devices_om:connect("object-added", function(_, dev) devices_om:connect("object-added", function(_, dev)
-- Ignore all devices that are not BT devices
if dev.properties["device.api"] ~= "bluez5" then
return
end
-- check available profiles
dev:connect ("params-changed", onDeviceParamsChanged) dev:connect ("params-changed", onDeviceParamsChanged)
checkProfiles (dev) checkProfiles (dev)
end) end)
@ -566,3 +551,15 @@ end
nodes_om:activate() nodes_om:activate()
devices_om:activate() devices_om:activate()
device_set_nodes_om:activate() device_set_nodes_om:activate()
function evaluateAutoswitch ()
-- Evaluate loopbacks on all BT devices
for dev in devices_om:iterate () do
checkProfiles (dev)
end
end
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
evaluateAutoswitch ()
end)
evaluateAutoswitch ()

View file

@ -34,7 +34,7 @@ SimpleEventHook {
return return
end end
-- create the node -- create the node
local node = Node ("spa-node-factory", properties) local node = LocalNode ("spa-node-factory", properties)
node:activate (Feature.Proxy.BOUND) node:activate (Feature.Proxy.BOUND)
parent:store_managed_object (id, node) parent:store_managed_object (id, node)
end end

View file

@ -157,7 +157,7 @@ SimpleEventHook {
-- Create group loopback module if it does not exist -- Create group loopback module if it does not exist
local m = group_loopback_modules [direction][group] local m = group_loopback_modules [direction][group]
if m == nil then if m == nil then
Log.warning ("Creating " .. direction .. " loopback for audio group " .. group .. Log.info ("Creating " .. direction .. " loopback for audio group " .. group ..
(target_object and (" with target object " .. tostring (target_object)) or "")) (target_object and (" with target object " .. tostring (target_object)) or ""))
m = CreateStreamLoopback (stream_props, group, target_object, direction) m = CreateStreamLoopback (stream_props, group, target_object, direction)
group_loopback_modules [direction][group] = m group_loopback_modules [direction][group] = m

View file

@ -16,6 +16,7 @@ items = {}
function configProperties (node) function configProperties (node)
local properties = node.properties local properties = node.properties
local media_class = properties ["media.class"] or "" local media_class = properties ["media.class"] or ""
local factory_name = properties ["factory.name"] or ""
-- ensure a media.type is set -- ensure a media.type is set
if not properties ["media.type"] then if not properties ["media.type"] then
@ -40,6 +41,7 @@ function configProperties (node)
properties ["item.features.control-port"] = properties ["item.features.control-port"] =
Settings.get_boolean ("node.features.audio.control-port") Settings.get_boolean ("node.features.audio.control-port")
properties ["item.features.mono"] = properties ["item.features.mono"] =
(factory_name == "api.alsa.pcm.sink" or factory_name == "api.bluez5.a2dp.sink") and
Settings.get_boolean ("node.features.audio.mono") Settings.get_boolean ("node.features.audio.mono")
properties ["node.id"] = node ["bound-id"] properties ["node.id"] = node ["bound-id"]

View file

@ -0,0 +1,53 @@
-- WirePlumber
--
-- Copyright © 2025 The WirePlumber project contributors
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
log = Log.open_topic("s-node")
config = {}
config.rules = Conf.get_section_as_json ("node.filter-graph.rules", Json.Array{})
function setNodeFilterGraphParams (node, graph_params)
local pod = Pod.Object {
"Spa:Pod:Object:Param:Props", "Props",
params = Pod.Struct (graph_params)
}
node:set_params("Props", pod)
end
SimpleEventHook {
name = "node/create-filter-graph",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "library.name", "=", "audioconvert/libspa-audioconvert", type = "pw" },
},
},
execute = function(event)
local node = event:get_subject()
JsonUtils.match_rules (config.rules, node.properties, function (action, value)
if action == "create-filter-graph" then
local graphs = value:parse (1)
local graph_params = {}
for idx, val in ipairs (graphs) do
local index = tonumber(idx) - 1
local key = "audioconvert.filter-graph." .. tostring (index)
log:info (node, "setting node filter graph param '" .. key .. "' to: " .. val)
table.insert(graph_params, key)
table.insert(graph_params, val)
end
setNodeFilterGraphParams (node, graph_params)
end
end)
end
}:register()

View file

@ -0,0 +1,94 @@
-- WirePlumber
--
-- Copyright © 2025 Phosh.mobi e.V.
-- @author Guido Günther <agx@sigxcpu.org>
--
-- SPDX-License-Identifier: MIT
--
-- Select the media role default volume
log = Log.open_topic("s-node")
local cutils = require ("common-utils")
function findHighestPriorityRoleNode (node_om)
local best_role = nil
local best_prio = 0
local default_role = Settings.get ("node.stream.default-media-role")
if default_role then
default_role = default_role:parse()
end
for ni in node_om:iterate {
type = "node",
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.name", "#", "input.loopback.sink.role.*" },
} do
local ni_props = ni.properties
local roles = ni_props["device.intended-roles"]
local node_name = ni_props ["node.name"]
local prio = tonumber(ni_props ["policy.role-based.priority"])
-- Use the node that handles the default_role as fallback
-- when no node is in running state
if best_role == nil and roles and default_role then
local roles_table = Json.Raw(roles):parse()
for i, v in ipairs (roles_table) do
if default_role == v then
best_role = node_name
best_prio = prio
break
end
end
end
if ni.state == "running" then
if prio > best_prio then
best_role = node_name
best_prio = prio
end
end
end
log:info (string.format ("Volume control is on : '%s', prio %d", best_role, best_prio))
local metadata = cutils.get_default_metadata_object ()
metadata:set (0, "current.role-based.volume.control", "Spa:String:JSON",
Json.Object { ["name"] = best_role }:to_string ())
end
SimpleEventHook {
name = "node/rescan-for-media-role-volume",
interests = {
EventInterest {
Constraint { "event.type", "=", "rescan-for-media-role-volume" }
},
},
execute = function (event)
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
findHighestPriorityRoleNode (node_om)
end
}:register ()
-- Track best volume control for media role based priorities
SimpleEventHook {
name = "node/find-media-role-default-volume",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
},
EventInterest {
Constraint { "event.type", "=", "node-state-changed" },
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
},
},
execute = function (event)
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
source:call ("schedule-rescan", "media-role-volume")
end
}:register ()

View file

@ -348,8 +348,18 @@ function buildDefaultChannelVolumes (node)
for pod in node:iterate_params("Format") do for pod in node:iterate_params("Format") do
local pod_parsed = pod:parse() local pod_parsed = pod:parse()
if pod_parsed ~= nil then if pod_parsed ~= nil then
channels = pod_parsed.properties.channels local t = type(pod_parsed.properties.channels)
break if t == "number" then
channels = pod_parsed.properties.channels
break
elseif t == "table" and #pod_parsed.properties.channels > 0 then
-- in some misbehaving clients a non-fixed Format may appear here, which means the number of
-- channels will be some kind of choice. If this is the case, pick the first number in the
-- choice (which is either the default in an enum or range, or may just happen to be the
-- right number in other cases)
channels = pod_parsed.properties.channels[1]
break
end
end end
end end

View file

@ -1,30 +1,38 @@
if systemd.found() if get_option('systemd-system-service') or get_option('systemd-user-service')
systemd_config = configuration_data() systemd_config = configuration_data()
systemd_config.set('WP_BINARY', wireplumber_bin_dir / 'wireplumber') systemd_config.set('WP_BINARY', wireplumber_bin_dir / 'wireplumber')
systemd_system_unit_dir = ''
systemd_user_unit_dir = ''
if systemd.found()
systemd_system_unit_dir = systemd.get_variable(
pkgconfig: 'systemdsystemunitdir',
pkgconfig_define: ['prefix', get_option('prefix')])
systemd_user_unit_dir = systemd.get_variable(
pkgconfig: 'systemduserunitdir',
pkgconfig_define: ['prefix', get_option('prefix')])
endif
# system service # system service
if get_option('systemd-system-service') if get_option('systemd-system-service')
systemd_system_unit_dir = systemd.get_variable(
pkgconfig: 'systemdsystemunitdir',
pkgconfig_define: ['prefix', get_option('prefix')])
if get_option('systemd-system-unit-dir') != '' if get_option('systemd-system-unit-dir') != ''
systemd_system_unit_dir = get_option('systemd-system-unit-dir') systemd_system_unit_dir = get_option('systemd-system-unit-dir')
endif endif
subdir('system') if systemd_system_unit_dir != ''
subdir('system')
endif
endif endif
# user service # user service
if get_option('systemd-user-service') if get_option('systemd-user-service')
systemd_user_unit_dir = systemd.get_variable(
pkgconfig: 'systemduserunitdir',
pkgconfig_define: ['prefix', get_option('prefix')])
if get_option('systemd-user-unit-dir') != '' if get_option('systemd-user-unit-dir') != ''
systemd_user_unit_dir = get_option('systemd-user-unit-dir') systemd_user_unit_dir = get_option('systemd-user-unit-dir')
endif endif
subdir('user') if systemd_user_unit_dir != ''
subdir('user')
endif
endif endif
endif endif

View file

@ -9,7 +9,7 @@ LockPersonality=yes
MemoryDenyWriteExecute=yes MemoryDenyWriteExecute=yes
NoNewPrivileges=yes NoNewPrivileges=yes
SystemCallArchitectures=native SystemCallArchitectures=native
SystemCallFilter=@system-service SystemCallFilter=@system-service mincore
Type=simple Type=simple
AmbientCapabilities=CAP_SYS_NICE AmbientCapabilities=CAP_SYS_NICE
ExecStart=@WP_BINARY@ -p main-systemwide ExecStart=@WP_BINARY@ -p main-systemwide

View file

@ -14,7 +14,7 @@ LockPersonality=yes
MemoryDenyWriteExecute=yes MemoryDenyWriteExecute=yes
NoNewPrivileges=yes NoNewPrivileges=yes
SystemCallArchitectures=native SystemCallArchitectures=native
SystemCallFilter=@system-service SystemCallFilter=@system-service mincore
Type=simple Type=simple
AmbientCapabilities=CAP_SYS_NICE AmbientCapabilities=CAP_SYS_NICE
ExecStart=@WP_BINARY@ -p %i ExecStart=@WP_BINARY@ -p %i

View file

@ -9,7 +9,7 @@ LockPersonality=yes
MemoryDenyWriteExecute=yes MemoryDenyWriteExecute=yes
NoNewPrivileges=yes NoNewPrivileges=yes
SystemCallArchitectures=native SystemCallArchitectures=native
SystemCallFilter=@system-service SystemCallFilter=@system-service mincore
Type=simple Type=simple
ExecStart=@WP_BINARY@ ExecStart=@WP_BINARY@
Restart=on-failure Restart=on-failure

View file

@ -14,7 +14,7 @@ LockPersonality=yes
MemoryDenyWriteExecute=yes MemoryDenyWriteExecute=yes
NoNewPrivileges=yes NoNewPrivileges=yes
SystemCallArchitectures=native SystemCallArchitectures=native
SystemCallFilter=@system-service SystemCallFilter=@system-service mincore
Type=simple Type=simple
ExecStart=@WP_BINARY@ -p %i ExecStart=@WP_BINARY@ -p %i
Restart=on-failure Restart=on-failure

View file

@ -4,6 +4,11 @@ executable('wpctl',
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep, libintl_dep], dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep, libintl_dep],
) )
install_data('shell-completion/wpctl.bash',
install_dir: get_option('datadir') / 'bash-completion/completions',
rename: 'wpctl'
)
install_data('shell-completion/wpctl.zsh', install_data('shell-completion/wpctl.zsh',
install_dir: get_option('datadir') / 'zsh/site-functions', install_dir: get_option('datadir') / 'zsh/site-functions',
rename: '_wpctl' rename: '_wpctl'

View file

@ -0,0 +1,41 @@
_wpctl_pw_defaults() {
local defaults="@DEFAULT_SINK@ @DEFAULT_AUDIO_SINK@ @DEFAULT_SOURCE@
@DEFAULT_AUDIO_SOURCE@ @DEFAULT_VIDEO_SOURCE@"
COMPREPLY+=($(compgen -W "$defaults" -- "$cur"))
}
_wpctl() {
local cur prev words cword
local commands="status get-volume inspect set-default set-volume set-mute
set-profile set-route clear-default settings set-log-level
list"
_init_completion -n = || return
if [[ ${#COMP_WORDS[@]} -eq 2 ]]; then
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
return
fi
case $prev in
get-volume | inspect | set-volume | set-mute | set-profile | set-route)
_wpctl_pw_defaults
;;
clear-default)
COMPREPLY+=($(compgen -W "0 1 2" -- "$cur"))
;;
list)
COMPREPLY+=($(compgen -W "audio video" -- "$cur"))
;;
audio|video)
if [[ ${COMP_WORDS[COMP_CWORD-2]} == "list" ]]; then
COMPREPLY+=($(compgen -W "devices sinks sources" -- "$cur"))
fi
;;
esac
}
complete -F _wpctl wpctl

View file

@ -35,12 +35,31 @@ struct _WpCtl
gint exit_code; gint exit_code;
}; };
typedef enum {
LIST_MEDIA_ALL = 0,
LIST_MEDIA_AUDIO,
LIST_MEDIA_VIDEO,
} ListMediaType;
typedef enum {
LIST_OBJECT_ALL = 0,
LIST_OBJECT_DEVICES,
LIST_OBJECT_SINKS,
LIST_OBJECT_SOURCES,
} ListObjectType;
static struct { static struct {
union { union {
struct { struct {
gboolean display_nicknames; gboolean display_nicknames;
gboolean display_names; gboolean display_names;
} status; } status;
struct {
ListMediaType media_type;
ListObjectType object_type;
} list;
struct { struct {
guint64 id; guint64 id;
gboolean show_referenced; gboolean show_referenced;
@ -543,6 +562,159 @@ status_run (WpCtl * self)
g_main_loop_quit (self->loop); g_main_loop_quit (self->loop);
} }
/* list */
static gboolean
list_parse_positional (gint argc, gchar ** argv, GError **error)
{
cmdline.list.media_type = LIST_MEDIA_ALL;
cmdline.list.object_type = LIST_OBJECT_ALL;
if (argc < 3)
return TRUE;
if (g_strcmp0 (argv[2], "audio") == 0) {
cmdline.list.media_type = LIST_MEDIA_AUDIO;
} else if (g_strcmp0 (argv[2], "video") == 0) {
cmdline.list.media_type = LIST_MEDIA_VIDEO;
} else {
g_set_error (error, wpctl_error_domain_quark(), 0,
"'%s' is not a valid list option", argv[2]);
return FALSE;
}
if (argc < 4)
return TRUE;
if (g_strcmp0 (argv[3], "devices") == 0) {
cmdline.list.object_type = LIST_OBJECT_DEVICES;
} else if (g_strcmp0 (argv[3], "sinks") == 0) {
cmdline.list.object_type = LIST_OBJECT_SINKS;
} else if (g_strcmp0 (argv[3], "sources") == 0) {
cmdline.list.object_type = LIST_OBJECT_SOURCES;
} else {
g_set_error (error, wpctl_error_domain_quark(), 0,
"'%s' is not a valid list option", argv[3]);
return FALSE;
}
return TRUE;
}
static gboolean
list_prepare (WpCtl * self, GError ** error)
{
wp_object_manager_add_interest (self->om, WP_TYPE_DEVICE, NULL);
wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
wp_object_manager_add_interest (self->om, WP_TYPE_METADATA, NULL);
return TRUE;
}
struct list_context
{
guint32 default_node;
const gchar *media_type;
const gchar *object_type;
};
static void
list_print_device (const GValue *item, gpointer data)
{
WpPipewireObject *obj = g_value_get_object (item);
struct list_context *context = data;
guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_NAME);
printf ("%u\t%s\t%s/%s\t \n", id, name, context->media_type, context->object_type);
}
static void
list_print_dev_node (const GValue *item, gpointer data)
{
WpPipewireObject *obj = g_value_get_object (item);
struct list_context *context = data;
guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
gboolean is_default = (context->default_node == id);
const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_NAME);
printf ("%u\t%s\t%s/%s\t%c\n", id, name, context->media_type, context->object_type, is_default ? '*' : ' ');
}
static const struct {
const gchar *title_name;
const gchar *lower_name;
ListMediaType value;
} list_media_types[] = {
{ "Audio", "audio", LIST_MEDIA_AUDIO },
{ "Video", "video", LIST_MEDIA_VIDEO },
};
static void
list_run (WpCtl * self)
{
struct list_context context;
g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
const ListMediaType media_filter = cmdline.list.media_type;
const ListObjectType object_filter = cmdline.list.object_type;
for (guint i = 0; i < G_N_ELEMENTS (list_media_types); i++) {
if (media_filter != LIST_MEDIA_ALL &&
media_filter != list_media_types[i].value)
continue;
const gchar *media_type = list_media_types[i].title_name;
context.media_type = list_media_types[i].lower_name;
gchar media_type_glob[16];
gchar media_class[24];
g_snprintf (media_type_glob, sizeof(media_type_glob), "*%s*", media_type);
/* Devices */
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_DEVICES) {
context.object_type = "device";
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
WP_TYPE_DEVICE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
NULL);
wp_iterator_foreach (it, list_print_device, &context);
}
/* Sinks */
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_SINKS) {
g_snprintf (media_class, sizeof(media_class), "%s/Sink", media_type);
context.default_node = -1;
context.object_type = "sink";
if (def_nodes_api)
g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
&context.default_node);
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
WP_TYPE_NODE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Sink*",
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
NULL);
wp_iterator_foreach (it, list_print_dev_node, &context);
}
/* Sources */
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_SOURCES) {
g_snprintf (media_class, sizeof(media_class), "%s/Source", media_type);
context.default_node = -1;
context.object_type = "source";
if (def_nodes_api)
g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
&context.default_node);
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
WP_TYPE_NODE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Source*",
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
NULL);
wp_iterator_foreach (it, list_print_dev_node, &context);
}
}
g_main_loop_quit (self->loop);
}
/* get-volume */ /* get-volume */
static gboolean static gboolean
@ -870,7 +1042,8 @@ set_default_run (WpCtl * self)
media_class = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (proxy), media_class = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (proxy),
PW_KEY_MEDIA_CLASS); PW_KEY_MEDIA_CLASS);
for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) { for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) {
if (!g_strcmp0 (media_class, DEFAULT_NODE_MEDIA_CLASSES[i])) { if (g_str_has_prefix (media_class, DEFAULT_NODE_MEDIA_CLASSES[i]) &&
!g_str_has_suffix (media_class, "/Internal")) {
gboolean res = FALSE; gboolean res = FALSE;
const gchar *name = wp_pipewire_object_get_property ( const gchar *name = wp_pipewire_object_get_property (
WP_PIPEWIRE_OBJECT (proxy), PW_KEY_NODE_NAME); WP_PIPEWIRE_OBJECT (proxy), PW_KEY_NODE_NAME);
@ -1713,6 +1886,16 @@ static const struct subcommand {
.prepare = status_prepare, .prepare = status_prepare,
.run = status_run, .run = status_run,
}, },
{
.name = "list",
.positional_args = "[audio|video] [devices|sinks|sources]",
.summary = "Displays PipeWire objects, optionally filtered by media and object type",
.description = NULL,
.entries = { { NULL } },
.parse_positional = list_parse_positional,
.prepare = list_prepare,
.run = list_run,
},
{ {
.name = "get-volume", .name = "get-volume",
.positional_args = "ID", .positional_args = "ID",
@ -1901,7 +2084,8 @@ main (gint argc, gchar **argv)
ctl.context = g_option_context_new ( ctl.context = g_option_context_new (
"COMMAND [COMMAND_OPTIONS] - WirePlumber Control CLI"); "COMMAND [COMMAND_OPTIONS] - WirePlumber Control CLI");
ctl.loop = g_main_loop_new (NULL, FALSE); ctl.loop = g_main_loop_new (NULL, FALSE);
ctl.core = wp_core_new (NULL, NULL, NULL); ctl.core = wp_core_new (NULL, NULL, wp_properties_new (PW_KEY_REMOTE_NAME,
("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"), NULL));
ctl.om = wp_object_manager_new (); ctl.om = wp_object_manager_new ();
/* find the subcommand */ /* find the subcommand */

View file

@ -1,12 +1,14 @@
[wrap-file] [wrap-file]
directory = lua-5.4.4 directory = lua-5.5.0
source_url = https://www.lua.org/ftp/lua-5.4.4.tar.gz source_url = https://www.lua.org/ftp/lua-5.5.0.tar.gz
source_filename = lua-5.4.4.tar.gz source_filename = lua-5.5.0.tar.gz
source_hash = 164c7849653b80ae67bec4b7473b884bf5cc8d2dca05653475ec2ed27b9ebf61 source_hash = 57ccc32bbbd005cab75bcc52444052535af691789dba2b9016d5c50640d68b3d
patch_filename = lua_5.4.4-1_patch.zip source_fallback_url = https://wrapdb.mesonbuild.com/v2/lua_5.5.0-1/get_source/lua-5.5.0.tar.gz
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.4-1/get_patch patch_filename = lua_5.5.0-1_patch.zip
patch_hash = e61cd965c629d6543176f41a9f1cb9050edfd1566cf00ce768ff211086e40bdc patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.5.0-1/get_patch
patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/lua_5.5.0-1/lua_5.5.0-1_patch.zip
patch_hash = 69ec4a2dd99ecf8e84830093d418f3a5be1202f16ba8d636b3008b67506e5cca
wrapdb_version = 5.5.0-1
[provide] [provide]
lua-5.4 = lua_dep dependency_names = lua, lua-5.5

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