Compare commits

..

No commits in common. "master" and "0.5.12" have entirely different histories.

106 changed files with 1558 additions and 7114 deletions

View file

@ -154,22 +154,13 @@ 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
# Set build options based on PipeWire version - meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
- | -Dpipewire-alsa=disabled -Dpipewire-jack=disabled
case "$PIPEWIRE_HEAD" in -Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
1.0|1.2|1.4) -Dvulkan=disabled -Dgstreamer=disabled -Dlibsystemd=disabled
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled" -Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
;; -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
*) -Decho-cancel-webrtc=disabled -Dsession-managers=[]
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
@ -245,9 +236,6 @@ 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:

View file

@ -1,37 +0,0 @@
## 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,144 +1,5 @@
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 WirePlumber 0.5.12
.................. ~~~~~~~~~~~~~~~~~~
Additions & Enhancements: Additions & Enhancements:
@ -166,6 +27,9 @@ 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,68 +58,3 @@ 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-based .. describe:: policy.role-priority-system
Enables the role based priority system policy. This system creates virtual sinks Enables the role 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,10 +43,6 @@ 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
@ -55,10 +51,6 @@ 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 -C build run $ make run
Run independently or without installing Run independently or without installing
--------------------------------------- ---------------------------------------

View file

@ -23,7 +23,6 @@ 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,7 +18,6 @@ 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

@ -1,17 +0,0 @@
.. _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 selected for a After that, once the media class of a device node has been select 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,7 +9,6 @@
#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")
@ -26,7 +25,6 @@ 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 (
@ -41,7 +39,6 @@ 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
@ -79,27 +76,11 @@ 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)
{ {
@ -107,7 +88,6 @@ 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 =
@ -241,30 +221,3 @@ 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,7 +10,6 @@
#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
@ -38,10 +37,6 @@ 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_copy_dict (info->props); props = wp_properties_new_wrap_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,161 +15,6 @@
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
{ {
@ -204,8 +49,7 @@ struct _WpEventDispatcher
GObject parent; GObject parent;
GWeakRef core; GWeakRef core;
GHashTable *defined_hooks; /* registered hooks for defined events */ GPtrArray *hooks; /* registered hooks */
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;
@ -260,7 +104,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);
if (levent) { while (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);
@ -300,8 +144,6 @@ 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;
@ -318,9 +160,7 @@ 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->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
(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;
@ -344,8 +184,7 @@ wp_event_dispatcher_finalize (GObject * object)
close (self->eventfd); close (self->eventfd);
g_clear_pointer (&self->defined_hooks, g_hash_table_unref); g_clear_pointer (&self->hooks, g_ptr_array_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);
@ -445,10 +284,6 @@ 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));
@ -457,74 +292,7 @@ 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);
} }
/*! /*!
@ -538,9 +306,6 @@ 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));
@ -549,29 +314,11 @@ 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
@ -580,56 +327,7 @@ add_unique (GPtrArray *array, WpEventHook * hook)
WpIterator * WpIterator *
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self) wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
{ {
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref); GPtrArray *items =
GHashTableIter iter; g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
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,12 +41,7 @@ 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,24 +254,6 @@ 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()
* *
@ -339,61 +321,34 @@ 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);
if (wp_object_interest_matches_full (interest, match = wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_NONE, WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL) gtype, subject, properties, properties);
/* the interest may have a GType that matches the GType of the subject
or it may have WP_TYPE_EVENT as its GType, in which case it will
match any type of subject */
if (match == WP_INTEREST_MATCH_ALL)
return TRUE;
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
match = wp_object_interest_matches_full (interest, 0,
WP_TYPE_EVENT, NULL, NULL, NULL);
if (match & WP_INTEREST_MATCH_GTYPE)
return TRUE; return TRUE;
}
return FALSE;
}
static void
add_unique (GPtrArray *array, const gchar * lookup)
{
for (guint i = 0; i < array->len; i++)
if (g_str_equal (g_ptr_array_index (array, i), lookup))
return;
g_ptr_array_add (array, g_strdup (lookup));
}
static GPtrArray *
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
{
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
WpInterestEventHookPrivate *priv =
wp_interest_event_hook_get_instance_private (self);
g_autoptr (GPtrArray) res = g_ptr_array_new_with_free_func (g_free);
guint i;
for (i = 0; i < priv->interests->len; i++) {
WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
g_autoptr (GPtrArray) values =
wp_object_interest_find_defined_constraint_values (interest,
WP_CONSTRAINT_TYPE_NONE, "event.type");
if (!values || values->len == 0) {
/* We always consider the hook undefined if it has at least one interest
* without a defined 'event.type' constraint */
return NULL;
} else {
for (guint j = 0; j < values->len; j++) {
GVariant *v = g_ptr_array_index (values, j);
if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
const gchar *v_str = g_variant_get_string (v, NULL);
add_unique (res, v_str);
}
}
}
} }
} }
return FALSE;
return g_steal_pointer (&res);
} }
static void static void
@ -404,8 +359,6 @@ 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,10 +39,8 @@ 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(4) WP_PADDING(5)
}; };
WP_API WP_API
@ -69,9 +67,6 @@ 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,11 +17,37 @@
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;
GPtrArray *hooks; struct spa_list hooks;
/* immutable fields */ /* immutable fields */
gint priority; gint priority;
@ -70,7 +96,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);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref); spa_list_init (&self->hooks);
self->priority = priority; self->priority = priority;
self->properties = properties ? self->properties = properties ?
@ -129,7 +155,11 @@ wp_event_get_name(WpEvent *self)
static void static void
wp_event_free (WpEvent * self) wp_event_free (WpEvent * self)
{ {
g_clear_pointer (&self->hooks, g_ptr_array_unref); HookData *hook_data;
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);
@ -286,6 +316,33 @@ 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
@ -298,37 +355,199 @@ wp_event_get_data (WpEvent * self, const gchar * key)
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);
/* Clear all current hooks */ /* hooks already collected */
g_ptr_array_set_size (event->hooks, 0); if (!spa_list_is_empty (&event->hooks))
return TRUE;
/* Get the event type */ spa_list_init (&collected);
event_type = wp_properties_get (event->properties, "event.type"); spa_list_init (&result);
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s", spa_list_init (&remaining);
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_for_event_type_iterator (dispatcher, all_hooks = wp_event_dispatcher_new_hooks_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)) {
g_ptr_array_add (event->hooks, g_object_ref (hook)); HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
wp_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);
} }
return event->hooks->len > 0; 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);
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()
@ -339,8 +558,15 @@ wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
WpIterator * WpIterator *
wp_event_new_hooks_iterator (WpEvent * event) wp_event_new_hooks_iterator (WpEvent * event)
{ {
GPtrArray *hooks; WpIterator *it = NULL;
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL); struct event_hooks_iterator_data *it_data;
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,7 +22,6 @@ 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',
@ -70,7 +69,6 @@ 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,8 +121,6 @@ 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,51 +881,3 @@ 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,10 +130,6 @@ 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

View file

@ -1,707 +0,0 @@
/* 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

@ -1,88 +0,0 @@
/* 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

@ -1,26 +0,0 @@
/* 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_copy_dict (props); d->properties = wp_properties_new_wrap_dict (props);
g_object_notify (G_OBJECT (instance), "properties"); g_object_notify (G_OBJECT (instance), "properties");
} }

View file

@ -6,9 +6,7 @@
* 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"
@ -147,21 +145,6 @@ 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
@ -172,46 +155,51 @@ 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);
char path [64]; g_autofree gchar *status = NULL;
spa_autoclose int base_fd = -1; g_autoptr (GError) error = NULL;
FILE *file; gsize length = 0;
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", {
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0); g_autofree gchar *path = g_strdup_printf ("/proc/%d/status", pid);
if (file) { if (g_file_get_contents (path, &status, &length, &error)) {
while (getline (&line, &size, file) > 1) const gchar *loc = strstr (status, "\nPPid:");
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1) if (loc) {
break; const gint res = sscanf (loc, "\nPPid:%d\n", &ret->parent);
fclose (file); if (!res || res == EOF)
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", {
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0); g_autofree gchar *path = g_strdup_printf ("/proc/%d/cgroup", pid);
if (file) { if (g_file_get_contents (path, &ret->cgroup, &length, &error)) {
if (getline (&line, &size, file) > 1) if (length > 0)
ret->cgroup = g_strstrip (g_strdup (line)); ret->cgroup [length - 1] = '\0'; /* Remove EOF character */
fclose (file); } else {
wp_warning ("failed to get cgroup for PID %d: %s", pid, error->message);
}
} }
/* Get args */ /* Get args */
file = fdopenat (base_fd, "cmdline", {
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0); g_autofree gchar *path = g_strdup_printf ("/proc/%d/cmdline", pid);
if (file) { FILE *file = fopen (path, "rb");
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS) if (file) {
ret->args[ret->n_args++] = g_strdup (line); g_autofree gchar *lineptr = NULL;
fclose (file); size_t size = 0;
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

@ -1163,7 +1163,7 @@ wp_spa_pod_get_double (WpSpaPod *self, double *value)
* *
* \ingroup wpspapod * \ingroup wpspapod
* \param self the spa pod object * \param self the spa pod object
* \param value (out) (transfer none): the string value * \param value (out): the string value
* \returns TRUE if the value was obtained, FALSE otherwise * \returns TRUE if the value was obtained, FALSE otherwise
*/ */
gboolean gboolean
@ -1709,7 +1709,7 @@ wp_spa_pod_get_struct_valist (WpSpaPod *self, va_list args)
* *
* \ingroup wpspapod * \ingroup wpspapod
* \param self the spa pod object * \param self the spa pod object
* \param key (out) (optional) (transfer none): the name of the property * \param key (out) (optional): the name of the property
* \param value (out) (optional): the spa pod value of the property * \param value (out) (optional): the spa pod value of the property
* \returns TRUE if the value was obtained, FALSE otherwise * \returns TRUE if the value was obtained, FALSE otherwise
*/ */
@ -1745,7 +1745,7 @@ wp_spa_pod_get_property (WpSpaPod *self, const char **key,
* \ingroup wpspapod * \ingroup wpspapod
* \param self the spa pod object * \param self the spa pod object
* \param offset (out) (optional): the offset of the control * \param offset (out) (optional): the offset of the control
* \param ctl_type (out) (optional) (transfer none): the control type (Properties, Midi, ...) * \param ctl_type (out) (optional): the control type (Properties, Midi, ...)
* \param value (out) (optional): the spa pod value of the control * \param value (out) (optional): the spa pod value of the control
* \returns TRUE if the value was obtained, FALSE otherwise * \returns TRUE if the value was obtained, FALSE otherwise
*/ */
@ -2566,7 +2566,7 @@ wp_spa_pod_parser_get_double (WpSpaPodParser *self, double *value)
* *
* \ingroup wpspapod * \ingroup wpspapod
* \param self the spa pod parser object * \param self the spa pod parser object
* \param value (out) (transfer none): the string value * \param value (out): the string value
* \returns TRUE if the value was obtained, FALSE otherwise * \returns TRUE if the value was obtained, FALSE otherwise
*/ */
gboolean gboolean

View file

@ -47,7 +47,6 @@
#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.14', version : '0.5.12',
license : 'MIT', license : 'MIT',
meson_version : '>= 0.59.0', meson_version : '>= 0.59.0',
default_options : [ default_options : [
@ -88,24 +88,27 @@ 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_versions = [ lua_dep = dependency('lua-5.4', required: false)
'-5.5', '5.5', '55', if not lua_dep.found()
'-5.4', '5.4', '54', lua_dep = dependency('lua5.4', required: false)
'-5.3', '5.3', '53', endif
] if not lua_dep.found()
lua_dep = dependency('lua54', required: false)
foreach v : lua_versions endif
lua_dep = dependency('lua' + v, required: false) if not lua_dep.found()
if lua_dep.found() lua_dep = dependency('lua-5.3', required: false)
break endif
endif if not lua_dep.found()
endforeach lua_dep = dependency('lua5.3', required: false)
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.5, 5.4 or 5.3 required') error ('Could not find lua. Lua version 5.4 or 5.3 required')
endif endif
endif endif
else else
@ -155,25 +158,7 @@ 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()
@ -182,9 +167,7 @@ if build_modules
subdir('modules') subdir('modules')
endif endif
subdir('src') subdir('src')
if build_daemon subdir('po')
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);
WpProperties *p = wp_core_get_properties (core); g_autoptr (WpProperties) p = wp_core_get_properties (core);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p); wplua_properties_to_table (L, 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);
WpProperties *p = wp_core_get_remote_properties (core); g_autoptr (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_pushboxed (L, WP_TYPE_PROPERTIES, p); wplua_properties_to_table (L, p);
lua_setfield (L, -2, "properties"); lua_setfield (L, -2, "properties");
return 1; return 1;
} }
@ -297,13 +297,8 @@ 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);
WpProperties *props = NULL; luaL_checktype (L, 1, LUA_TTABLE);
if (lua_istable (L, 1)) wp_core_update_properties (core, wplua_table_to_properties (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;
} }
@ -604,28 +599,6 @@ 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
@ -864,11 +837,7 @@ 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 = 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));
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");
@ -1028,11 +997,10 @@ 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_istable (L, 2)) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
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);
@ -1049,11 +1017,10 @@ 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_istable (L, 2)) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
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);
@ -1070,11 +1037,10 @@ 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_istable (L, 2)) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
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);
@ -1139,11 +1105,10 @@ 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_istable (L, 2)) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
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);
@ -1249,11 +1214,10 @@ 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_istable (L, 2)) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
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);
@ -1286,11 +1250,10 @@ 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_istable (L, 2)) if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
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)
@ -1366,12 +1329,9 @@ 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;
if (lua_istable (L, 2)) luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2); WpProperties *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;
@ -1388,22 +1348,10 @@ 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 }
}; };
@ -1443,12 +1391,46 @@ 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; WpProperties *props = wp_properties_new_empty ();
if (lua_istable (L, 2)) /* validate arguments */
props = wplua_table_to_properties (L, 2); luaL_checktype (L, 2, LUA_TTABLE);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES)); /* build the configuration 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;
@ -1470,23 +1452,12 @@ 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 }
}; };
@ -1556,24 +1527,19 @@ on_enum_params_done (WpPipewireObject * pwobj, GAsyncResult * res,
GClosure * closure) GClosure * closure)
{ {
g_autoptr (GError) error = NULL; g_autoptr (GError) error = NULL;
GValue vals[2] = { G_VALUE_INIT, G_VALUE_INIT }; GValue val = G_VALUE_INIT;
int n_vals = 1; int n_vals = 0;
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 (&vals[1], G_TYPE_STRING); g_value_init (&val, G_TYPE_STRING);
g_value_set_string (&vals[1], error->message); g_value_set_string (&val, error->message);
n_vals = 2; n_vals = 1;
} }
g_clear_pointer (&it, wp_iterator_unref); g_clear_pointer (&it, wp_iterator_unref);
g_closure_invoke (closure, NULL, n_vals, vals, NULL); g_closure_invoke (closure, NULL, n_vals, &val, 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);
} }
@ -1609,22 +1575,11 @@ 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 }
}; };
@ -1651,14 +1606,9 @@ 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);
g_autoptr (WpProperties) props = NULL; luaL_checktype (L, 2, LUA_TTABLE);
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 : "");
@ -1669,13 +1619,8 @@ 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);
g_autoptr (WpProperties) props = NULL; luaL_checktype (L, 2, LUA_TTABLE);
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;
} }
@ -1684,8 +1629,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);
WpProperties *props = wp_state_load (state); g_autoptr (WpProperties) props = wp_state_load (state);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props); wplua_properties_to_table (L, props);
return 1; return 1;
} }
@ -1703,18 +1648,17 @@ static int
impl_module_new (lua_State *L) impl_module_new (lua_State *L)
{ {
const char *name, *args = NULL; const char *name, *args = NULL;
g_autoptr (WpProperties) properties = NULL; 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_istable (L, 3)) if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
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);
@ -1737,10 +1681,9 @@ 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) {
@ -1778,7 +1721,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;
WpProperties *props = NULL; g_autoptr (WpProperties) props = NULL;
int argi = 1; int argi = 1;
/* check if called as method on object */ /* check if called as method on object */
@ -1793,8 +1736,6 @@ 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 ();
@ -1803,7 +1744,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_pushboxed (L, WP_TYPE_PROPERTIES, props); wplua_properties_to_table (L, props);
return 1; return 1;
} }
@ -1960,12 +1901,10 @@ 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);
if (lua_istable (L, 2)) 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));
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);
@ -1981,21 +1920,17 @@ 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)
{ {
WpProperties *properties = NULL; g_autoptr (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);
if (lua_istable (L, 2)) 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));
count = wp_json_utils_match_rules_update_properties (json, properties); count = wp_json_utils_match_rules_update_properties (json, properties);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, properties); wplua_properties_to_table (L, properties);
lua_pushinteger (L, count); lua_pushinteger (L, count);
return 2; return 2;
} }
@ -2077,108 +2012,6 @@ 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
@ -2472,8 +2305,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);
WpProperties *props = wp_event_get_properties (event); g_autoptr (WpProperties) props = wp_event_get_properties (event);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props); wplua_properties_to_table (L, props);
return 1; return 1;
} }
@ -2595,11 +2428,10 @@ 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_istable (L, -1)) if (lua_gettable (L, 1) != LUA_TNIL) {
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");
@ -2629,132 +2461,6 @@ 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
@ -3278,12 +2984,6 @@ 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,28 +184,6 @@ 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
@ -214,7 +192,6 @@ 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,
@ -240,8 +217,6 @@ 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,54 +300,40 @@ 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 ();
if (lua_istable (L, 1)) { luaL_checktype (L, 1, LUA_TTABLE);
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,9 +29,8 @@ _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_tolstring (L, 2, NULL); const gchar *key = luaL_checkstring (L, 2);
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;
@ -54,104 +53,6 @@ _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;
} }
@ -168,8 +69,6 @@ _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,6 +14,7 @@ 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) {
@ -23,34 +24,11 @@ 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 */
luaL_checkany (L, -2); key = luaL_tolstring (L, -2, NULL);
switch (lua_type (L, -2)) { value = luaL_tolstring (L, -2, NULL);
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 */
@ -335,7 +313,10 @@ 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:
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v)); if (G_VALUE_TYPE (v) == WP_TYPE_PROPERTIES)
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,15 +103,14 @@ 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);
g_autoptr (GError) err = NULL; GError *err = NULL;
GDBusProxy *call; GDBusProxy *call;
g_autoptr (GVariant) prop = NULL; GVariant *prop;
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) {
g_prefix_error (&err, "Failed to get call: "); wp_warning_object (wpmm, "Failed to get call");
wp_warning_object (wpmm, "%s", err->message);
return; return;
} }
@ -123,6 +122,8 @@ 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);
@ -164,7 +165,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,
@ -174,8 +175,9 @@ 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);
@ -183,6 +185,8 @@ 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);
} }
} }
@ -192,23 +196,22 @@ list_calls_done (GObject * obj,
gpointer data) gpointer data)
{ {
WpModemManager *wpmm = WP_MODEM_MANAGER (data); WpModemManager *wpmm = WP_MODEM_MANAGER (data);
g_autoptr (GVariant) params = NULL; GVariant *params;
g_autoptr (GVariantIter) calls = NULL; GVariantIter *calls;
gchar *path; gchar *path;
g_autoptr (GError) err = NULL; 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,
@ -219,6 +222,9 @@ list_calls_done (GObject * obj,
bind_call, bind_call,
wpmm); wpmm);
} }
g_variant_iter_free (calls);
g_variant_unref (params);
} }
static void static void
@ -347,7 +353,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);
g_autoptr (WpCore) core = NULL; WpCore *core;
GError *err; GError *err;
g_autoptr (GDBusConnection) conn = NULL; g_autoptr (GDBusConnection) conn = NULL;

View file

@ -98,10 +98,8 @@ static void item_free (gpointer data)
{ {
Item *item = data; Item *item = data;
g_clear_pointer (&item->desktop_entry, g_free); free(item->desktop_entry);
g_clear_pointer (&item->flatpak_app_id, g_free); free(item);
g_clear_pointer (&item->flatpak_instance_id, g_free);
g_free (item);
} }
static Players *players_new (GDBusConnection *conn) static Players *players_new (GDBusConnection *conn)
@ -130,7 +128,7 @@ static void players_unref (Players *players)
return; return;
g_mutex_clear (&players->lock); g_mutex_clear (&players->lock);
g_clear_pointer (&players->items, g_hash_table_unref); g_clear_object (&players->items);
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);
@ -238,7 +236,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_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) { if (!g_str_equal(g_variant_get_type_string (value), "s")) {
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;
} }
@ -428,8 +426,6 @@ 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
@ -533,7 +529,9 @@ 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 ("av")); g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
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,7 +13,6 @@ 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
{ {
@ -61,8 +60,7 @@ 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, g_variant_new ("(ss)", table, id), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL,
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);
@ -99,8 +97,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, create, id, permissions, data), g_variant_new ("(sbs@a{sas}@v)", table, id, permissions, data), NULL,
NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_CALL_TIMEOUT_MSEC, NULL, &error); G_DBUS_CALL_FLAGS_NONE, -1, 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,11 +137,10 @@ 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, n_pos; uint32_t i;
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;
n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position)); for (i = 0; i < info->channels; i++)
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;
@ -198,7 +197,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_N_ELEMENTS(raw_format.position))) !spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS))
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED); SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
if (mono) { if (mono) {
@ -350,8 +349,7 @@ 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 ();
guint n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position)); for (guint i = 0; i < info->channels; i++)
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,16 +195,12 @@ 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, &error)) { if (wp_object_activate_finish (proxy, res, NULL))
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,7 +38,6 @@ 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;
@ -49,7 +48,6 @@ 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)) {
@ -159,14 +157,10 @@ 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"))
@ -211,8 +205,7 @@ 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,18 +1,24 @@
# 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/-/" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
"issues\n" "POT-Creation-Date: 2025-02-05 03:57+0000\n"
"POT-Creation-Date: 2025-12-23 17:57+0000\n" "PO-Revision-Date: 2025-02-05 12:04+0100\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: Poedit 3.8\n" "X-Generator: Gtranslator 47.0\n"
#. WirePlumber #. WirePlumber
#. #.
@ -42,7 +48,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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* 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
@ -51,15 +57,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:438 #: src/scripts/monitors/alsa.lua:433
msgid "Loopback" msgid "Loopback"
msgstr "Локален интерфейс" msgstr "Локален интерфейс"
#: src/scripts/monitors/alsa.lua:440 #: src/scripts/monitors/alsa.lua:435
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Вградено аудио" msgstr "Вградено аудио"
#: src/scripts/monitors/alsa.lua:442 #: src/scripts/monitors/alsa.lua:437
msgid "Modem" msgid "Modem"
msgstr "Модем" msgstr "Модем"
@ -68,7 +74,6 @@ 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
@ -130,284 +135,3 @@ 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,16 +13,6 @@ 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"
@ -133,16 +123,6 @@ 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"
@ -175,7 +155,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 device sink nodes in MONO" msgid "Configure all audio 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 2026. # Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022.
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: 2026-01-29 15:48+0100\n" "PO-Revision-Date: 2022-07-25 13:53+0200\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.8\n" "X-Generator: Poedit 3.1.1\n"
#. WirePlumber #. WirePlumber
#. #.
@ -102,282 +102,3 @@ 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,41 +1,37 @@
# 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-2026. # Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2020.
# #
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\n" "issues/new\n"
"POT-Creation-Date: 2025-12-23 17:57+0000\n" "POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2026-03-01 19:33+0500\n" "PO-Revision-Date: 2020-06-30 08:04+0500\n"
"Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n" "Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n"
"Language-Team: Kazakh <kk_KZ@googlegroups.com>\n" "Language-Team: \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=1; plural=0;\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.8\n" "X-Generator: Poedit 2.3.1\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
#. unique device/node name tables #. Receive script arguments from config.lua
#. SPA ids to node names: name = id_name_table[device_id][node_id] #. ensure config.properties is not nil
#. create the underlying hidden ALSA node #. preprocess rules and create Interest objects
#. not suitable for loopback #. applies properties from config.rules when asked to
#: 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 amount of channels #. try to negotiate the max ammount 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,373 +41,16 @@ 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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. apply properties from config.rules
#. 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:438 #: src/scripts/monitors/alsa.lua:222
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:442 #: src/scripts/monitors/alsa.lua:224
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,22 +2,24 @@
# 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/-/issues\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"POT-Creation-Date: 2025-12-15 16:28+0000\n" "issues\n"
"PO-Revision-Date: 2025-12-15 23:31+0100\n" "POT-Creation-Date: 2025-08-21 03:57+0000\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%100==4 ? 3 : 0);\n" "Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n"
"X-Generator: Poedit 3.8\n" "%100==4 ? 3 : 0);\n"
"X-Generator: Poedit 2.2.1\n"
#. WirePlumber #. WirePlumber
#. #.
@ -47,7 +49,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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* 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
@ -73,7 +75,6 @@ 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
@ -200,34 +201,6 @@ 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"
@ -274,19 +247,6 @@ 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"
@ -317,16 +277,6 @@ 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,24 +3,22 @@
# 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\n" "issues/new\n"
"POT-Creation-Date: 2025-12-23 17:57+0000\n" "POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2026-04-11 14:02+0200\n" "PO-Revision-Date: 2012-01-30 09:55+0000\n"
"Last-Translator: Марко Костић <marko.m.kostic@gmail.com>\n" "Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n"
"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n" "Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
"Language: sr\n" "Language: \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<12 || n%100>14) ? 1 : 2);\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Poedit 3.9\n"
#. WirePlumber #. WirePlumber
#. #.
@ -28,19 +26,13 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. unique device/node name tables #. Receive script arguments from config.lua
#. SPA ids to node names: name = id_name_table[device_id][node_id] #. ensure config.properties is not nil
#. create the underlying hidden ALSA node #. preprocess rules and create Interest objects
#. not suitable for loopback #. applies properties from config.rules when asked to
#: 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 amount of channels #. try to negotiate the max ammount 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
@ -50,366 +42,16 @@ 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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. apply properties from config.rules
#. 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:438 #: src/scripts/monitors/alsa.lua:222
msgid "Loopback"
msgstr "Затворена петља"
#: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Унутрашњи звук" msgstr "Унутрашњи звук"
#: src/scripts/monitors/alsa.lua:442 #: src/scripts/monitors/alsa.lua:224
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,26 +1,24 @@
# Serbian translations for pipewire # Serbian(Latin) 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\n" "issues/new\n"
"POT-Creation-Date: 2025-12-23 17:57+0000\n" "POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2026-04-11 14:02+0200\n" "PO-Revision-Date: 2012-01-30 09:55+0000\n"
"Last-Translator: Marko Kostić <marko.m.kostic@gmail.com>\n" "Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n"
"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n" "Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
"Language: sr\n" "Language: \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<12 || n%100>14) ? 1 : 2);\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Poedit 3.9\n"
#. WirePlumber #. WirePlumber
#. #.
@ -28,19 +26,13 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. unique device/node name tables #. Receive script arguments from config.lua
#. SPA ids to node names: name = id_name_table[device_id][node_id] #. ensure config.properties is not nil
#. create the underlying hidden ALSA node #. preprocess rules and create Interest objects
#. not suitable for loopback #. applies properties from config.rules when asked to
#: 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 amount of channels #. try to negotiate the max ammount 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
@ -50,366 +42,16 @@ msgstr "Podeli %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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. apply properties from config.rules
#. 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:438 #: src/scripts/monitors/alsa.lua:222
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:442 #: src/scripts/monitors/alsa.lua:224
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-2026 Free Software Foundation, Inc. # Copyright © 2008-2024 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, 2025, 2026. # Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021, 2022, 2024.
# #
# 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: 2025-12-23 17:57+0000\n" "POT-Creation-Date: 2024-03-11 15:33+0000\n"
"PO-Revision-Date: 2026-02-24 01:32+0100\n" "PO-Revision-Date: 2024-01-11 01:08+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.8\n" "X-Generator: Poedit 3.4.2\n"
#. WirePlumber #. WirePlumber
#. #.
@ -37,18 +37,9 @@ 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 amount of channels #. try to negotiate the max ammount 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
@ -58,24 +49,22 @@ msgstr "Dela %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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes #. add vm.type 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:438 #: src/scripts/monitors/alsa.lua:214
msgid "Loopback" msgid "Loopback"
msgstr "Loopback" msgstr "Loopback"
#: src/scripts/monitors/alsa.lua:440 #: src/scripts/monitors/alsa.lua:216
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Inbyggt ljud" msgstr "Inbyggt ljud"
#: src/scripts/monitors/alsa.lua:442 #: src/scripts/monitors/alsa.lua:218
msgid "Modem" msgid "Modem"
msgstr "Modem" msgstr "Modem"
@ -84,7 +73,6 @@ 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
@ -146,277 +134,3 @@ 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"

398
po/tr.po
View file

@ -1,25 +1,26 @@
# Turkish translation for WirePlumber. # Turkish translation for PipeWire.
# Copyright (C) 2025 WirePlumber's COPYRIGHT HOLDER # Copyright (C) 2014 PipeWire'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 PipeWire package.
# # Necdet Yücel <necdetyucel@gmail.com>, 2014.
# Sabri Ünal <yakushabb@gmail.com>, 2025. # Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
# Emin Tufan Çetin <etcetin@gmail.com>, 2025 # Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
# Oğuz Ersen <oguzersen@protonmail.com>, 2021.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WirePlumber master\n" "Project-Id-Version: PipeWire 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/new\n"
"POT-Creation-Date: 2025-11-09 04:07+0000\n" "POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2025-11-09 08:00+0300\n" "PO-Revision-Date: 2021-12-06 21:31+0300\n"
"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n" "Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n"
"Language-Team: Turkish <takim@gnome.org.tr>\n" "Language-Team: Turkish <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: Poedit 3.8\n" "X-Generator: Weblate 4.4.2\n"
#. WirePlumber #. WirePlumber
#. #.
@ -27,19 +28,13 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com> #. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. #.
#. SPDX-License-Identifier: MIT #. SPDX-License-Identifier: MIT
#. unique device/node name tables #. Receive script arguments from config.lua
#. SPA ids to node names: name = id_name_table[device_id][node_id] #. ensure config.properties is not nil
#. create the underlying hidden ALSA node #. preprocess rules and create Interest objects
#. not suitable for loopback #. applies properties from config.rules when asked to
#: 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 amount of channels #. try to negotiate the max ammount 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
@ -50,360 +45,15 @@ msgstr "Bölük %s"
#. 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
#. add cpu.vm.name for rule matching purposes #. apply properties from config.rules
#. 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:438 #: src/scripts/monitors/alsa.lua:222
msgid "Loopback"
msgstr "Geri Döngü"
#: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio" msgid "Built-in Audio"
msgstr "Yerleşik Ses" msgstr "Dahili Ses"
#: src/scripts/monitors/alsa.lua:442 #: src/scripts/monitors/alsa.lua:224
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-12-15 16:28+0000\n" "POT-Creation-Date: 2025-10-01 16:13+0000\n"
"PO-Revision-Date: 2025-12-16 10:10+0800\n" "PO-Revision-Date: 2025-10-02 07:57+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.* and alsa.* properties for rule matching purposes #. add api.alsa.card.* 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,7 +79,6 @@ 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
@ -274,18 +273,6 @@ 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"
@ -318,8 +305,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 device sink nodes in MONO" msgid "Configure all audio 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,9 +470,6 @@ 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
@ -488,43 +485,23 @@ wireplumber.components = [
## Client access configuration hooks ## Client access configuration hooks
{ {
name = client/select-access.lua, type = script/lua name = client/access-default.lua, type = script/lua
provides = script.client.select-access provides = script.client.access-default
} }
{ {
name = client/find-config-access.lua, type = script/lua name = client/access-portal.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/find-default-access.lua, type = script/lua name = client/access-snap.lua, type = script/lua
provides = script.client.access-default provides = script.client.access-snap
}
{
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
requires = [ script.client.select-access, wants = [ script.client.access-default,
script.client.access-config, script.client.access-portal,
script.client.access-default, script.client.access-snap ]
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
@ -650,17 +627,12 @@ 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
@ -676,11 +648,6 @@ 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
@ -734,8 +701,7 @@ 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.rescan-on-linkable, wants = [ hooks.linking.target.find-media-role,
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,
@ -751,21 +717,10 @@ 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
@ -876,12 +831,6 @@ 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 = {
@ -963,12 +912,6 @@ 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 = {
@ -991,7 +934,7 @@ wireplumber.settings.schema = {
} }
node.features.audio.mono = { node.features.audio.mono = {
name = "Mono" name = "Mono"
description = "Configure all audio device sink nodes in MONO" description = "Configure all audio nodes in MONO"
type = "bool" type = "bool"
default = false default = false
} }

View file

@ -1,80 +1,13 @@
## The WirePlumber access configuration ## The WirePlumber access configuration
access.permission-managers = [
## The list of access permission managers
## 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 = [ access.rules = [
## The list of access rules # The list of access rules
## This rule attaches the 'custom' permission manager to paplay clients. # The following are the default rules applied if none overrides them.
## Note: This is only used if there is no 'default_permissions' action.
# { # {
# matches = [ # matches = [
# { # {
# application.name = "paplay" # access = "flatpak"
# }
# ]
# 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"
# } # }
# ] # ]
@ -86,14 +19,10 @@ 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 = [
# { # {
# pipewire.access = "flatpak" # access = "flatpak"
# media.category = "!\"Manager\""
# } # }
# ] # ]
# actions = { # actions = {
@ -103,13 +32,10 @@ access.rules = [
# } # }
# } # }
## This rule grants read-exec-only permissions to clients with 'restricted'
## access.
## Note: WirePlumber already does this by default.
# { # {
# matches = [ # matches = [
# { # {
# pipewire.access = "restricted" # access = "restricted"
# } # }
# ] # ]
# actions = { # actions = {
@ -119,12 +45,10 @@ access.rules = [
# } # }
# } # }
## This rule grants all permissions to clients with 'unrestricted' access.
## Note: WirePlumber already does this by default.
# { # {
# matches = [ # matches = [
# { # {
# pipewire.access = "unrestricted" # access = "default"
# } # }
# ] # ]
# actions = { # actions = {

View file

@ -1,43 +0,0 @@
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,7 +150,6 @@ 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,25 +19,5 @@ 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

@ -0,0 +1,89 @@
-- 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

@ -0,0 +1,143 @@
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

@ -0,0 +1,87 @@
-- 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

@ -1,73 +0,0 @@
-- 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

@ -1,120 +0,0 @@
-- 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

@ -1,61 +0,0 @@
-- 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

@ -1,69 +0,0 @@
-- 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

@ -1,161 +0,0 @@
-- 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

@ -1,89 +0,0 @@
-- 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

@ -1,21 +0,0 @@
-- 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,9 +24,6 @@ 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,8 +18,6 @@ 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
@ -39,12 +37,6 @@ 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
@ -54,8 +46,6 @@ 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,14 +55,9 @@ 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
-- See if we have a per-device override { is_input and Settings.get_float ("device.routes.default-source-volume")
(is_input and tonumber(device.properties["device.routes.default-source-volume"])) or Settings.get_float ("device.routes.default-sink-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,7 +7,6 @@
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 {
@ -195,19 +194,17 @@ 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) and not hooks_registered then if mute_alsa or mute_bluez 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 ()
hooks_registered = true else
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,25 +28,43 @@
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
local PROFILE_RESTORE_TIMEOUT_MSEC = 2000 state = nil
local PROFILE_SWITCH_TIMEOUT_MSEC = 500 headset_profiles = nil
local state = nil local profile_restore_timeout_msec = 2000
local headset_profiles = {} local profile_switch_timeout_msec = 500
local non_headset_profiles = {}
local capture_stream_links = {} local INVALID = -1
local restore_timeout_source = {} local restore_timeout_source = {}
local switch_timeout_source = {} local switch_timeout_source = {}
function saveHeadsetProfile (device, profile_name, persistent) local last_profiles = {}
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
if state ~= nil and persistent then state:save_after_timeout (headset_profiles)
state:save_after_timeout (headset_profiles)
end
end end
function getSavedHeadsetProfile (device) function getSavedHeadsetProfile (device)
@ -54,38 +72,87 @@ function getSavedHeadsetProfile (device)
return headset_profiles [key] return headset_profiles [key]
end end
function saveNonHeadsetProfile (device, profile_name) function saveLastProfile (device, profile_name)
non_headset_profiles [device.properties ["device.name"]] = profile_name last_profiles [device.properties ["device.name"]] = profile_name
end end
function getSavedNonHeadsetProfile (device) function getSavedLastProfile (device)
return non_headset_profiles [device.properties ["device.name"]] return last_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 profile ~= nil then if not profile then
if (index ~= nil and profile.index == index) or goto skip_enum_profile
(name ~= nil and profile.name == name) then
return profile
end
end end
log:debug ("Profile name: " .. profile.name .. ", priority: "
.. 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 end
return nil return INVALID, INVALID, nil
end end
function getCurrentProfile (device) function getCurrentProfile (device)
for p in device:iterate_params ("Profile") do for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile") local profile = cutils.parseParam (p, "Profile")
if profile then if profile then
return profile return profile.name
end end
end end
return nil return nil
end end
function highestPrioProfileWithInputRoute (device)
local profile_priority = INVALID
local profile_index = INVALID
local profile_name = nil
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
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
return profile_priority, profile_index, profile_name
end
function hasProfileInputRoute (device, profile_index) function hasProfileInputRoute (device, profile_index)
for p in device:iterate_params ("EnumRoute") do for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute") local route = cutils.parseParam (p, "EnumRoute")
@ -100,51 +167,6 @@ 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 {
@ -155,43 +177,49 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
return return
end end
-- Do not switch if the current profile is already a headset profile local cur_profile_name = getCurrentProfile (device)
local cur_profile = getCurrentProfile (device) local priority, index, name = findProfile (device, nil, cur_profile_name)
if cur_profile ~= nil and isHeadsetProfile (device, cur_profile) then if hasProfileInputRoute (device, index) then
log:info (device, log:info ("Current profile has input route, not switching")
"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
-- Get saved headset profile if any, otherwise find the highest priority one if isSwitchedToHeadsetProfile (device) then
local profile = nil log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
local profile_name = getSavedHeadsetProfile (device) return
if profile_name ~= nil then end
profile = findProfile (device, nil, profile_name)
if profile ~= nil and not isHeadsetProfile (device, profile) then local saved_headset_profile = getSavedHeadsetProfile (device)
saveHeadsetProfile (device, nil, false)
profile = nil index = INVALID
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 profile == nil then if index == INVALID then
profile = highestPrioHeadsetProfile (device) priority, index, name = highestPrioProfileWithInputRoute (device)
end end
-- Switch if headset profile was found if index ~= INVALID then
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 = profile.index, index = index
save = false
} }
log:info (device, "Switching profile from: " .. cur_profile.name
.. " to: " .. profile.name) -- store the current profile (needed when restoring)
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 ("Could not find valid headset profile, not switching") log:warning ("Got invalid index when switching profile")
end end
end end
@ -205,139 +233,142 @@ function restoreProfile (dev_id, device_om)
return return
end end
-- Do not restore if the current profile is already a non-headset profile if not isSwitchedToHeadsetProfile (device) then
local cur_profile = getCurrentProfile (device) log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
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
-- Get saved non-headset profile if any, otherwise find the highest priority one local profile_name = getSavedLastProfile (device)
local profile = nil local cur_profile_name = getCurrentProfile (device)
local profile_name = getSavedNonHeadsetProfile (device) local priority, index, name
if profile_name ~= nil then
profile = findProfile (device, nil, profile_name) if cur_profile_name then
if profile ~= nil and isHeadsetProfile (device, profile) then priority, index, name = findProfile (device, nil, cur_profile_name)
saveNonHeadsetProfile (device, nil)
profile = nil if index ~= INVALID and hasProfileInputRoute (device, index) then
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
-- Restore if non-headset profile was found if profile_name then
if profile ~= nil then priority, index, name = findProfile (device, nil, profile_name)
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile", if index ~= INVALID then
index = profile.index, local pod = Pod.Object {
save = false "Spa:Pod:Object:Param:Profile", "Profile",
} index = index
log:info (device, "Restoring profile from: " .. cur_profile.name }
.. " to: " .. profile.name)
device:set_params ("Profile", pod) -- clear last profile as we will restore it now
else saveLastProfile (device, nil)
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 (source, dev_id) function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
-- 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
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] = 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 (source, dev_id) function triggerRestoreProfile (dev_id, device_om)
-- 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
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] = 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
function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups) -- We consider a Stream of interest if it is linked to a bluetooth loopback
local stream_id = stream["bound-id"] -- source filter
function checkStreamStatus (stream, node_om, visited_link_groups)
-- Make sure the node is linked -- check if the stream is linked to a bluetooth loopback source
local link = link_om:lookup { local stream_id = tonumber(stream["bound-id"])
Constraint { "link.input.node", "=", stream_id, type = "pw-global"} local peer_id = lutils.getNodePeerId (stream_id)
} if peer_id ~= nil then
if link == nil then local bt_node = node_om:lookup {
return nil Constraint { "bound-id", "=", peer_id, type = "gobject" },
end Constraint { "bluez5.loopback", "=", "true", type = "pw" }
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 if bt_node ~= nil then
local filter_link_group = filter_main_node.properties ["node.link-group"] local dev_id = bt_node.properties["device.id"]
if visited_link_groups == nil then if dev_id ~= nil then
visited_link_groups = {} -- 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
end end
if visited_link_groups [filter_link_group] then else
return nil -- Check if it is linked to a filter main node, and recursively advance if so
else local filter_main_node = node_om:lookup {
visited_link_groups [filter_link_group] = true Constraint { "bound-id", "=", peer_id, type = "gobject" },
end Constraint { "node.link-group", "+", type = "pw" }
for filter_stream_node in node_om:iterate { }
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" }, if filter_main_node ~= nil then
Constraint { "stream.monitor", "!", "true", type = "pw" }, -- Now check all stream nodes for this filter
Constraint { "bluez5.loopback", "!", "true", type = "pw" }, local filter_link_group = filter_main_node.properties ["node.link-group"]
Constraint { "node.link-group", "=", filter_link_group, type = "pw" } if visited_link_groups == nil then
} do visited_link_groups = {}
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_node, node_om, link_om, visited_link_groups) end
if bt_node ~= nil then if visited_link_groups [filter_link_group] then
return bt_node return nil
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
@ -346,118 +377,60 @@ function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om
return nil return nil
end end
function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) function handleStream (stream, node_om, device_om)
local bt_node_id = bt_node["bound-id"] if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
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
local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om) handleStream (stream, node_om, device_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
local switch_profile_hook = AsyncEventHook { SimpleEventHook {
name = "switch-profile@autoswitch-bluetooth-profile", name = "node-removed@autoswitch-bluetooth-profile",
interests = { interests = {
EventInterest { EventInterest {
Constraint { "event.type", "=", "autoswitch-bluez-headset-profile" }, Constraint { "event.type", "=", "node-removed" },
}, 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 node_om = source:call ("get-object-manager", "node") local device_om = source:call ("get-object-manager", "device")
local link_om = source:call ("get-object-manager", "link")
-- Evaluate all bluetooth loopback source nodes, and switch to headset local dev_id = active_streams[stream.id]
-- profile only if the node is running and linked to a stream that is not a active_streams[stream.id] = nil
-- monitor, otherwise just restore the profile. previous_streams[stream.id] = nil
-- if dev_id ~= nil then
-- If the bluetooth node is linked to a stream that is a monitor, its state triggerRestoreProfile (dev_id, device_om)
-- 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 ()
local link_added_hook = SimpleEventHook { SimpleEventHook {
name = "link-added@autoswitch-bluetooth-profile", name = "link-added@autoswitch-bluetooth-profile",
interests = { interests = {
EventInterest { EventInterest {
@ -465,164 +438,46 @@ local link_added_hook = 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 link = event:get_subject () local device_om = source:call ("get-object-manager", "device")
local in_stream_id = link.properties["link.input.node"] local link_props = link.properties
-- Only evaluate bluetooth profiles if a capture stream was linked for stream in node_om:iterate {
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" }
Constraint { "bound-id", "=", in_stream_id, type = "gobject" }, } do
} local in_id = tonumber(link_props["link.input.node"])
if stream ~= nil then local stream_id = tonumber(stream["bound-id"])
capture_stream_links [link.id] = true if in_id == stream_id then
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil) handleStream (stream, node_om, device_om)
end
end end
end end
} }:register ()
local link_removed_hook = SimpleEventHook { SimpleEventHook {
name = "link-removed@autoswitch-bluetooth-profile", name = "bluez-device-added@autoswitch-bluetooth-profile",
interests = { interests = {
EventInterest { EventInterest {
Constraint { "event.type", "=", "link-removed" }, Constraint { "event.type", "=", "device-added" },
},
},
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")
-- Always save the current profile when it changes -- Devices are unswitched initially
local cur_profile = getCurrentProfile (device) saveLastProfile (device, nil)
if cur_profile ~= nil then
if isHeadsetProfile (device, cur_profile) then -- Handle all streams when BT device is added
log:info (device, "Saving headset profile " .. cur_profile.name) handleAllStreams (node_om, device_om)
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 Properties() local selected_routes = event:get_data ("selected-routes") or {}
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,37 +13,6 @@ 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",
@ -62,22 +31,19 @@ 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")
@ -95,8 +61,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, string.format ( log:info (device, "Profiles listed in 'device.profile.priority.rules'"
"Could not find preferred profile for device '%s'", device_name)) .. " do not match the available ones of device: " .. device_name)
end end
end end

View file

@ -26,11 +26,3 @@ 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,12 +34,9 @@ find_stored_profile_hook = SimpleEventHook {
end end
local device = event:get_subject () local device = event:get_subject ()
local device_props = device.properties local dev_name = device.properties["device.name"]
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:warning (device, "invalid device.name") log:critical (device, "invalid device.name")
return return
end end
@ -48,8 +45,7 @@ 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" and if profile.name == profile_name and profile.available ~= "no" then
(not dont_restore_off_profile or profile.index ~= 0) then
selected_profile = profile selected_profile = profile
break break
end end
@ -91,7 +87,7 @@ function updateStoredProfile (device, profile)
local index = nil local index = nil
if not dev_name then if not dev_name then
log:warning (device, "invalid device.name") log:critical (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 Properties() local selected_routes = event:get_data ("selected-routes") or {}
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 Properties() local selected_routes = event:get_data ("selected-routes") or {}
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 selected_routes:get_count () == 0 then if next (selected_routes) == nil 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,126 +159,130 @@ store_or_restore_routes_hook = AsyncEventHook {
}, },
steps = { steps = {
start = { start = {
next = "none", next = "evaluate",
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
device:enum_params ("EnumRoute", function (enum_route_it, e) local device = event:get_subject ()
local selected_routes = {} device:enum_params ("EnumRoute", function (_, e)
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));
return else
end
-- Make sure the device is still valid
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
transition:advance () transition:advance ()
return
end 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 enum_route_it:iterate() do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
-- find cached route information
local route_info = devinfo.find_route_info (dev_info, route, true)
if not route_info then
goto skip_enum_route
end
-- update properties
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos [route.index] = route_info
::skip_enum_route::
end
-- update route_infos with new prev_active, active and save changes
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params ("Route") do
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = devinfo.find_route_info (dev_info, route, false)
if not route_info then
goto skip_route
end
-- update route_info state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- 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
selected_routes [tostring (route.device)] =
Json.Object { index = route_info.index }:to_string ()
push_select_routes = true
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
-- save selected routes for the active profile
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
saveProfileRoutes (dev_info, profile.name)
end
-- push a select-routes event to re-apply the routes with new properties
if push_select_routes then
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)
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
-- find cached route information
local route_info = devinfo.find_route_info (dev_info, route, true)
if not route_info then
goto skip_enum_route
end
-- update properties
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos [route.index] = route_info
::skip_enum_route::
end
-- update route_infos with new prev_active, active and save changes
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params ("Route") do
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = devinfo.find_route_info (dev_info, route, false)
if not route_info then
goto skip_route
end
-- update route_info state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- 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
selected_routes [tostring (route.device)] =
Json.Object { index = route_info.index }:to_string ()
push_select_routes = true
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
-- save selected routes for the active profile
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
saveProfileRoutes (dev_info, profile.name)
end
-- push a select-routes event to re-apply the routes with new properties
if push_select_routes then
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
} }
} }
} }

View file

@ -91,24 +91,4 @@ 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 linking event and to schedule a "rescan-for-linking" event, which is the lowest priority 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,8 +13,7 @@ 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

@ -1,81 +0,0 @@
-- 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

@ -1,25 +0,0 @@
-- 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:get_property ("node.link-group") local link_group = node.properties["node.link-group"]
if link_group == nil then if link_group == nil then
return true return true
end end
@ -43,34 +43,36 @@ 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 si:get_property ("item.node.type") ~= "stream" and if not si_props or (si_props ["item.node.type"] ~= "stream"
not handle_nonstreams then and not handle_nonstreams) then
return false return false, si_props
end end
-- check filters -- check filters
if not checkFilter (si, om, handle_nonstreams) then if not checkFilter (si, om, handle_nonstreams) then
return false return false, si_props
end end
return true return true, si_props
end end
function unhandleLinkable (si, om) function unhandleLinkable (si, om)
if not checkLinkable (si, om, true) then local si_id = si.id
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 silink_props = silink.properties local out_id = tonumber (silink.properties ["out.item.id"])
local out_id = silink_props:get_int ("out.item.id") local in_id = tonumber (silink.properties ["in.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)
@ -82,7 +84,7 @@ function unhandleLinkable (si, om)
out_flags.peer_id = nil out_flags.peer_id = nil
end end
if silink_props:get_boolean ("is.role.policy.link") then if cutils.parseBool (silink.properties["is.role.policy.link"]) then
lutils.clearPriorityMediaRoleLink(silink) lutils.clearPriorityMediaRoleLink(silink)
end end
@ -111,67 +113,17 @@ 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
if not checkLinkable (si, om) then local valid, si_props = checkLinkable (si, om)
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 = si_props:get_boolean ("node.autoconnect") local autoconnect = cutils.parseBool (si_props ["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
@ -203,7 +155,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:get_property ("node.link-group") local link_group = node.properties["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
@ -218,6 +170,11 @@ 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" },
@ -278,6 +235,7 @@ 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.* and alsa.* properties for rule matching purposes -- add api.alsa.card.* 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%..*") or k:find("^alsa%..*") then if k:find("^api%.alsa%.card%..*") then
properties[k] = v properties[k] = v
end end
end end
@ -494,11 +494,6 @@ 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 Properties()) do for k, v in pairs(config.properties or {}) do
monitor_props[k] = v monitor_props[k] = v
end end

View file

@ -8,7 +8,6 @@
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")
@ -21,10 +20,13 @@ 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" },
} }
} }
@ -211,15 +213,13 @@ function createSetNode(parent, id, type, factory, properties)
) )
end end
local combine_props = properties:parse () properties["node.virtual"] = false
combine_props["node.virtual"] = false properties["device.api"] = "bluez5"
combine_props["device.api"] = "bluez5" properties["api.bluez5.set.members"] = nil
combine_props["api.bluez5.set.members"] = nil properties["api.bluez5.set.channels"] = nil
combine_props["api.bluez5.set.channels"] = nil properties["api.bluez5.set.leader"] = true
combine_props["api.bluez5.set.leader"] = true properties["audio.position"] = Json.Array (channels)
combine_props["audio.position"] = Json.Array (channels) args["combine.props"] = Json.Object (properties)
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,12 +230,6 @@ 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"]
@ -264,11 +258,25 @@ 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,
properties["api.bluez5.address"], dev_props["device.name"], id) -- hide the source node because we use the loopback source instead
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
@ -296,16 +304,10 @@ 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
-- Set sink/source specific properties if factory == "api.bluez5.sco.source" then
if factory == "api.bluez5.sco.source" or sco_source_node_properties[parent_spa_id] = properties
(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)
@ -315,9 +317,15 @@ 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
@ -391,7 +399,7 @@ function createDevice(parent, id, type, factory, properties)
end end
function removeDevice(parent, id) function removeDevice(parent, id)
log:debug("Remove device: " .. tostring (id)) sco_source_node_properties[id] = nil
end end
function createMonitor() function createMonitor()
@ -409,18 +417,7 @@ function createMonitor()
return monitor return monitor
end end
function CreateDeviceLoopbackSource (dev_props, dev_id) function CreateDeviceLoopbackSource (dev_name, dec_desc, 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),
@ -435,7 +432,6 @@ function CreateDeviceLoopbackSource (dev_props, 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),
@ -446,8 +442,15 @@ function CreateDeviceLoopbackSource (dev_props, 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(), {})
@ -458,6 +461,11 @@ 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)
@ -465,48 +473,51 @@ function checkProfiles (dev)
return return
end end
-- Check if the device supports headset profile -- Ignore devices that don't support both A2DP sink and HSP/HFP profiles
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 ("headset") then if profile.name:find ("a2dp") and profile.name:find ("sink") 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
if has_headset_profile then -- Create the loopback device if never created before
-- Always create the source loopback device if autoswitch is enabled. local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
-- Otherwise, only create the source loopback device if the current profile if loopback == nil then
-- is headset, and destroy the source loopback deivce if the current profile local dev_name = props["api.bluez5.address"] or props["device.name"]
-- is A2DP. local dec_desc = props["device.description"] or props["device.name"]
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then or props["device.nick"] or props["device.alias"] or "bluetooth-device"
-- 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
if is_current_profile_headset then log:info("create SCO loopback node: " .. dev_name)
-- Create source loopback
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID) -- sanitize description, replace ':' with ' '
if source_loopback == nil and has_headset_profile then dec_desc = dec_desc:gsub("(:)", " ")
source_loopback = CreateDeviceLoopbackSource (props, device_id) loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback) spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback)
end
else -- recreate any sco source node
-- Destroy source loopback local properties = sco_source_node_properties[device_spa_id]
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, nil) if properties ~= nil then
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
@ -515,12 +526,16 @@ 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)
@ -551,15 +566,3 @@ 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 = LocalNode ("spa-node-factory", properties) local node = Node ("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.info ("Creating " .. direction .. " loopback for audio group " .. group .. Log.warning ("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,7 +16,6 @@ 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
@ -41,7 +40,6 @@ 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

@ -1,53 +0,0 @@
-- 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

@ -1,94 +0,0 @@
-- 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,18 +348,8 @@ 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
local t = type(pod_parsed.properties.channels) channels = pod_parsed.properties.channels
if t == "number" then break
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,38 +1,30 @@
if get_option('systemd-system-service') or get_option('systemd-user-service') if systemd.found()
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
if systemd_system_unit_dir != '' subdir('system')
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
if systemd_user_unit_dir != '' subdir('user')
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 mincore SystemCallFilter=@system-service
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 mincore SystemCallFilter=@system-service
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 mincore SystemCallFilter=@system-service
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 mincore SystemCallFilter=@system-service
Type=simple Type=simple
ExecStart=@WP_BINARY@ -p %i ExecStart=@WP_BINARY@ -p %i
Restart=on-failure Restart=on-failure

View file

@ -4,11 +4,6 @@ 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

@ -1,41 +0,0 @@
_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,31 +35,12 @@ 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;
@ -562,159 +543,6 @@ 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
@ -1042,8 +870,7 @@ 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_str_has_prefix (media_class, DEFAULT_NODE_MEDIA_CLASSES[i]) && if (!g_strcmp0 (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);
@ -1135,7 +962,6 @@ do_set_volume (WpCtl * self, WpPipewireObject *proxy)
GVariant *variant = NULL; GVariant *variant = NULL;
gboolean res = FALSE; gboolean res = FALSE;
gdouble curr_volume = 1.0; gdouble curr_volume = 1.0;
gdouble new_volume = cmdline.set_volume.volume;
guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy)); guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy));
if (cmdline.set_volume.type == 's') { if (cmdline.set_volume.type == 's') {
@ -1148,19 +974,19 @@ do_set_volume (WpCtl * self, WpPipewireObject *proxy)
g_variant_lookup (variant, "volume", "d", &curr_volume); g_variant_lookup (variant, "volume", "d", &curr_volume);
g_clear_pointer (&variant, g_variant_unref); g_clear_pointer (&variant, g_variant_unref);
new_volume += curr_volume; cmdline.set_volume.volume = (cmdline.set_volume.volume + curr_volume);
} }
if (new_volume < 0) { if (cmdline.set_volume.volume < 0) {
new_volume = 0.0; cmdline.set_volume.volume = 0.0;
} }
if (cmdline.set_volume.limit > 0) { if (cmdline.set_volume.limit > 0) {
if (new_volume > cmdline.set_volume.limit) { if (cmdline.set_volume.volume > cmdline.set_volume.limit) {
new_volume = cmdline.set_volume.limit; cmdline.set_volume.volume = cmdline.set_volume.limit;
} }
} }
g_variant_builder_add (&b, "{sv}", "volume", g_variant_builder_add (&b, "{sv}", "volume",
g_variant_new_double (new_volume)); g_variant_new_double (cmdline.set_volume.volume));
variant = g_variant_builder_end (&b); variant = g_variant_builder_end (&b);
g_signal_emit_by_name (mixer_api, "set-volume", id, variant, &res); g_signal_emit_by_name (mixer_api, "set-volume", id, variant, &res);
@ -1887,16 +1713,6 @@ 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",
@ -2085,8 +1901,7 @@ 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, wp_properties_new (PW_KEY_REMOTE_NAME, ctl.core = wp_core_new (NULL, NULL, NULL);
("[" 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 */

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