mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-08 11:38:04 +02:00
Compare commits
No commits in common. "master" and "0.5.13" have entirely different histories.
63 changed files with 754 additions and 4597 deletions
77
NEWS.rst
77
NEWS.rst
|
|
@ -1,77 +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
|
WirePlumber 0.5.13
|
||||||
..................
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Additions & Enhancements:
|
Additions & Enhancements:
|
||||||
|
|
||||||
|
|
@ -137,6 +65,9 @@ Fixes:
|
||||||
|
|
||||||
- Simplified event-hook interest matching for better performance (!758)
|
- Simplified event-hook interest matching for better performance (!758)
|
||||||
|
|
||||||
|
Past releases
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
WirePlumber 0.5.12
|
WirePlumber 0.5.12
|
||||||
..................
|
..................
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
|
|
||||||
/* get the highest priority event */
|
/* get the highest priority event */
|
||||||
GList *levent = g_list_first (d->events);
|
GList *levent = g_list_first (d->events);
|
||||||
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 +300,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;
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
|
||||||
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
|
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
|
||||||
WpInterestEventHookPrivate *priv =
|
WpInterestEventHookPrivate *priv =
|
||||||
wp_interest_event_hook_get_instance_private (self);
|
wp_interest_event_hook_get_instance_private (self);
|
||||||
g_autoptr (GPtrArray) res = g_ptr_array_new_with_free_func (g_free);
|
GPtrArray *res = g_ptr_array_new_with_free_func (g_free);
|
||||||
guint i;
|
guint i;
|
||||||
|
|
||||||
for (i = 0; i < priv->interests->len; i++) {
|
for (i = 0; i < priv->interests->len; i++) {
|
||||||
|
|
@ -393,7 +393,7 @@ wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return g_steal_pointer (&res);
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
37
meson.build
37
meson.build
|
|
@ -1,5 +1,5 @@
|
||||||
project('wireplumber', ['c'],
|
project('wireplumber', ['c'],
|
||||||
version : '0.5.14',
|
version : '0.5.13',
|
||||||
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
|
||||||
|
|
@ -182,9 +185,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')
|
||||||
|
|
|
||||||
|
|
@ -1388,22 +1388,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 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1703,7 +1691,7 @@ 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);
|
||||||
|
|
||||||
|
|
@ -2629,132 +2617,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
|
||||||
|
|
@ -3282,8 +3144,6 @@ wp_lua_scripting_api_init (lua_State *L)
|
||||||
NULL, iterator_funcs);
|
NULL, iterator_funcs);
|
||||||
wplua_register_type_methods (L, WP_TYPE_PROPERTIES,
|
wplua_register_type_methods (L, WP_TYPE_PROPERTIES,
|
||||||
properties_new, properties_funcs);
|
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)) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -241,7 +218,6 @@ SANDBOX_EXPORT = {
|
||||||
JsonUtils = JsonUtils,
|
JsonUtils = JsonUtils,
|
||||||
ProcUtils = ProcUtils,
|
ProcUtils = ProcUtils,
|
||||||
Properties = WpProperties_new,
|
Properties = WpProperties_new,
|
||||||
PermissionManager = WpPermissionManager_new,
|
|
||||||
SimpleEventHook = WpSimpleEventHook_new,
|
SimpleEventHook = WpSimpleEventHook_new,
|
||||||
AsyncEventHook = WpAsyncEventHook_new,
|
AsyncEventHook = WpAsyncEventHook_new,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,6 @@ 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"))
|
||||||
|
|
@ -211,8 +209,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
308
po/bg.po
|
|
@ -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 "Цел за възстановяване"
|
|
||||||
|
|
|
||||||
10
po/conf.pot
10
po/conf.pot
|
|
@ -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"
|
||||||
|
|
|
||||||
285
po/ka.po
285
po/ka.po
|
|
@ -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
399
po/kk.po
|
|
@ -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 "Мақсатты қалпына келтіру"
|
|
||||||
|
|
|
||||||
392
po/sr.po
392
po/sr.po
|
|
@ -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 "Поврати циљ"
|
|
||||||
|
|
|
||||||
394
po/sr@latin.po
394
po/sr@latin.po
|
|
@ -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
308
po/sv.po
|
|
@ -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"
|
|
||||||
|
|
|
||||||
21
po/zh_CN.po
21
po/zh_CN.po
|
|
@ -6,15 +6,15 @@
|
||||||
# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
|
# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
|
||||||
# Frank Hill <hxf.prc@gmail.com>, 2015.
|
# Frank Hill <hxf.prc@gmail.com>, 2015.
|
||||||
# Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015.
|
# Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015.
|
||||||
# lumingzh <lumingzh@qq.com>, 2024-2026.
|
# lumingzh <lumingzh@qq.com>, 2024-2025.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
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: 2026-04-30 11:35+0000\n"
|
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
|
||||||
"PO-Revision-Date: 2026-05-07 16:56+0800\n"
|
"PO-Revision-Date: 2025-12-16 10:10+0800\n"
|
||||||
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
|
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
|
||||||
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
|
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
|
||||||
"Language: zh_CN\n"
|
"Language: zh_CN\n"
|
||||||
|
|
@ -22,7 +22,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"
|
||||||
"X-Launchpad-Export-Date: 2016-03-22 13:23+0000\n"
|
"X-Launchpad-Export-Date: 2016-03-22 13:23+0000\n"
|
||||||
"X-Generator: Gtranslator 50.0\n"
|
"X-Generator: Gtranslator 49.0\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
#. WirePlumber
|
#. WirePlumber
|
||||||
|
|
@ -154,19 +154,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 ""
|
|
||||||
"自动选择配置文件时首选更好质量或更低延迟(仅接受“quality”或“latency”设定值)"
|
|
||||||
|
|
||||||
#. /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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -676,11 +653,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 +706,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,
|
||||||
|
|
@ -876,12 +847,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 = {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
89
src/scripts/client/access-default.lua
Normal file
89
src/scripts/client/access-default.lua
Normal 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()
|
||||||
143
src/scripts/client/access-portal.lua
Normal file
143
src/scripts/client/access-portal.lua
Normal 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()
|
||||||
87
src/scripts/client/access-snap.lua
Normal file
87
src/scripts/client/access-snap.lua
Normal 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()
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,42 @@ function getCurrentProfile (device)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function highestPrioProfileWithInputRoute (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 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 highestPrioProfileWithoutInputRoute (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 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 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 +136,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 {
|
||||||
|
|
@ -157,13 +148,11 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
|
|
||||||
-- Do not switch if the current profile is already a headset profile
|
-- Do not switch if the current profile is already a headset profile
|
||||||
local cur_profile = getCurrentProfile (device)
|
local cur_profile = getCurrentProfile (device)
|
||||||
if cur_profile ~= nil and isHeadsetProfile (device, cur_profile) then
|
if cur_profile ~= nil and
|
||||||
|
hasProfileInputRoute (device, cur_profile.index) then
|
||||||
log:info (device,
|
log:info (device,
|
||||||
"Current profile is already a headset profile, no need to switch")
|
"Current profile is already a headset profile, no need to switch")
|
||||||
return
|
return
|
||||||
elseif cur_profile == nil then
|
|
||||||
log:info (device, "Could not get current profile, not switching")
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get saved headset profile if any, otherwise find the highest priority one
|
-- Get saved headset profile if any, otherwise find the highest priority one
|
||||||
|
|
@ -171,13 +160,12 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
local profile_name = getSavedHeadsetProfile (device)
|
local profile_name = getSavedHeadsetProfile (device)
|
||||||
if profile_name ~= nil then
|
if profile_name ~= nil then
|
||||||
profile = findProfile (device, nil, profile_name)
|
profile = findProfile (device, nil, profile_name)
|
||||||
if profile ~= nil and not isHeadsetProfile (device, profile) then
|
if profile ~= nil and not hasProfileInputRoute (device, profile.index) then
|
||||||
saveHeadsetProfile (device, nil, false)
|
saveHeadsetProfile (device, nil, false)
|
||||||
profile = nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if profile == nil then
|
if profile == nil then
|
||||||
profile = highestPrioHeadsetProfile (device)
|
profile = highestPrioProfileWithInputRoute (device)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Switch if headset profile was found
|
-- Switch if headset profile was found
|
||||||
|
|
@ -207,13 +195,11 @@ function restoreProfile (dev_id, device_om)
|
||||||
|
|
||||||
-- Do not restore if the current profile is already a non-headset profile
|
-- Do not restore if the current profile is already a non-headset profile
|
||||||
local cur_profile = getCurrentProfile (device)
|
local cur_profile = getCurrentProfile (device)
|
||||||
if cur_profile ~= nil and not isHeadsetProfile (device, cur_profile) then
|
if cur_profile ~= nil and
|
||||||
|
not hasProfileInputRoute (device, cur_profile.index) then
|
||||||
log:info (device,
|
log:info (device,
|
||||||
"Current profile is already a non-headset profile, no need to restore")
|
"Current profile is already a non-headset profile, no need to restore")
|
||||||
return
|
return
|
||||||
elseif cur_profile == nil then
|
|
||||||
log:info (device, "Could not get current profile, not switching")
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get saved non-headset profile if any, otherwise find the highest priority one
|
-- Get saved non-headset profile if any, otherwise find the highest priority one
|
||||||
|
|
@ -221,13 +207,12 @@ function restoreProfile (dev_id, device_om)
|
||||||
local profile_name = getSavedNonHeadsetProfile (device)
|
local profile_name = getSavedNonHeadsetProfile (device)
|
||||||
if profile_name ~= nil then
|
if profile_name ~= nil then
|
||||||
profile = findProfile (device, nil, profile_name)
|
profile = findProfile (device, nil, profile_name)
|
||||||
if profile ~= nil and isHeadsetProfile (device, profile) then
|
if profile ~= nil and hasProfileInputRoute (device, profile.index) then
|
||||||
saveNonHeadsetProfile (device, nil)
|
saveNonHeadsetProfile (device, nil)
|
||||||
profile = nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if profile == nil then
|
if profile == nil then
|
||||||
profile = highestPrioNonHeadsetProfile (device)
|
profile = highestPrioProfileWithoutInputRoute (device)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Restore if non-headset profile was found
|
-- Restore if non-headset profile was found
|
||||||
|
|
@ -245,7 +230,7 @@ function restoreProfile (dev_id, device_om)
|
||||||
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 ()
|
||||||
|
|
@ -262,14 +247,11 @@ function triggerSwitchDeviceToHeadsetProfile (source, dev_id)
|
||||||
log:info ("Triggering profile switch on device " .. tostring (dev_id))
|
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)
|
||||||
-- 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 ()
|
||||||
|
|
@ -286,10 +268,7 @@ function triggerRestoreProfile (source, dev_id)
|
||||||
log:info ("Triggering profile restore on device " .. tostring (dev_id))
|
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
|
||||||
|
|
||||||
|
|
@ -335,7 +314,8 @@ function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om
|
||||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||||
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
||||||
} do
|
} do
|
||||||
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_node, node_om, link_om, visited_link_groups)
|
local filter_stream_id = filter_stream_node["bound-id"]
|
||||||
|
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_id, node_om, link_om, visited_link_groups)
|
||||||
if bt_node ~= nil then
|
if bt_node ~= nil then
|
||||||
return bt_node
|
return bt_node
|
||||||
end
|
end
|
||||||
|
|
@ -365,60 +345,6 @@ function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local switch_profile_hook = AsyncEventHook {
|
|
||||||
name = "switch-profile@autoswitch-bluetooth-profile",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "autoswitch-bluez-headset-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")
|
|
||||||
|
|
||||||
-- 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 {
|
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
|
||||||
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
|
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
|
||||||
interests = {
|
interests = {
|
||||||
|
|
@ -429,6 +355,7 @@ local evaluate_bluetooth_profiles_hook = SimpleEventHook {
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
local source = event:get_source ()
|
local source = event:get_source ()
|
||||||
local node_om = source:call ("get-object-manager", "node")
|
local node_om = source:call ("get-object-manager", "node")
|
||||||
|
local device_om = source:call ("get-object-manager", "device")
|
||||||
local link_om = source:call ("get-object-manager", "link")
|
local link_om = source:call ("get-object-manager", "link")
|
||||||
|
|
||||||
-- Evaluate all bluetooth loopback source nodes, and switch to headset
|
-- Evaluate all bluetooth loopback source nodes, and switch to headset
|
||||||
|
|
@ -449,9 +376,9 @@ local evaluate_bluetooth_profiles_hook = SimpleEventHook {
|
||||||
|
|
||||||
if bt_node_state == "running" and
|
if bt_node_state == "running" and
|
||||||
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
|
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
|
||||||
triggerSwitchDeviceToHeadsetProfile (source, bt_dev_id)
|
triggerSwitchDeviceToHeadsetProfile (bt_dev_id, device_om)
|
||||||
else
|
else
|
||||||
triggerRestoreProfile (source, bt_dev_id)
|
triggerRestoreProfile (bt_dev_id, device_om)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -516,17 +443,6 @@ local state_changed_hook = SimpleEventHook {
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
local source = event:get_source ()
|
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)
|
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
@ -548,7 +464,7 @@ local node_added_hook = SimpleEventHook {
|
||||||
}
|
}
|
||||||
|
|
||||||
local device_profile_changed_hook = SimpleEventHook {
|
local device_profile_changed_hook = SimpleEventHook {
|
||||||
name = "bluez-profile-changed@autoswitch-bluetooth-profile",
|
name = "device/store-user-selected-profile",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "device-params-changed" },
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
|
@ -562,7 +478,7 @@ local device_profile_changed_hook = SimpleEventHook {
|
||||||
-- Always save the current profile when it changes
|
-- Always save the current profile when it changes
|
||||||
local cur_profile = getCurrentProfile (device)
|
local cur_profile = getCurrentProfile (device)
|
||||||
if cur_profile ~= nil then
|
if cur_profile ~= nil then
|
||||||
if isHeadsetProfile (device, cur_profile) then
|
if hasProfileInputRoute (device, cur_profile.index) then
|
||||||
log:info (device, "Saving headset profile " .. cur_profile.name)
|
log:info (device, "Saving headset profile " .. cur_profile.name)
|
||||||
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
|
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
|
||||||
else
|
else
|
||||||
|
|
@ -592,8 +508,6 @@ function evaluateAutoswitch ()
|
||||||
capture_stream_links = {}
|
capture_stream_links = {}
|
||||||
restore_timeout_source = {}
|
restore_timeout_source = {}
|
||||||
switch_timeout_source = {}
|
switch_timeout_source = {}
|
||||||
switch_profile_hook:register ()
|
|
||||||
restore_profile_hook:register ()
|
|
||||||
evaluate_bluetooth_profiles_hook:register ()
|
evaluate_bluetooth_profiles_hook:register ()
|
||||||
link_added_hook:register ()
|
link_added_hook:register ()
|
||||||
link_removed_hook:register ()
|
link_removed_hook:register ()
|
||||||
|
|
@ -605,8 +519,6 @@ function evaluateAutoswitch ()
|
||||||
capture_stream_links = nil
|
capture_stream_links = nil
|
||||||
restore_timeout_source = nil
|
restore_timeout_source = nil
|
||||||
switch_timeout_source = nil
|
switch_timeout_source = nil
|
||||||
switch_profile_hook:remove ()
|
|
||||||
restore_profile_hook:remove ()
|
|
||||||
evaluate_bluetooth_profiles_hook:remove ()
|
evaluate_bluetooth_profiles_hook:remove ()
|
||||||
link_added_hook:remove ()
|
link_added_hook:remove ()
|
||||||
link_removed_hook:remove ()
|
link_removed_hook:remove ()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ find_stored_profile_hook = SimpleEventHook {
|
||||||
local dont_restore_off_profile = cutils.parseBool (
|
local dont_restore_off_profile = cutils.parseBool (
|
||||||
device_props["session.dont-restore-off-profile"])
|
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
|
||||||
|
|
||||||
|
|
@ -91,7 +91,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ()
|
|
||||||
|
|
@ -218,6 +218,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" },
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
COMBINE_OFFSET = 64
|
COMBINE_OFFSET = 64
|
||||||
LOOPBACK_SOURCE_ID = 128
|
LOOPBACK_SOURCE_ID = 128
|
||||||
|
LOOPBACK_SINK_ID = 129
|
||||||
DEVICE_SOURCE_ID = 0
|
DEVICE_SOURCE_ID = 0
|
||||||
DEVICE_SINK_ID = 1
|
DEVICE_SINK_ID = 1
|
||||||
|
|
||||||
|
|
@ -21,10 +22,15 @@ 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 = {}
|
||||||
|
|
||||||
|
-- Properties used for previously creating a SCO or A2DP sink node. key: SPA device id
|
||||||
|
sco_a2dp_sink_node_properties = {}
|
||||||
|
|
||||||
devices_om = ObjectManager {
|
devices_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
type = "device",
|
type = "device",
|
||||||
Constraint { "device.api", "=", "bluez5" },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,15 +217,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 +234,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 +262,35 @@ 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
|
||||||
|
|
||||||
|
-- hide the sink node because we use the loopback sink instead
|
||||||
|
if parent:get_managed_object (LOOPBACK_SINK_ID) ~= nil and
|
||||||
|
(factory == "api.bluez5.sco.sink" or
|
||||||
|
factory == "api.bluez5.a2dp.sink") then
|
||||||
|
properties["bluez5.sink-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 +318,13 @@ 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))
|
||||||
|
if factory == "api.bluez5.sco.source" then
|
||||||
-- Set sink/source specific properties
|
|
||||||
if factory == "api.bluez5.sco.source" or
|
|
||||||
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"])) then
|
|
||||||
properties["bluez5.loopback"] = false
|
properties["bluez5.loopback"] = false
|
||||||
if properties["api.bluez5.profile"] ~= "headset-audio-gateway" then
|
sco_source_node_properties[parent_spa_id] = properties
|
||||||
properties["api.bluez5.internal"] = true
|
elseif factory == "api.bluez5.sco.sink" or factory == "api.bluez5.a2dp.sink" then
|
||||||
end
|
properties["bluez5.sink-loopback"] = false
|
||||||
|
sco_a2dp_sink_node_properties[parent_spa_id] = properties
|
||||||
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 +334,21 @@ 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]
|
||||||
|
local sink_properties = sco_a2dp_sink_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 source properties")
|
||||||
|
sco_source_node_properties[parent_spa_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if sink_properties ~= nil and id == tonumber(sink_properties["spa.object.id"]) then
|
||||||
|
log:debug("Clear old SCO-A2DP sink properties")
|
||||||
|
sco_a2dp_sink_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 +422,8 @@ 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
|
||||||
|
sco_a2dp_sink_node_properties[id] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function createMonitor()
|
function createMonitor()
|
||||||
|
|
@ -409,18 +441,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 +456,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),
|
||||||
|
|
@ -448,6 +468,47 @@ function CreateDeviceLoopbackSource (dev_props, dev_id)
|
||||||
["device.routes"] = "1",
|
["device.routes"] = "1",
|
||||||
["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(), {})
|
||||||
|
end
|
||||||
|
|
||||||
|
function CreateDeviceLoopbackSink (dev_name, dec_desc, dev_id)
|
||||||
|
local args = Json.Object {
|
||||||
|
["capture.props"] = Json.Object {
|
||||||
|
["node.name"] = string.format ("bluez_output.%s", dev_name),
|
||||||
|
["node.description"] = string.format ("%s", dec_desc),
|
||||||
|
["node.virtual"] = false,
|
||||||
|
["audio.position"] = "[FL, FR]",
|
||||||
|
["media.class"] = "Audio/Sink",
|
||||||
|
["device.id"] = dev_id,
|
||||||
|
["card.profile.device"] = DEVICE_SINK_ID,
|
||||||
|
["device.routes"] = "1",
|
||||||
|
["priority.session"] = 2010,
|
||||||
|
["bluez5.sink-loopback"] = true,
|
||||||
|
["filter.smart"] = true,
|
||||||
|
["filter.smart.target"] = Json.Object {
|
||||||
|
["bluez5.sink-loopback-target"] = true,
|
||||||
|
["bluez5.sink-loopback"] = false,
|
||||||
|
["device.id"] = dev_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["playback.props"] = Json.Object {
|
||||||
|
["node.name"] = string.format ("bluez_playback_internal.%s", dev_name),
|
||||||
|
["media.class"] = "Stream/Output/Audio/Internal",
|
||||||
|
["node.description"] =
|
||||||
|
string.format ("Bluetooth internal playback stream for %s", dec_desc),
|
||||||
|
["bluez5.sink-loopback"] = true,
|
||||||
|
["node.passive"] = true,
|
||||||
|
["node.dont-fallback"] = true,
|
||||||
|
["node.linger"] = true,
|
||||||
|
["state.restore-props"] = false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||||
|
|
@ -458,6 +519,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 +531,84 @@ 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 source loopback device if never created before
|
||||||
-- Always create the source loopback device if autoswitch is enabled.
|
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||||
-- Otherwise, only create the source loopback device if the current profile
|
if source_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 source 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)
|
source_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, source_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
|
||||||
|
|
||||||
|
local sink_loopback = spa_device:get_managed_object (LOOPBACK_SINK_ID)
|
||||||
|
if sink_loopback == nil then
|
||||||
|
local dev_name = props["api.bluez5.address"] or props["device.name"]
|
||||||
|
local dec_desc = props["device.description"] or props["device.name"]
|
||||||
|
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
|
||||||
|
|
||||||
|
log:info("create SCO-A2DP sink loopback node: " .. dev_name)
|
||||||
|
|
||||||
|
-- sanitize description, replace ':' with ' '
|
||||||
|
dec_desc = dec_desc:gsub("(:)", " ")
|
||||||
|
sink_loopback = CreateDeviceLoopbackSink (dev_name, dec_desc, device_id)
|
||||||
|
spa_device:store_managed_object(LOOPBACK_SINK_ID, sink_loopback)
|
||||||
|
|
||||||
|
-- recreate any sco-a2dp sink node
|
||||||
|
local properties = sco_a2dp_sink_node_properties[device_spa_id]
|
||||||
|
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.sink-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 +617,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 +657,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 ()
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ _wpctl_pw_defaults() {
|
||||||
_wpctl() {
|
_wpctl() {
|
||||||
local cur prev words cword
|
local cur prev words cword
|
||||||
local commands="status get-volume inspect set-default set-volume set-mute
|
local commands="status get-volume inspect set-default set-volume set-mute
|
||||||
set-profile set-route clear-default settings set-log-level
|
set-profile set-route clear-default settings set-log-level"
|
||||||
list"
|
|
||||||
|
|
||||||
_init_completion -n = || return
|
_init_completion -n = || return
|
||||||
|
|
||||||
|
|
@ -25,16 +24,6 @@ _wpctl() {
|
||||||
clear-default)
|
clear-default)
|
||||||
COMPREPLY+=($(compgen -W "0 1 2" -- "$cur"))
|
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
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
[wrap-file]
|
[wrap-file]
|
||||||
directory = lua-5.5.0
|
directory = lua-5.4.4
|
||||||
source_url = https://www.lua.org/ftp/lua-5.5.0.tar.gz
|
source_url = https://www.lua.org/ftp/lua-5.4.4.tar.gz
|
||||||
source_filename = lua-5.5.0.tar.gz
|
source_filename = lua-5.4.4.tar.gz
|
||||||
source_hash = 57ccc32bbbd005cab75bcc52444052535af691789dba2b9016d5c50640d68b3d
|
source_hash = 164c7849653b80ae67bec4b7473b884bf5cc8d2dca05653475ec2ed27b9ebf61
|
||||||
source_fallback_url = https://wrapdb.mesonbuild.com/v2/lua_5.5.0-1/get_source/lua-5.5.0.tar.gz
|
patch_filename = lua_5.4.4-1_patch.zip
|
||||||
patch_filename = lua_5.5.0-1_patch.zip
|
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.4-1/get_patch
|
||||||
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.5.0-1/get_patch
|
patch_hash = e61cd965c629d6543176f41a9f1cb9050edfd1566cf00ce768ff211086e40bdc
|
||||||
patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/lua_5.5.0-1/lua_5.5.0-1_patch.zip
|
|
||||||
patch_hash = 69ec4a2dd99ecf8e84830093d418f3a5be1202f16ba8d636b3008b67506e5cca
|
|
||||||
wrapdb_version = 5.5.0-1
|
|
||||||
|
|
||||||
[provide]
|
[provide]
|
||||||
dependency_names = lua, lua-5.5
|
lua-5.4 = lua_dep
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ fi
|
||||||
echo "Using build directory: ${BUILDDIR}"
|
echo "Using build directory: ${BUILDDIR}"
|
||||||
|
|
||||||
export WIREPLUMBER_MODULE_DIR="${BUILDDIR}/modules"
|
export WIREPLUMBER_MODULE_DIR="${BUILDDIR}/modules"
|
||||||
export WIREPLUMBER_CONFIG_DIR="${SOURCEDIR}/src/${CONFIGDIR}${WIREPLUMBER_CONFIG_DIR:+:$WIREPLUMBER_CONFIG_DIR}"
|
export WIREPLUMBER_CONFIG_DIR="${SOURCEDIR}/src/${CONFIGDIR}"
|
||||||
export WIREPLUMBER_DATA_DIR="${SOURCEDIR}/src"
|
export WIREPLUMBER_DATA_DIR="${SOURCEDIR}/src"
|
||||||
export PATH="${BUILDDIR}/src:${BUILDDIR}/src/tools:$PATH"
|
export PATH="${BUILDDIR}/src:${BUILDDIR}/src/tools:$PATH"
|
||||||
export LD_LIBRARY_PATH="${BUILDDIR}/lib/wp:$LD_LIBRARY_PATH"
|
export LD_LIBRARY_PATH="${BUILDDIR}/lib/wp:$LD_LIBRARY_PATH"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue