mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-05 17:08:01 +02:00
Compare commits
No commits in common. "master" and "0.5.12" have entirely different histories.
106 changed files with 1558 additions and 7114 deletions
|
|
@ -154,22 +154,13 @@ include:
|
||||||
# Fedora also ships that, but without the test plugins that we need...
|
# Fedora also ships that, but without the test plugins that we need...
|
||||||
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
|
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
|
||||||
https://gitlab.freedesktop.org/pipewire/pipewire.git
|
https://gitlab.freedesktop.org/pipewire/pipewire.git
|
||||||
# Set build options based on PipeWire version
|
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
|
||||||
- |
|
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled
|
||||||
case "$PIPEWIRE_HEAD" in
|
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
|
||||||
1.0|1.2|1.4)
|
-Dvulkan=disabled -Dgstreamer=disabled -Dlibsystemd=disabled
|
||||||
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
|
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
|
||||||
;;
|
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
|
||||||
*)
|
-Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
||||||
export PIPEWIRE_BUILD_OPTIONS="-Dlibsystemd=disabled"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" $PIPEWIRE_BUILD_OPTIONS
|
|
||||||
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled
|
|
||||||
-Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled
|
|
||||||
-Dgstreamer=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled
|
|
||||||
-Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled
|
|
||||||
-Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
|
||||||
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
|
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
|
||||||
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
|
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
|
||||||
# misc environment only for wireplumber
|
# misc environment only for wireplumber
|
||||||
|
|
@ -245,9 +236,6 @@ build_on_fedora_no_docs:
|
||||||
stage: build
|
stage: build
|
||||||
variables:
|
variables:
|
||||||
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
|
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
|
||||||
parallel:
|
|
||||||
matrix:
|
|
||||||
- PIPEWIRE_HEAD: ['master', '1.4', '1.2', '1.0']
|
|
||||||
|
|
||||||
build_on_ubuntu_with_gir:
|
build_on_ubuntu_with_gir:
|
||||||
extends:
|
extends:
|
||||||
|
|
|
||||||
37
AGENTS.md
37
AGENTS.md
|
|
@ -1,37 +0,0 @@
|
||||||
## Building and Testing
|
|
||||||
|
|
||||||
- To compile the project: `meson compile -C build` (compiles everything, no target needed)
|
|
||||||
- To run tests: `meson test -C build`
|
|
||||||
- The build artifacts always live in a directory called `build` or `builddir`.
|
|
||||||
If `build` doesn't exist, use `-C builddir` in the meson commands.
|
|
||||||
|
|
||||||
## Git Workflow
|
|
||||||
|
|
||||||
- Main branch: `master`
|
|
||||||
- Always create feature branches for new work
|
|
||||||
- Use descriptive commit messages following project conventions
|
|
||||||
- Reference GitLab MR/issue numbers in commits where applicable
|
|
||||||
- Never commit build artifacts or temporary files
|
|
||||||
- Use `glab` CLI tool for GitLab interactions (MRs, issues, etc.)
|
|
||||||
|
|
||||||
## Making a release
|
|
||||||
|
|
||||||
- Each release always consists of an entry in NEWS.rst, at the top of the file, which describes
|
|
||||||
the changes between the previous release and the current one. In addition, each release is given
|
|
||||||
a unique version number, which is present:
|
|
||||||
1. on the section header of that NEWS.rst entry
|
|
||||||
2. in the project() command in meson.build
|
|
||||||
3. on the commit message of the commit that introduces the above 2 changes
|
|
||||||
4. on the git tag that marks the above commit
|
|
||||||
- In order to make a release:
|
|
||||||
- Begin by analyzing the git history and the merged MRs from GitLab between the previous release
|
|
||||||
and today. GitLab MRs that are relevant always have the new release's version number set as a
|
|
||||||
"milestone"
|
|
||||||
- Create a new entry in NEWS.rst describing the changes, in a similar style and format as the
|
|
||||||
previous entries. Consolidate the changes to larger work items and also reference the relevant
|
|
||||||
gitlab MR that corresponds to each change and/or the gitlab issues that were addressed by each
|
|
||||||
change.
|
|
||||||
- Make sure to move the "Past releases" section header up, so that the only 2 top-level sections
|
|
||||||
are the new release section and the "Past releases" section.
|
|
||||||
- Edit meson.build to change the project version to the new release number
|
|
||||||
- Do not commit anything to git. Let the user review the changes and commit manually.
|
|
||||||
144
NEWS.rst
144
NEWS.rst
|
|
@ -1,144 +1,5 @@
|
||||||
WirePlumber 0.5.14
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Additions & Enhancements:
|
|
||||||
|
|
||||||
- Added per-device default volume configuration via the
|
|
||||||
``device.routes.default-{source,sink}-volume`` property, allowing device-specific volume
|
|
||||||
defaults (e.g. a comfortable default for internal speakers or no attenuation for HDMI) (!772)
|
|
||||||
|
|
||||||
- Added Lua 5.5 support; the bundled Lua subproject wrap has also been updated to 5.5.0
|
|
||||||
(!775, !788)
|
|
||||||
|
|
||||||
- Enhanced libcamera monitor to load camera nodes locally within the WirePlumber
|
|
||||||
process instead of the PipeWire daemon, eliminating race conditions that could occur
|
|
||||||
during initial enumeration and hotplug events (!790)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth loopback nodes to always be created when a device supports both
|
|
||||||
A2DP and HSP/HFP profiles, simplifying the logic and making the BT profile autoswitch
|
|
||||||
setting take effect immediately without requiring device reconnection (!782)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth loopback nodes to use ``target.object`` property instead of smart
|
|
||||||
filters, fixing issues that prevented users from setting them as default nodes and
|
|
||||||
also allowing smart filters to be used with them (#898; !792)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth profile autoswitch logic with further robustness improvements,
|
|
||||||
including better headset profile detection using profile name patterns and resolving
|
|
||||||
race conditions by running profile switching after ``device/apply-profile`` in a
|
|
||||||
dedicated event hook (#926, #923; !776, !777, !808)
|
|
||||||
|
|
||||||
- Enhanced wpctl ``set-default`` command to accept virtual nodes (e.g.
|
|
||||||
``Audio/Source/Virtual``) in addition to regular device nodes (#896; !787)
|
|
||||||
|
|
||||||
- Improved stream linking to make the full graph rescan optional when linkable items
|
|
||||||
change, saving CPU on low-end systems and reducing audio startup latency when
|
|
||||||
connecting multiple streams in quick succession (!800)
|
|
||||||
|
|
||||||
- Allowed installation of systemd service units without libsystemd being present,
|
|
||||||
useful for distributions like Alpine Linux that allow systemd service subpackages
|
|
||||||
(!793)
|
|
||||||
|
|
||||||
- Allowed the ``mincore`` syscall in the WirePlumber systemd sandbox, required for
|
|
||||||
Mesa/EGL (e.g. for the libcamera GPUISP pipeline)
|
|
||||||
|
|
||||||
- Allowed passing ``WIREPLUMBER_CONFIG_DIR`` via the ``wp-uninstalled`` script,
|
|
||||||
useful for passing additional configuration paths in an uninstalled environment (!801)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Removed Bluetooth sink loopback node, which was causing issues with KDE and GNOME (!794)
|
|
||||||
|
|
||||||
- Fixed default audio source selection to never automatically use ``Audio/Sink`` nodes
|
|
||||||
as the default source unless explicitly selected by the user (#886; !781)
|
|
||||||
|
|
||||||
- Fixed crash in ``state-stream`` when the Format parameter has a Choice for the
|
|
||||||
number of channels (#903; !795)
|
|
||||||
|
|
||||||
- Fixed BAP Bluetooth device set channel properties, where ``audio.position`` was
|
|
||||||
incorrectly serialized as a pointer address instead of the channel array (!786)
|
|
||||||
|
|
||||||
- Fixed memory leaks in ``wp_interest_event_hook_get_matching_event_types`` and in
|
|
||||||
the Lua ``LocalModule()`` implementation (!784, !810)
|
|
||||||
|
|
||||||
- Fixed HFP HF stream media class being incorrectly assigned due to
|
|
||||||
``api.bluez5.internal=true`` being set on HFP HF streams (!809)
|
|
||||||
|
|
||||||
- Fixed Lua 5.4 compatibility in ``state-stream`` script
|
|
||||||
|
|
||||||
- Updated translations: Bulgarian, Georgian, Kazakh, Swedish
|
|
||||||
|
|
||||||
Past releases
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
WirePlumber 0.5.13
|
|
||||||
..................
|
|
||||||
|
|
||||||
Additions & Enhancements:
|
|
||||||
|
|
||||||
- Added internal filter graph support for audio nodes, allowing users to
|
|
||||||
create audio preprocessing and postprocessing chains without exposing
|
|
||||||
filters to applications, useful for software DSP (!743)
|
|
||||||
|
|
||||||
- Added new Lua Properties API that significantly improves performance by
|
|
||||||
avoiding constant serialization between WpProperties and Lua tables,
|
|
||||||
resulting in approximately 40% faster node linking (!757)
|
|
||||||
|
|
||||||
- Added WpIterator Lua API for more efficient parameter enumeration (!746)
|
|
||||||
|
|
||||||
- Added bash completions for wpctl command (!762)
|
|
||||||
|
|
||||||
- Added script to find suitable volume control when using role-based policy,
|
|
||||||
allowing volume sliders to automatically adjust the volume of the currently
|
|
||||||
active role (e.g., ringing, call, media) (!711)
|
|
||||||
|
|
||||||
- Added experimental HDMI channel detection setting to use HDMI ELD
|
|
||||||
information for channel configuration (!749)
|
|
||||||
|
|
||||||
- Enhanced role-based policy to allow setting preferred target sinks for
|
|
||||||
media role loopbacks via ``policy.role-based.preferred-target`` (!754)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth profile autoswitch logic to be more robust and handle
|
|
||||||
saved profiles correctly, including support for loopback sink nodes (!739)
|
|
||||||
|
|
||||||
- Enhanced ALSA monitor to include ``alsa.*`` device properties on nodes for
|
|
||||||
rule matching (!761)
|
|
||||||
|
|
||||||
- Optimized stream node linking for common cases to reduce latency when new
|
|
||||||
audio/video streams are added (!760)
|
|
||||||
|
|
||||||
- Improved event dispatcher performance by using hash table registration for
|
|
||||||
event hooks, eliminating performance degradation as more hooks are
|
|
||||||
registered (!765)
|
|
||||||
|
|
||||||
- Increased audio headroom for VMware and VirtualBox virtual machines (!756)
|
|
||||||
|
|
||||||
- Added setting to prevent restoring "Off" profiles via
|
|
||||||
``session.dont-restore-off-profile`` property (!753)
|
|
||||||
|
|
||||||
- Added support for 128 audio channels when compiled with a recent version of
|
|
||||||
PipeWire (pipewire#4995; CI checks in !768)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed memory leaks and issues in the modem manager module (!770, !764)
|
|
||||||
|
|
||||||
- Fixed MPRIS module incorrectly treating GHashTable as GObject (!759)
|
|
||||||
|
|
||||||
- Fixed warning messages when process files in ``/proc/<pid>/*`` don't exist,
|
|
||||||
particularly when processes are removed quickly (#816, !717)
|
|
||||||
|
|
||||||
- Fixed MONO audio configuration to only apply to device sink nodes, allowing
|
|
||||||
multi-channel mixing in the graph (!769)
|
|
||||||
|
|
||||||
- Fixed event dispatcher hook registration and removal to avoid spurious
|
|
||||||
errors (!747)
|
|
||||||
|
|
||||||
- Improved logging for standard-link activation failures (!744)
|
|
||||||
|
|
||||||
- Simplified event-hook interest matching for better performance (!758)
|
|
||||||
|
|
||||||
WirePlumber 0.5.12
|
WirePlumber 0.5.12
|
||||||
..................
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Additions & Enhancements:
|
Additions & Enhancements:
|
||||||
|
|
||||||
|
|
@ -166,6 +27,9 @@ Fixes:
|
||||||
|
|
||||||
- Improved device hook documentation and configuration (!736)
|
- Improved device hook documentation and configuration (!736)
|
||||||
|
|
||||||
|
Past releases
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
WirePlumber 0.5.11
|
WirePlumber 0.5.11
|
||||||
..................
|
..................
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -140,9 +140,9 @@ Policies
|
||||||
for enabling devices, linking streams, granting permissions to clients,
|
for enabling devices, linking streams, granting permissions to clients,
|
||||||
etc, as appropriate for a desktop system.
|
etc, as appropriate for a desktop system.
|
||||||
|
|
||||||
.. describe:: policy.role-based
|
.. describe:: policy.role-priority-system
|
||||||
|
|
||||||
Enables the role based priority system policy. This system creates virtual sinks
|
Enables the role priority system policy. This system creates virtual sinks
|
||||||
that group streams based on their ``media.role`` property, and assigns a
|
that group streams based on their ``media.role`` property, and assigns a
|
||||||
priority to each role. Depending on the priority configuration, lower
|
priority to each role. Depending on the priority configuration, lower
|
||||||
priority roles may be corked or ducked when a higher priority role stream
|
priority roles may be corked or ducked when a higher priority role stream
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ Synopsis:
|
||||||
|
|
||||||
$ meson -Dsession-managers="[ 'wireplumber' ]" build
|
$ meson -Dsession-managers="[ 'wireplumber' ]" build
|
||||||
$ ninja -C build
|
$ ninja -C build
|
||||||
$ make -C build run
|
$ make run
|
||||||
|
|
||||||
Run independently or without installing
|
Run independently or without installing
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
|
|
@ -42,7 +42,7 @@ assignments:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
After that, once the media class of a device node has been selected for a
|
After that, once the media class of a device node has been select for a
|
||||||
particular stream node, and there are more than 1 device node matching such
|
particular stream node, and there are more than 1 device node matching such
|
||||||
media class, WirePlumber will select one based on a set of priorities:
|
media class, WirePlumber will select one based on a set of priorities:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,7 @@ spa_device_event_object_info (void *data, uint32_t id,
|
||||||
g_autoptr (WpProperties) props = NULL;
|
g_autoptr (WpProperties) props = NULL;
|
||||||
|
|
||||||
type = spa_debug_type_short_name (info->type);
|
type = spa_debug_type_short_name (info->type);
|
||||||
props = wp_properties_new_copy_dict (info->props);
|
props = wp_properties_new_wrap_dict (info->props);
|
||||||
|
|
||||||
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
|
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
|
||||||
id, type, info->factory_name);
|
id, type, info->factory_name);
|
||||||
|
|
|
||||||
|
|
@ -15,161 +15,6 @@
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
|
||||||
|
|
||||||
typedef struct _HookData HookData;
|
|
||||||
struct _HookData
|
|
||||||
{
|
|
||||||
struct spa_list link;
|
|
||||||
WpEventHook *hook;
|
|
||||||
GPtrArray *dependencies;
|
|
||||||
};
|
|
||||||
|
|
||||||
static inline HookData *
|
|
||||||
hook_data_new (WpEventHook * hook)
|
|
||||||
{
|
|
||||||
HookData *hook_data = g_new0 (HookData, 1);
|
|
||||||
spa_list_init (&hook_data->link);
|
|
||||||
hook_data->hook = g_object_ref (hook);
|
|
||||||
hook_data->dependencies = g_ptr_array_new ();
|
|
||||||
return hook_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
hook_data_free (HookData *self)
|
|
||||||
{
|
|
||||||
g_clear_object (&self->hook);
|
|
||||||
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
|
||||||
g_free (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
record_dependency (struct spa_list *list, const gchar *target,
|
|
||||||
const gchar *dependency)
|
|
||||||
{
|
|
||||||
HookData *hook_data;
|
|
||||||
spa_list_for_each (hook_data, list, link) {
|
|
||||||
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
|
||||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline gboolean
|
|
||||||
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
|
||||||
{
|
|
||||||
HookData *hook_data;
|
|
||||||
if (!spa_list_is_empty (list)) {
|
|
||||||
spa_list_for_each (hook_data, list, link) {
|
|
||||||
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
sort_hooks (GPtrArray *hooks)
|
|
||||||
{
|
|
||||||
struct spa_list collected, result, remaining;
|
|
||||||
HookData *sorted_hook_data = NULL;
|
|
||||||
|
|
||||||
spa_list_init (&collected);
|
|
||||||
spa_list_init (&result);
|
|
||||||
spa_list_init (&remaining);
|
|
||||||
|
|
||||||
for (guint i = 0; i < hooks->len; i++) {
|
|
||||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
|
||||||
HookData *hook_data = hook_data_new (hook);
|
|
||||||
|
|
||||||
/* record "after" dependencies directly */
|
|
||||||
const gchar * const * strv =
|
|
||||||
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
|
||||||
while (strv && *strv) {
|
|
||||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
|
||||||
strv++;
|
|
||||||
}
|
|
||||||
|
|
||||||
spa_list_append (&collected, &hook_data->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!spa_list_is_empty (&collected)) {
|
|
||||||
HookData *hook_data;
|
|
||||||
|
|
||||||
/* convert "before" dependencies into "after" dependencies */
|
|
||||||
spa_list_for_each (hook_data, &collected, link) {
|
|
||||||
const gchar * const * strv =
|
|
||||||
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
|
||||||
while (strv && *strv) {
|
|
||||||
/* record hook_data->hook as a dependency of the *strv hook */
|
|
||||||
record_dependency (&collected, *strv,
|
|
||||||
wp_event_hook_get_name (hook_data->hook));
|
|
||||||
strv++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sort */
|
|
||||||
while (!spa_list_is_empty (&collected)) {
|
|
||||||
gboolean made_progress = FALSE;
|
|
||||||
|
|
||||||
/* examine each hook to see if its dependencies are satisfied in the
|
|
||||||
result list; if yes, then append it to the result too */
|
|
||||||
spa_list_consume (hook_data, &collected, link) {
|
|
||||||
guint deps_satisfied = 0;
|
|
||||||
|
|
||||||
spa_list_remove (&hook_data->link);
|
|
||||||
|
|
||||||
for (guint i = 0; i < hook_data->dependencies->len; i++) {
|
|
||||||
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
|
|
||||||
/* if the dependency is already in the sorted result list or if
|
|
||||||
it doesn't exist at all, we consider it satisfied */
|
|
||||||
if (hook_exists_in (dep, &result) ||
|
|
||||||
!(hook_exists_in (dep, &collected) ||
|
|
||||||
hook_exists_in (dep, &remaining))) {
|
|
||||||
deps_satisfied++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deps_satisfied == hook_data->dependencies->len) {
|
|
||||||
spa_list_append (&result, &hook_data->link);
|
|
||||||
made_progress = TRUE;
|
|
||||||
} else {
|
|
||||||
spa_list_append (&remaining, &hook_data->link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (made_progress) {
|
|
||||||
/* run again with the remaining hooks */
|
|
||||||
spa_list_insert_list (&collected, &remaining);
|
|
||||||
spa_list_init (&remaining);
|
|
||||||
}
|
|
||||||
else if (!spa_list_is_empty (&remaining)) {
|
|
||||||
/* if we did not make any progress towards growing the result list,
|
|
||||||
it means the dependencies cannot be satisfied because of circles */
|
|
||||||
spa_list_consume (hook_data, &result, link) {
|
|
||||||
spa_list_remove (&hook_data->link);
|
|
||||||
hook_data_free (hook_data);
|
|
||||||
}
|
|
||||||
spa_list_consume (hook_data, &remaining, link) {
|
|
||||||
spa_list_remove (&hook_data->link);
|
|
||||||
hook_data_free (hook_data);
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* clear hooks and add the sorted ones */
|
|
||||||
g_ptr_array_set_size (hooks, 0);
|
|
||||||
spa_list_consume (sorted_hook_data, &result, link) {
|
|
||||||
spa_list_remove (&sorted_hook_data->link);
|
|
||||||
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
|
|
||||||
hook_data_free (sorted_hook_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct _EventData EventData;
|
typedef struct _EventData EventData;
|
||||||
struct _EventData
|
struct _EventData
|
||||||
{
|
{
|
||||||
|
|
@ -204,8 +49,7 @@ struct _WpEventDispatcher
|
||||||
GObject parent;
|
GObject parent;
|
||||||
|
|
||||||
GWeakRef core;
|
GWeakRef core;
|
||||||
GHashTable *defined_hooks; /* registered hooks for defined events */
|
GPtrArray *hooks; /* registered hooks */
|
||||||
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
|
|
||||||
GSource *source; /* the event loop source */
|
GSource *source; /* the event loop source */
|
||||||
GList *events; /* the events stack */
|
GList *events; /* the events stack */
|
||||||
struct spa_system *system;
|
struct spa_system *system;
|
||||||
|
|
@ -260,7 +104,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
|
|
||||||
/* get the highest priority event */
|
/* get the highest priority event */
|
||||||
GList *levent = g_list_first (d->events);
|
GList *levent = g_list_first (d->events);
|
||||||
if (levent) {
|
while (levent) {
|
||||||
EventData *event_data = (EventData *) (levent->data);
|
EventData *event_data = (EventData *) (levent->data);
|
||||||
WpEvent *event = event_data->event;
|
WpEvent *event = event_data->event;
|
||||||
GCancellable *cancellable = wp_event_get_cancellable (event);
|
GCancellable *cancellable = wp_event_get_cancellable (event);
|
||||||
|
|
@ -300,8 +144,6 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
|
|
||||||
/* get the next event */
|
/* get the next event */
|
||||||
levent = g_list_first (d->events);
|
levent = g_list_first (d->events);
|
||||||
if (levent && !((EventData *) levent->data)->current_hook_in_async)
|
|
||||||
spa_system_eventfd_write (d->system, d->eventfd, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
|
|
@ -318,9 +160,7 @@ static void
|
||||||
wp_event_dispatcher_init (WpEventDispatcher * self)
|
wp_event_dispatcher_init (WpEventDispatcher * self)
|
||||||
{
|
{
|
||||||
g_weak_ref_init (&self->core, NULL);
|
g_weak_ref_init (&self->core, NULL);
|
||||||
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||||
(GDestroyNotify)g_ptr_array_unref);
|
|
||||||
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
|
||||||
|
|
||||||
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
|
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
|
||||||
((WpEventSource *) self->source)->dispatcher = self;
|
((WpEventSource *) self->source)->dispatcher = self;
|
||||||
|
|
@ -344,8 +184,7 @@ wp_event_dispatcher_finalize (GObject * object)
|
||||||
|
|
||||||
close (self->eventfd);
|
close (self->eventfd);
|
||||||
|
|
||||||
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
|
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
||||||
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
|
|
||||||
g_weak_ref_clear (&self->core);
|
g_weak_ref_clear (&self->core);
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
|
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
|
||||||
|
|
@ -445,10 +284,6 @@ void
|
||||||
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
||||||
WpEventHook * hook)
|
WpEventHook * hook)
|
||||||
{
|
{
|
||||||
g_autoptr (GPtrArray) event_types = NULL;
|
|
||||||
gboolean is_defined = FALSE;
|
|
||||||
const gchar *hook_name;
|
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||||
|
|
||||||
|
|
@ -457,74 +292,7 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
||||||
g_return_if_fail (already_registered_dispatcher == NULL);
|
g_return_if_fail (already_registered_dispatcher == NULL);
|
||||||
|
|
||||||
wp_event_hook_set_dispatcher (hook, self);
|
wp_event_hook_set_dispatcher (hook, self);
|
||||||
|
g_ptr_array_add (self->hooks, g_object_ref (hook));
|
||||||
/* Register the event hook in the defined hooks table if it is defined */
|
|
||||||
hook_name = wp_event_hook_get_name (hook);
|
|
||||||
event_types = wp_event_hook_get_matching_event_types (hook);
|
|
||||||
if (event_types) {
|
|
||||||
for (guint i = 0; i < event_types->len; i++) {
|
|
||||||
const gchar *event_type = g_ptr_array_index (event_types, i);
|
|
||||||
GPtrArray *hooks;
|
|
||||||
|
|
||||||
wp_debug_object (self, "Registering hook %s for defined event type %s",
|
|
||||||
hook_name, event_type);
|
|
||||||
|
|
||||||
/* Check if the event type was registered in the hash table */
|
|
||||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
|
||||||
if (hooks) {
|
|
||||||
g_ptr_array_add (hooks, g_object_ref (hook));
|
|
||||||
if (!sort_hooks (hooks))
|
|
||||||
goto sort_error;
|
|
||||||
} else {
|
|
||||||
GPtrArray *new_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
|
||||||
/* Add undefined hooks */
|
|
||||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
|
||||||
WpEventHook *uh = g_ptr_array_index (self->undefined_hooks, i);
|
|
||||||
g_ptr_array_add (new_hooks, g_object_ref (uh));
|
|
||||||
}
|
|
||||||
/* Add current hook */
|
|
||||||
g_ptr_array_add (new_hooks, g_object_ref (hook));
|
|
||||||
g_hash_table_insert (self->defined_hooks, g_strdup (event_type),
|
|
||||||
new_hooks);
|
|
||||||
if (!sort_hooks (new_hooks))
|
|
||||||
goto sort_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_defined = TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Otherwise just register it as undefined hook */
|
|
||||||
if (!is_defined) {
|
|
||||||
GHashTableIter iter;
|
|
||||||
gpointer value;
|
|
||||||
|
|
||||||
wp_debug_object (self, "Registering hook %s for undefined event types",
|
|
||||||
hook_name);
|
|
||||||
|
|
||||||
/* Add it to the defined hooks table */
|
|
||||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
|
||||||
GPtrArray *defined_hooks = value;
|
|
||||||
g_ptr_array_add (defined_hooks, g_object_ref (hook));
|
|
||||||
if (!sort_hooks (defined_hooks))
|
|
||||||
goto sort_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add it to the undefined hooks */
|
|
||||||
g_ptr_array_add (self->undefined_hooks, g_object_ref (hook));
|
|
||||||
if (!sort_hooks (self->undefined_hooks))
|
|
||||||
goto sort_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_info_object (self, "Registered hook %s successfully", hook_name);
|
|
||||||
return;
|
|
||||||
|
|
||||||
sort_error:
|
|
||||||
/* Unregister hook */
|
|
||||||
wp_event_dispatcher_unregister_hook (self, hook);
|
|
||||||
wp_warning_object (self,
|
|
||||||
"Could not register hook %s because of circular dependencies", hook_name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
@ -538,9 +306,6 @@ void
|
||||||
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||||
WpEventHook * hook)
|
WpEventHook * hook)
|
||||||
{
|
{
|
||||||
GHashTableIter iter;
|
|
||||||
gpointer value;
|
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||||
|
|
||||||
|
|
@ -549,29 +314,11 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||||
g_return_if_fail (already_registered_dispatcher == self);
|
g_return_if_fail (already_registered_dispatcher == self);
|
||||||
|
|
||||||
wp_event_hook_set_dispatcher (hook, NULL);
|
wp_event_hook_set_dispatcher (hook, NULL);
|
||||||
|
g_ptr_array_remove_fast (self->hooks, hook);
|
||||||
/* Remove hook from defined table and undefined list */
|
|
||||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
|
||||||
GPtrArray *defined_hooks = value;
|
|
||||||
g_ptr_array_remove (defined_hooks, hook);
|
|
||||||
}
|
|
||||||
g_ptr_array_remove (self->undefined_hooks, hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_unique (GPtrArray *array, WpEventHook * hook)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < array->len; i++)
|
|
||||||
if (g_ptr_array_index (array, i) == hook)
|
|
||||||
return;
|
|
||||||
g_ptr_array_add (array, g_object_ref (hook));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns an iterator to iterate over all the registered hooks
|
* \brief Returns an iterator to iterate over all the registered hooks
|
||||||
* \deprecated Use \ref wp_event_dispatcher_new_hooks_for_event_type_iterator
|
|
||||||
* instead.
|
|
||||||
* \ingroup wpeventdispatcher
|
* \ingroup wpeventdispatcher
|
||||||
*
|
*
|
||||||
* \param self the event dispatcher
|
* \param self the event dispatcher
|
||||||
|
|
@ -580,56 +327,7 @@ add_unique (GPtrArray *array, WpEventHook * hook)
|
||||||
WpIterator *
|
WpIterator *
|
||||||
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
||||||
{
|
{
|
||||||
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
|
GPtrArray *items =
|
||||||
GHashTableIter iter;
|
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
|
||||||
gpointer value;
|
|
||||||
|
|
||||||
/* Add all defined hooks */
|
|
||||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
|
||||||
GPtrArray *hooks = value;
|
|
||||||
for (guint i = 0; i < hooks->len; i++) {
|
|
||||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
|
||||||
add_unique (items, hook);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add all undefined hooks */
|
|
||||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
|
||||||
WpEventHook *hook = g_ptr_array_index (self->undefined_hooks, i);
|
|
||||||
add_unique (items, hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Returns an iterator to iterate over the registered hooks for a
|
|
||||||
* particular event type.
|
|
||||||
* \ingroup wpeventdispatcher
|
|
||||||
*
|
|
||||||
* \param self the event dispatcher
|
|
||||||
* \param event_type the event type
|
|
||||||
* \return (transfer full): a new iterator
|
|
||||||
* \since 0.5.13
|
|
||||||
*/
|
|
||||||
WpIterator *
|
|
||||||
wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
|
||||||
WpEventDispatcher * self, const gchar *event_type)
|
|
||||||
{
|
|
||||||
GPtrArray *items;
|
|
||||||
GPtrArray *hooks;
|
|
||||||
|
|
||||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
|
||||||
if (hooks) {
|
|
||||||
wp_debug_object (self, "Using %d defined hooks for event type %s",
|
|
||||||
hooks->len, event_type);
|
|
||||||
} else {
|
|
||||||
hooks = self->undefined_hooks;
|
|
||||||
wp_debug_object (self, "Using %d undefined hooks for event type %s",
|
|
||||||
hooks->len, event_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
items = g_ptr_array_copy (hooks, (GCopyFunc) g_object_ref, NULL);
|
|
||||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,7 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||||
WpEventHook * hook);
|
WpEventHook * hook);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
|
||||||
G_GNUC_DEPRECATED_FOR (wp_event_dispatcher_new_hooks_for_event_type_iterator);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpIterator * wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
|
||||||
WpEventDispatcher * self, const gchar *event_type);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,24 +254,6 @@ wp_event_hook_run (WpEventHook * self,
|
||||||
callback_data);
|
callback_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets all the matching event types for this hook if any.
|
|
||||||
*
|
|
||||||
* \ingroup wpeventhook
|
|
||||||
* \param self the event hook
|
|
||||||
* \returns (element-type gchar*) (transfer full) (nullable): the matching
|
|
||||||
* event types for this hook if any.
|
|
||||||
* \since 0.5.13
|
|
||||||
*/
|
|
||||||
GPtrArray *
|
|
||||||
wp_event_hook_get_matching_event_types (WpEventHook * self)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
|
|
||||||
g_return_val_if_fail (
|
|
||||||
WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
|
|
||||||
return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Finishes the async operation that was started by wp_event_hook_run()
|
* \brief Finishes the async operation that was started by wp_event_hook_run()
|
||||||
*
|
*
|
||||||
|
|
@ -339,61 +321,34 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
|
||||||
wp_interest_event_hook_get_instance_private (self);
|
wp_interest_event_hook_get_instance_private (self);
|
||||||
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
|
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
|
||||||
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
||||||
|
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
|
||||||
guint i;
|
guint i;
|
||||||
WpObjectInterest *interest = NULL;
|
WpObjectInterest *interest = NULL;
|
||||||
|
WpInterestMatch match;
|
||||||
|
|
||||||
|
const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
|
||||||
|
WP_INTEREST_MATCH_PW_PROPERTIES |
|
||||||
|
WP_INTEREST_MATCH_G_PROPERTIES);
|
||||||
|
|
||||||
for (i = 0; i < priv->interests->len; i++) {
|
for (i = 0; i < priv->interests->len; i++) {
|
||||||
interest = g_ptr_array_index (priv->interests, i);
|
interest = g_ptr_array_index (priv->interests, i);
|
||||||
if (wp_object_interest_matches_full (interest,
|
match = wp_object_interest_matches_full (interest,
|
||||||
WP_INTEREST_MATCH_FLAGS_NONE,
|
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
|
||||||
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
|
gtype, subject, properties, properties);
|
||||||
|
|
||||||
|
/* the interest may have a GType that matches the GType of the subject
|
||||||
|
or it may have WP_TYPE_EVENT as its GType, in which case it will
|
||||||
|
match any type of subject */
|
||||||
|
if (match == WP_INTEREST_MATCH_ALL)
|
||||||
|
return TRUE;
|
||||||
|
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
|
||||||
|
match = wp_object_interest_matches_full (interest, 0,
|
||||||
|
WP_TYPE_EVENT, NULL, NULL, NULL);
|
||||||
|
if (match & WP_INTEREST_MATCH_GTYPE)
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_unique (GPtrArray *array, const gchar * lookup)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < array->len; i++)
|
|
||||||
if (g_str_equal (g_ptr_array_index (array, i), lookup))
|
|
||||||
return;
|
|
||||||
g_ptr_array_add (array, g_strdup (lookup));
|
|
||||||
}
|
|
||||||
|
|
||||||
static GPtrArray *
|
|
||||||
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
|
|
||||||
{
|
|
||||||
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
|
|
||||||
WpInterestEventHookPrivate *priv =
|
|
||||||
wp_interest_event_hook_get_instance_private (self);
|
|
||||||
g_autoptr (GPtrArray) res = g_ptr_array_new_with_free_func (g_free);
|
|
||||||
guint i;
|
|
||||||
|
|
||||||
for (i = 0; i < priv->interests->len; i++) {
|
|
||||||
WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
|
|
||||||
if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
|
|
||||||
WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
|
|
||||||
g_autoptr (GPtrArray) values =
|
|
||||||
wp_object_interest_find_defined_constraint_values (interest,
|
|
||||||
WP_CONSTRAINT_TYPE_NONE, "event.type");
|
|
||||||
if (!values || values->len == 0) {
|
|
||||||
/* We always consider the hook undefined if it has at least one interest
|
|
||||||
* without a defined 'event.type' constraint */
|
|
||||||
return NULL;
|
|
||||||
} else {
|
|
||||||
for (guint j = 0; j < values->len; j++) {
|
|
||||||
GVariant *v = g_ptr_array_index (values, j);
|
|
||||||
if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
|
|
||||||
const gchar *v_str = g_variant_get_string (v, NULL);
|
|
||||||
add_unique (res, v_str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return FALSE;
|
||||||
return g_steal_pointer (&res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -404,8 +359,6 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
|
||||||
|
|
||||||
object_class->finalize = wp_interest_event_hook_finalize;
|
object_class->finalize = wp_interest_event_hook_finalize;
|
||||||
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
|
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
|
||||||
hook_class->get_matching_event_types =
|
|
||||||
wp_interest_event_hook_get_matching_event_types;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,8 @@ struct _WpEventHookClass
|
||||||
|
|
||||||
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
|
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
|
||||||
|
|
||||||
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
|
|
||||||
|
|
||||||
/*< private >*/
|
/*< private >*/
|
||||||
WP_PADDING(4)
|
WP_PADDING(5)
|
||||||
};
|
};
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
|
|
@ -69,9 +67,6 @@ void wp_event_hook_run (WpEventHook * self,
|
||||||
WpEvent * event, GCancellable * cancellable,
|
WpEvent * event, GCancellable * cancellable,
|
||||||
GAsyncReadyCallback callback, gpointer callback_data);
|
GAsyncReadyCallback callback, gpointer callback_data);
|
||||||
|
|
||||||
WP_API
|
|
||||||
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
|
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
|
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
|
||||||
GError ** error);
|
GError ** error);
|
||||||
|
|
|
||||||
262
lib/wp/event.c
262
lib/wp/event.c
|
|
@ -17,11 +17,37 @@
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
|
||||||
|
|
||||||
|
typedef struct _HookData HookData;
|
||||||
|
struct _HookData
|
||||||
|
{
|
||||||
|
struct spa_list link;
|
||||||
|
WpEventHook *hook;
|
||||||
|
GPtrArray *dependencies;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline HookData *
|
||||||
|
hook_data_new (WpEventHook * hook)
|
||||||
|
{
|
||||||
|
HookData *hook_data = g_new0 (HookData, 1);
|
||||||
|
spa_list_init (&hook_data->link);
|
||||||
|
hook_data->hook = g_object_ref (hook);
|
||||||
|
hook_data->dependencies = g_ptr_array_new ();
|
||||||
|
return hook_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
hook_data_free (HookData *self)
|
||||||
|
{
|
||||||
|
g_clear_object (&self->hook);
|
||||||
|
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
||||||
|
g_free (self);
|
||||||
|
}
|
||||||
|
|
||||||
struct _WpEvent
|
struct _WpEvent
|
||||||
{
|
{
|
||||||
grefcount ref;
|
grefcount ref;
|
||||||
GData *datalist;
|
GData *datalist;
|
||||||
GPtrArray *hooks;
|
struct spa_list hooks;
|
||||||
|
|
||||||
/* immutable fields */
|
/* immutable fields */
|
||||||
gint priority;
|
gint priority;
|
||||||
|
|
@ -70,7 +96,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
|
||||||
WpEvent * self = g_new0 (WpEvent, 1);
|
WpEvent * self = g_new0 (WpEvent, 1);
|
||||||
g_ref_count_init (&self->ref);
|
g_ref_count_init (&self->ref);
|
||||||
g_datalist_init (&self->datalist);
|
g_datalist_init (&self->datalist);
|
||||||
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
spa_list_init (&self->hooks);
|
||||||
|
|
||||||
self->priority = priority;
|
self->priority = priority;
|
||||||
self->properties = properties ?
|
self->properties = properties ?
|
||||||
|
|
@ -129,7 +155,11 @@ wp_event_get_name(WpEvent *self)
|
||||||
static void
|
static void
|
||||||
wp_event_free (WpEvent * self)
|
wp_event_free (WpEvent * self)
|
||||||
{
|
{
|
||||||
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
HookData *hook_data;
|
||||||
|
spa_list_consume (hook_data, &self->hooks, link) {
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
hook_data_free (hook_data);
|
||||||
|
}
|
||||||
g_datalist_clear (&self->datalist);
|
g_datalist_clear (&self->datalist);
|
||||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||||
g_clear_object (&self->source);
|
g_clear_object (&self->source);
|
||||||
|
|
@ -286,6 +316,33 @@ wp_event_get_data (WpEvent * self, const gchar * key)
|
||||||
return g_datalist_get_data (&self->datalist, key);
|
return g_datalist_get_data (&self->datalist, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
record_dependency (struct spa_list *list, const gchar *target,
|
||||||
|
const gchar *dependency)
|
||||||
|
{
|
||||||
|
HookData *hook_data;
|
||||||
|
spa_list_for_each (hook_data, list, link) {
|
||||||
|
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
||||||
|
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline gboolean
|
||||||
|
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
||||||
|
{
|
||||||
|
HookData *hook_data;
|
||||||
|
if (!spa_list_is_empty (list)) {
|
||||||
|
spa_list_for_each (hook_data, list, link) {
|
||||||
|
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Collects all the hooks registered in the \a dispatcher that run for
|
* \brief Collects all the hooks registered in the \a dispatcher that run for
|
||||||
* this \a event
|
* this \a event
|
||||||
|
|
@ -298,37 +355,199 @@ wp_event_get_data (WpEvent * self, const gchar * key)
|
||||||
gboolean
|
gboolean
|
||||||
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
||||||
{
|
{
|
||||||
|
struct spa_list collected, result, remaining;
|
||||||
g_autoptr (WpIterator) all_hooks = NULL;
|
g_autoptr (WpIterator) all_hooks = NULL;
|
||||||
g_auto (GValue) value = G_VALUE_INIT;
|
g_auto (GValue) value = G_VALUE_INIT;
|
||||||
const gchar *event_type = NULL;
|
|
||||||
|
|
||||||
g_return_val_if_fail (event != NULL, FALSE);
|
g_return_val_if_fail (event != NULL, FALSE);
|
||||||
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
|
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
|
||||||
|
|
||||||
/* Clear all current hooks */
|
/* hooks already collected */
|
||||||
g_ptr_array_set_size (event->hooks, 0);
|
if (!spa_list_is_empty (&event->hooks))
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
/* Get the event type */
|
spa_list_init (&collected);
|
||||||
event_type = wp_properties_get (event->properties, "event.type");
|
spa_list_init (&result);
|
||||||
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
|
spa_list_init (&remaining);
|
||||||
event->name, event_type);
|
|
||||||
|
|
||||||
/* Collect hooks that run for this event */
|
/* collect hooks that run for this event */
|
||||||
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
|
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
|
||||||
event_type);
|
|
||||||
while (wp_iterator_next (all_hooks, &value)) {
|
while (wp_iterator_next (all_hooks, &value)) {
|
||||||
WpEventHook *hook = g_value_get_object (&value);
|
WpEventHook *hook = g_value_get_object (&value);
|
||||||
|
|
||||||
if (wp_event_hook_runs_for_event (hook, event)) {
|
if (wp_event_hook_runs_for_event (hook, event)) {
|
||||||
g_ptr_array_add (event->hooks, g_object_ref (hook));
|
HookData *hook_data = hook_data_new (hook);
|
||||||
|
|
||||||
|
/* record "after" dependencies directly */
|
||||||
|
const gchar * const * strv =
|
||||||
|
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
||||||
|
while (strv && *strv) {
|
||||||
|
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
||||||
|
strv++;
|
||||||
|
}
|
||||||
|
|
||||||
|
spa_list_append (&collected, &hook_data->link);
|
||||||
|
|
||||||
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
||||||
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
|
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
|
||||||
}
|
}
|
||||||
|
|
||||||
g_value_unset (&value);
|
g_value_unset (&value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return event->hooks->len > 0;
|
if (!spa_list_is_empty (&collected)) {
|
||||||
|
HookData *hook_data;
|
||||||
|
|
||||||
|
/* convert "before" dependencies into "after" dependencies */
|
||||||
|
spa_list_for_each (hook_data, &collected, link) {
|
||||||
|
const gchar * const * strv =
|
||||||
|
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
||||||
|
while (strv && *strv) {
|
||||||
|
/* record hook_data->hook as a dependency of the *strv hook */
|
||||||
|
record_dependency (&collected, *strv,
|
||||||
|
wp_event_hook_get_name (hook_data->hook));
|
||||||
|
strv++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sort */
|
||||||
|
while (!spa_list_is_empty (&collected)) {
|
||||||
|
gboolean made_progress = FALSE;
|
||||||
|
|
||||||
|
/* examine each hook to see if its dependencies are satisfied in the
|
||||||
|
result list; if yes, then append it to the result too */
|
||||||
|
spa_list_consume (hook_data, &collected, link) {
|
||||||
|
guint deps_satisfied = 0;
|
||||||
|
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
|
||||||
|
wp_trace_boxed (WP_TYPE_EVENT, event,
|
||||||
|
"examining: %s", wp_event_hook_get_name (hook_data->hook));
|
||||||
|
|
||||||
|
for (guint i = 0; i < hook_data->dependencies->len; i++) {
|
||||||
|
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
|
||||||
|
/* if the dependency is already in the sorted result list or if
|
||||||
|
it doesn't exist at all, we consider it satisfied */
|
||||||
|
if (hook_exists_in (dep, &result) ||
|
||||||
|
!(hook_exists_in (dep, &collected) ||
|
||||||
|
hook_exists_in (dep, &remaining))) {
|
||||||
|
deps_satisfied++;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_trace_boxed (WP_TYPE_EVENT, event, "depends: %s, satisfied: %u/%u",
|
||||||
|
dep, deps_satisfied, hook_data->dependencies->len);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deps_satisfied == hook_data->dependencies->len) {
|
||||||
|
wp_trace_boxed (WP_TYPE_EVENT, event,
|
||||||
|
"sorted: "WP_OBJECT_FORMAT"(%s)",
|
||||||
|
WP_OBJECT_ARGS (hook_data->hook),
|
||||||
|
wp_event_hook_get_name (hook_data->hook));
|
||||||
|
|
||||||
|
spa_list_append (&result, &hook_data->link);
|
||||||
|
made_progress = TRUE;
|
||||||
|
} else {
|
||||||
|
spa_list_append (&remaining, &hook_data->link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (made_progress) {
|
||||||
|
/* run again with the remaining hooks */
|
||||||
|
spa_list_insert_list (&collected, &remaining);
|
||||||
|
spa_list_init (&remaining);
|
||||||
|
}
|
||||||
|
else if (!spa_list_is_empty (&remaining)) {
|
||||||
|
/* if we did not make any progress towards growing the result list,
|
||||||
|
it means the dependencies cannot be satisfied because of circles */
|
||||||
|
wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
|
||||||
|
"dependencies in the collected hooks!");
|
||||||
|
|
||||||
|
/* clean up */
|
||||||
|
spa_list_consume (hook_data, &result, link) {
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
hook_data_free (hook_data);
|
||||||
|
}
|
||||||
|
spa_list_consume (hook_data, &remaining, link) {
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
hook_data_free (hook_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spa_list_insert_list (&event->hooks, &result);
|
||||||
|
return !spa_list_is_empty (&event->hooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct event_hooks_iterator_data
|
||||||
|
{
|
||||||
|
WpEvent *event;
|
||||||
|
HookData *cur;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_hooks_iterator_reset (WpIterator *it)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
struct spa_list *list = &it_data->event->hooks;
|
||||||
|
|
||||||
|
if (!spa_list_is_empty (list))
|
||||||
|
it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
event_hooks_iterator_next (WpIterator *it, GValue *item)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
struct spa_list *list = &it_data->event->hooks;
|
||||||
|
|
||||||
|
if (!spa_list_is_empty (list) &&
|
||||||
|
!spa_list_is_end (it_data->cur, list, link)) {
|
||||||
|
g_value_init (item, WP_TYPE_EVENT_HOOK);
|
||||||
|
g_value_set_object (item, it_data->cur->hook);
|
||||||
|
it_data->cur = spa_list_next (it_data->cur, link);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
struct spa_list *list = &it_data->event->hooks;
|
||||||
|
HookData *hook_data;
|
||||||
|
|
||||||
|
if (!spa_list_is_empty (list)) {
|
||||||
|
spa_list_for_each (hook_data, list, link) {
|
||||||
|
g_auto (GValue) item = G_VALUE_INIT;
|
||||||
|
g_value_init (&item, WP_TYPE_EVENT_HOOK);
|
||||||
|
g_value_set_object (&item, hook_data->hook);
|
||||||
|
if (!func (&item, ret, data))
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_hooks_iterator_finalize (WpIterator *it)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
wp_event_unref (it_data->event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const WpIteratorMethods event_hooks_iterator_methods = {
|
||||||
|
.version = WP_ITERATOR_METHODS_VERSION,
|
||||||
|
.reset = event_hooks_iterator_reset,
|
||||||
|
.next = event_hooks_iterator_next,
|
||||||
|
.fold = event_hooks_iterator_fold,
|
||||||
|
.finalize = event_hooks_iterator_finalize,
|
||||||
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns an iterator that iterates over all the hooks that were
|
* \brief Returns an iterator that iterates over all the hooks that were
|
||||||
* collected by wp_event_collect_hooks()
|
* collected by wp_event_collect_hooks()
|
||||||
|
|
@ -339,8 +558,15 @@ wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
||||||
WpIterator *
|
WpIterator *
|
||||||
wp_event_new_hooks_iterator (WpEvent * event)
|
wp_event_new_hooks_iterator (WpEvent * event)
|
||||||
{
|
{
|
||||||
GPtrArray *hooks;
|
WpIterator *it = NULL;
|
||||||
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL);
|
struct event_hooks_iterator_data *it_data;
|
||||||
return wp_iterator_new_ptr_array (hooks, WP_TYPE_EVENT_HOOK);
|
|
||||||
|
|
||||||
|
g_return_val_if_fail (event != NULL, NULL);
|
||||||
|
|
||||||
|
it = wp_iterator_new (&event_hooks_iterator_methods,
|
||||||
|
sizeof (struct event_hooks_iterator_data));
|
||||||
|
it_data = wp_iterator_get_user_data (it);
|
||||||
|
it_data->event = wp_event_ref (event);
|
||||||
|
event_hooks_iterator_reset (it);
|
||||||
|
return it;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -881,51 +881,3 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Finds all the defined constraint values for a subject in \a self.
|
|
||||||
*
|
|
||||||
* A defined constraint value is the value of a constraint with the 'equal' or
|
|
||||||
* 'in-list' verb, because the full value must be defined with those verbs. This
|
|
||||||
* can be useful for cases where we want to enumerate interests that are
|
|
||||||
* interested in specific subjects.
|
|
||||||
*
|
|
||||||
* \ingroup wpobjectinterest
|
|
||||||
* \param self the object interest
|
|
||||||
* \param type the constraint type
|
|
||||||
* \param subject the subject that the constraint applies to
|
|
||||||
* \returns (element-type GVariant) (transfer full) (nullable): the defined
|
|
||||||
* constraint values for this object interest.
|
|
||||||
* \since 0.5.13
|
|
||||||
*/
|
|
||||||
GPtrArray *
|
|
||||||
wp_object_interest_find_defined_constraint_values (WpObjectInterest * self,
|
|
||||||
WpConstraintType type, const gchar * subject)
|
|
||||||
{
|
|
||||||
GPtrArray *res = g_ptr_array_new_with_free_func (
|
|
||||||
(GDestroyNotify)g_variant_unref);
|
|
||||||
struct constraint *c;
|
|
||||||
|
|
||||||
pw_array_for_each (c, &self->constraints) {
|
|
||||||
if ((c->type == type || WP_CONSTRAINT_TYPE_NONE == type) &&
|
|
||||||
g_str_equal (c->subject, subject)) {
|
|
||||||
switch (c->verb) {
|
|
||||||
case WP_CONSTRAINT_VERB_EQUALS:
|
|
||||||
g_ptr_array_add (res, g_variant_ref (c->value));
|
|
||||||
break;
|
|
||||||
case WP_CONSTRAINT_VERB_IN_LIST: {
|
|
||||||
GVariantIter iter;
|
|
||||||
GVariant *child;
|
|
||||||
g_variant_iter_init (&iter, c->value);
|
|
||||||
while ((child = g_variant_iter_next_value (&iter)))
|
|
||||||
g_ptr_array_add (res, child);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -130,10 +130,6 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
|
||||||
WpInterestMatchFlags flags, GType object_type, gpointer object,
|
WpInterestMatchFlags flags, GType object_type, gpointer object,
|
||||||
WpProperties * pw_props, WpProperties * pw_global_props);
|
WpProperties * pw_props, WpProperties * pw_global_props);
|
||||||
|
|
||||||
WP_API
|
|
||||||
GPtrArray * wp_object_interest_find_defined_constraint_values (
|
|
||||||
WpObjectInterest * self, WpConstraintType type, const gchar * subject);
|
|
||||||
|
|
||||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
|
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -783,7 +783,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
|
||||||
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
|
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
|
||||||
|
|
||||||
g_clear_pointer (&d->properties, wp_properties_unref);
|
g_clear_pointer (&d->properties, wp_properties_unref);
|
||||||
d->properties = wp_properties_new_copy_dict (props);
|
d->properties = wp_properties_new_wrap_dict (props);
|
||||||
|
|
||||||
g_object_notify (G_OBJECT (instance), "properties");
|
g_object_notify (G_OBJECT (instance), "properties");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <spa/utils/cleanup.h>
|
|
||||||
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "proc-utils.h"
|
#include "proc-utils.h"
|
||||||
|
|
@ -147,21 +145,6 @@ wp_proc_info_get_cgroup (WpProcInfo * self)
|
||||||
return self->cgroup;
|
return self->cgroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
static FILE *
|
|
||||||
fdopenat (int dirfd, const char *path, int flags, const char *mode, mode_t perm)
|
|
||||||
{
|
|
||||||
int fd = openat (dirfd, path, flags, perm);
|
|
||||||
if (fd >= 0) {
|
|
||||||
FILE *f = fdopen (fd, mode);
|
|
||||||
if (f)
|
|
||||||
return f;
|
|
||||||
close (fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Gets the process information of a given PID
|
* \brief Gets the process information of a given PID
|
||||||
* \ingroup wpprocutils
|
* \ingroup wpprocutils
|
||||||
|
|
@ -172,46 +155,51 @@ WpProcInfo *
|
||||||
wp_proc_utils_get_proc_info (pid_t pid)
|
wp_proc_utils_get_proc_info (pid_t pid)
|
||||||
{
|
{
|
||||||
WpProcInfo *ret = wp_proc_info_new (pid);
|
WpProcInfo *ret = wp_proc_info_new (pid);
|
||||||
char path [64];
|
g_autofree gchar *status = NULL;
|
||||||
spa_autoclose int base_fd = -1;
|
g_autoptr (GError) error = NULL;
|
||||||
FILE *file;
|
gsize length = 0;
|
||||||
g_autofree gchar *line = NULL;
|
|
||||||
size_t size = 0;
|
|
||||||
|
|
||||||
snprintf (path, sizeof(path), "/proc/%d", pid);
|
|
||||||
base_fd = open (path,
|
|
||||||
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY, 0);
|
|
||||||
if (base_fd < 0) {
|
|
||||||
wp_info ("Could not open process info directory %s, skipping", path);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get parent PID */
|
/* Get parent PID */
|
||||||
file = fdopenat (base_fd, "status",
|
{
|
||||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
g_autofree gchar *path = g_strdup_printf ("/proc/%d/status", pid);
|
||||||
if (file) {
|
if (g_file_get_contents (path, &status, &length, &error)) {
|
||||||
while (getline (&line, &size, file) > 1)
|
const gchar *loc = strstr (status, "\nPPid:");
|
||||||
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
|
if (loc) {
|
||||||
break;
|
const gint res = sscanf (loc, "\nPPid:%d\n", &ret->parent);
|
||||||
fclose (file);
|
if (!res || res == EOF)
|
||||||
|
wp_warning ("failed to parse status PPID for PID %d", pid);
|
||||||
|
} else {
|
||||||
|
wp_warning ("failed to find status parent PID for PID %d", pid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wp_warning ("failed to get status for PID %d: %s", pid, error->message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get cgroup */
|
/* Get cgroup */
|
||||||
file = fdopenat (base_fd, "cgroup",
|
{
|
||||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cgroup", pid);
|
||||||
if (file) {
|
if (g_file_get_contents (path, &ret->cgroup, &length, &error)) {
|
||||||
if (getline (&line, &size, file) > 1)
|
if (length > 0)
|
||||||
ret->cgroup = g_strstrip (g_strdup (line));
|
ret->cgroup [length - 1] = '\0'; /* Remove EOF character */
|
||||||
fclose (file);
|
} else {
|
||||||
|
wp_warning ("failed to get cgroup for PID %d: %s", pid, error->message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get args */
|
/* Get args */
|
||||||
file = fdopenat (base_fd, "cmdline",
|
{
|
||||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cmdline", pid);
|
||||||
if (file) {
|
FILE *file = fopen (path, "rb");
|
||||||
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
|
if (file) {
|
||||||
ret->args[ret->n_args++] = g_strdup (line);
|
g_autofree gchar *lineptr = NULL;
|
||||||
fclose (file);
|
size_t size = 0;
|
||||||
|
while (getdelim (&lineptr, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
|
||||||
|
ret->args[ret->n_args++] = g_strdup (lineptr);
|
||||||
|
fclose (file);
|
||||||
|
} else {
|
||||||
|
wp_warning ("failed to get cmdline for PID %d: %m", pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
55
meson.build
55
meson.build
|
|
@ -1,5 +1,5 @@
|
||||||
project('wireplumber', ['c'],
|
project('wireplumber', ['c'],
|
||||||
version : '0.5.14',
|
version : '0.5.12',
|
||||||
license : 'MIT',
|
license : 'MIT',
|
||||||
meson_version : '>= 0.59.0',
|
meson_version : '>= 0.59.0',
|
||||||
default_options : [
|
default_options : [
|
||||||
|
|
@ -88,24 +88,27 @@ if build_modules
|
||||||
error('Specified Lua version "' + lua_version_requested + '" not found')
|
error('Specified Lua version "' + lua_version_requested + '" not found')
|
||||||
endif
|
endif
|
||||||
else
|
else
|
||||||
lua_versions = [
|
lua_dep = dependency('lua-5.4', required: false)
|
||||||
'-5.5', '5.5', '55',
|
if not lua_dep.found()
|
||||||
'-5.4', '5.4', '54',
|
lua_dep = dependency('lua5.4', required: false)
|
||||||
'-5.3', '5.3', '53',
|
endif
|
||||||
]
|
if not lua_dep.found()
|
||||||
|
lua_dep = dependency('lua54', required: false)
|
||||||
foreach v : lua_versions
|
endif
|
||||||
lua_dep = dependency('lua' + v, required: false)
|
if not lua_dep.found()
|
||||||
if lua_dep.found()
|
lua_dep = dependency('lua-5.3', required: false)
|
||||||
break
|
endif
|
||||||
endif
|
if not lua_dep.found()
|
||||||
endforeach
|
lua_dep = dependency('lua5.3', required: false)
|
||||||
|
endif
|
||||||
|
if not lua_dep.found()
|
||||||
|
lua_dep = dependency('lua53', required: false)
|
||||||
|
endif
|
||||||
if not lua_dep.found()
|
if not lua_dep.found()
|
||||||
lua_dep = dependency('lua', version: ['>=5.3.0'], required: false)
|
lua_dep = dependency('lua', version: ['>=5.3.0'], required: false)
|
||||||
endif
|
endif
|
||||||
if not lua_dep.found()
|
if not lua_dep.found()
|
||||||
error ('Could not find lua. Lua version 5.5, 5.4 or 5.3 required')
|
error ('Could not find lua. Lua version 5.4 or 5.3 required')
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
else
|
else
|
||||||
|
|
@ -155,25 +158,7 @@ common_args = [
|
||||||
'-DG_LOG_USE_STRUCTURED',
|
'-DG_LOG_USE_STRUCTURED',
|
||||||
'-DWP_USE_LOCAL_LOG_TOPIC_IN_G_LOG',
|
'-DWP_USE_LOCAL_LOG_TOPIC_IN_G_LOG',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Check if SPA_AUDIO_MAX_CHANNELS can be overridden
|
|
||||||
# (newer headers have #ifndef guards, older ones don't)
|
|
||||||
check_spa_max_channels_override = '''
|
|
||||||
#define SPA_AUDIO_MAX_CHANNELS 128u
|
|
||||||
#include <spa/param/audio/raw.h>
|
|
||||||
void main() { int x = SPA_AUDIO_MAX_CHANNELS; }
|
|
||||||
'''
|
|
||||||
spa_max_channels = 64
|
|
||||||
if cc.compiles(check_spa_max_channels_override,
|
|
||||||
dependencies: spa_dep,
|
|
||||||
args: ['-Werror'],
|
|
||||||
name: 'SPA_AUDIO_MAX_CHANNELS override')
|
|
||||||
common_args += ['-DSPA_AUDIO_MAX_CHANNELS=128u']
|
|
||||||
spa_max_channels = 128
|
|
||||||
endif
|
|
||||||
|
|
||||||
add_project_arguments(common_args, language: 'c')
|
add_project_arguments(common_args, language: 'c')
|
||||||
summary({'SPA_AUDIO_MAX_CHANNELS': spa_max_channels})
|
|
||||||
|
|
||||||
i18n_conf = files()
|
i18n_conf = files()
|
||||||
|
|
||||||
|
|
@ -182,9 +167,7 @@ if build_modules
|
||||||
subdir('modules')
|
subdir('modules')
|
||||||
endif
|
endif
|
||||||
subdir('src')
|
subdir('src')
|
||||||
if build_daemon
|
subdir('po')
|
||||||
subdir('po')
|
|
||||||
endif
|
|
||||||
subdir('docs')
|
subdir('docs')
|
||||||
|
|
||||||
if get_option('tests')
|
if get_option('tests')
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,8 @@ static int
|
||||||
core_get_properties (lua_State *L)
|
core_get_properties (lua_State *L)
|
||||||
{
|
{
|
||||||
WpCore * core = get_wp_core (L);
|
WpCore * core = get_wp_core (L);
|
||||||
WpProperties *p = wp_core_get_properties (core);
|
g_autoptr (WpProperties) p = wp_core_get_properties (core);
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
|
wplua_properties_to_table (L, p);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ static int
|
||||||
core_get_info (lua_State *L)
|
core_get_info (lua_State *L)
|
||||||
{
|
{
|
||||||
WpCore * core = get_wp_core (L);
|
WpCore * core = get_wp_core (L);
|
||||||
WpProperties *p = wp_core_get_remote_properties (core);
|
g_autoptr (WpProperties) p = wp_core_get_remote_properties (core);
|
||||||
|
|
||||||
lua_newtable (L);
|
lua_newtable (L);
|
||||||
lua_pushinteger (L, wp_core_get_remote_cookie (core));
|
lua_pushinteger (L, wp_core_get_remote_cookie (core));
|
||||||
|
|
@ -168,7 +168,7 @@ core_get_info (lua_State *L)
|
||||||
lua_setfield (L, -2, "host_name");
|
lua_setfield (L, -2, "host_name");
|
||||||
lua_pushstring (L, wp_core_get_remote_version (core));
|
lua_pushstring (L, wp_core_get_remote_version (core));
|
||||||
lua_setfield (L, -2, "version");
|
lua_setfield (L, -2, "version");
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
|
wplua_properties_to_table (L, p);
|
||||||
lua_setfield (L, -2, "properties");
|
lua_setfield (L, -2, "properties");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -297,13 +297,8 @@ static int
|
||||||
core_update_properties (lua_State *L)
|
core_update_properties (lua_State *L)
|
||||||
{
|
{
|
||||||
WpCore *core = get_wp_core(L);
|
WpCore *core = get_wp_core(L);
|
||||||
WpProperties *props = NULL;
|
luaL_checktype (L, 1, LUA_TTABLE);
|
||||||
if (lua_istable (L, 1))
|
wp_core_update_properties (core, wplua_table_to_properties (L, 1));
|
||||||
props = wplua_table_to_properties (L, 1);
|
|
||||||
else
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
wp_core_update_properties (core, props);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,28 +599,6 @@ push_wpiterator (lua_State *L, WpIterator *it)
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
iterator_reset (lua_State *L)
|
|
||||||
{
|
|
||||||
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
|
|
||||||
wp_iterator_reset (it);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
iterator_iterate (lua_State *L)
|
|
||||||
{
|
|
||||||
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
|
|
||||||
return push_wpiterator (L, wp_iterator_ref (it));
|
|
||||||
}
|
|
||||||
|
|
||||||
static const luaL_Reg iterator_funcs[] = {
|
|
||||||
{ "next", iterator_next },
|
|
||||||
{ "reset", iterator_reset },
|
|
||||||
{ "iterate", iterator_iterate },
|
|
||||||
{ NULL, NULL }
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Settings WpIterator */
|
/* Settings WpIterator */
|
||||||
|
|
||||||
static int
|
static int
|
||||||
|
|
@ -864,11 +837,7 @@ object_interest_matches (lua_State *L)
|
||||||
matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
|
matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
|
||||||
}
|
}
|
||||||
else if (lua_istable (L, 2)) {
|
else if (lua_istable (L, 2)) {
|
||||||
g_autoptr (WpProperties) props = NULL;
|
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
|
||||||
if (lua_istable (L, 2))
|
|
||||||
props = wplua_table_to_properties (L, 2);
|
|
||||||
else
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
matches = wp_object_interest_matches (interest, props);
|
matches = wp_object_interest_matches (interest, props);
|
||||||
} else
|
} else
|
||||||
luaL_argerror (L, 2, "expected GObject or table");
|
luaL_argerror (L, 2, "expected GObject or table");
|
||||||
|
|
@ -1028,11 +997,10 @@ impl_metadata_new (lua_State *L)
|
||||||
const char *name = luaL_checkstring (L, 1);
|
const char *name = luaL_checkstring (L, 1);
|
||||||
WpProperties *properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
properties = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L),
|
WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L),
|
||||||
name, properties);
|
name, properties);
|
||||||
|
|
@ -1049,11 +1017,10 @@ device_new (lua_State *L)
|
||||||
const char *factory = luaL_checkstring (L, 1);
|
const char *factory = luaL_checkstring (L, 1);
|
||||||
WpProperties *properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
properties = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L),
|
WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L),
|
||||||
factory, properties);
|
factory, properties);
|
||||||
|
|
@ -1070,11 +1037,10 @@ spa_device_new (lua_State *L)
|
||||||
const char *factory = luaL_checkstring (L, 1);
|
const char *factory = luaL_checkstring (L, 1);
|
||||||
WpProperties *properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
properties = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L),
|
WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L),
|
||||||
factory, properties);
|
factory, properties);
|
||||||
|
|
@ -1139,11 +1105,10 @@ node_new (lua_State *L)
|
||||||
const char *factory = luaL_checkstring (L, 1);
|
const char *factory = luaL_checkstring (L, 1);
|
||||||
WpProperties *properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
properties = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
properties = wp_properties_ref (
|
|
||||||
wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpNode *d = wp_node_new_from_factory (get_wp_export_core (L),
|
WpNode *d = wp_node_new_from_factory (get_wp_export_core (L),
|
||||||
factory, properties);
|
factory, properties);
|
||||||
|
|
@ -1249,11 +1214,10 @@ impl_node_new (lua_State *L)
|
||||||
const char *factory = luaL_checkstring (L, 1);
|
const char *factory = luaL_checkstring (L, 1);
|
||||||
WpProperties *properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
properties = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L),
|
WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L),
|
||||||
factory, properties);
|
factory, properties);
|
||||||
|
|
@ -1286,11 +1250,10 @@ link_new (lua_State *L)
|
||||||
const char *factory = luaL_checkstring (L, 1);
|
const char *factory = luaL_checkstring (L, 1);
|
||||||
WpProperties *properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
properties = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties);
|
WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties);
|
||||||
if (l)
|
if (l)
|
||||||
|
|
@ -1366,12 +1329,9 @@ static int
|
||||||
client_update_properties (lua_State *L)
|
client_update_properties (lua_State *L)
|
||||||
{
|
{
|
||||||
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
|
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
|
||||||
WpProperties *properties = NULL;
|
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
WpProperties *properties = wplua_table_to_properties (L, 2);
|
||||||
else
|
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
wp_client_update_properties (client, properties);
|
wp_client_update_properties (client, properties);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -1388,22 +1348,10 @@ client_send_error (lua_State *L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
client_attach_permission_manager (lua_State *L)
|
|
||||||
{
|
|
||||||
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
|
|
||||||
WpPermissionManager *pm =
|
|
||||||
wplua_checkobject (L, 2, WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
|
|
||||||
wp_client_attach_permission_manager (client, pm);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const luaL_Reg client_methods[] = {
|
static const luaL_Reg client_methods[] = {
|
||||||
{ "update_permissions", client_update_permissions },
|
{ "update_permissions", client_update_permissions },
|
||||||
{ "update_properties", client_update_properties },
|
{ "update_properties", client_update_properties },
|
||||||
{ "send_error", client_send_error },
|
{ "send_error", client_send_error },
|
||||||
{ "attach_permission_manager", client_attach_permission_manager },
|
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1443,12 +1391,46 @@ static int
|
||||||
session_item_configure (lua_State *L)
|
session_item_configure (lua_State *L)
|
||||||
{
|
{
|
||||||
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
|
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
|
||||||
WpProperties *props;
|
WpProperties *props = wp_properties_new_empty ();
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
/* validate arguments */
|
||||||
props = wplua_table_to_properties (L, 2);
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
else
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
/* build the configuration properties */
|
||||||
|
lua_pushnil (L);
|
||||||
|
while (lua_next (L, 2)) {
|
||||||
|
const gchar *key = NULL;
|
||||||
|
g_autofree gchar *var = NULL;
|
||||||
|
|
||||||
|
switch (lua_type (L, -1)) {
|
||||||
|
case LUA_TBOOLEAN:
|
||||||
|
var = g_strdup_printf ("%u", lua_toboolean (L, -1));
|
||||||
|
break;
|
||||||
|
case LUA_TNUMBER:
|
||||||
|
if (lua_isinteger (L, -1))
|
||||||
|
var = g_strdup_printf ("%lld", lua_tointeger (L, -1));
|
||||||
|
else
|
||||||
|
var = g_strdup_printf ("%f", lua_tonumber (L, -1));
|
||||||
|
break;
|
||||||
|
case LUA_TSTRING:
|
||||||
|
var = g_strdup (lua_tostring (L, -1));
|
||||||
|
break;
|
||||||
|
case LUA_TUSERDATA: {
|
||||||
|
GValue *v = lua_touserdata (L, -1);
|
||||||
|
gpointer p = g_value_peek_pointer (v);
|
||||||
|
var = g_strdup_printf ("%p", p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
luaL_error (L, "configure does not support lua type ",
|
||||||
|
lua_typename(L, lua_type(L, -1)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = luaL_tolstring (L, -2, NULL);
|
||||||
|
wp_properties_set (props, key, var);
|
||||||
|
lua_pop (L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
lua_pushboolean (L, wp_session_item_configure (si, props));
|
lua_pushboolean (L, wp_session_item_configure (si, props));
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -1470,23 +1452,12 @@ session_item_remove (lua_State *L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
session_item_get_property (lua_State *L)
|
|
||||||
{
|
|
||||||
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
|
|
||||||
const char *key = luaL_checkstring (L, 2);
|
|
||||||
const char *val = wp_session_item_get_property (si, key);
|
|
||||||
lua_pushstring (L, val);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const luaL_Reg session_item_methods[] = {
|
static const luaL_Reg session_item_methods[] = {
|
||||||
{ "get_associated_proxy", session_item_get_associated_proxy },
|
{ "get_associated_proxy", session_item_get_associated_proxy },
|
||||||
{ "reset", session_item_reset },
|
{ "reset", session_item_reset },
|
||||||
{ "configure", session_item_configure },
|
{ "configure", session_item_configure },
|
||||||
{ "register", session_item_register },
|
{ "register", session_item_register },
|
||||||
{ "remove", session_item_remove },
|
{ "remove", session_item_remove },
|
||||||
{ "get_property", session_item_get_property },
|
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1556,24 +1527,19 @@ on_enum_params_done (WpPipewireObject * pwobj, GAsyncResult * res,
|
||||||
GClosure * closure)
|
GClosure * closure)
|
||||||
{
|
{
|
||||||
g_autoptr (GError) error = NULL;
|
g_autoptr (GError) error = NULL;
|
||||||
GValue vals[2] = { G_VALUE_INIT, G_VALUE_INIT };
|
GValue val = G_VALUE_INIT;
|
||||||
int n_vals = 1;
|
int n_vals = 0;
|
||||||
WpIterator *it;
|
WpIterator *it;
|
||||||
|
|
||||||
it = wp_pipewire_object_enum_params_finish (pwobj, res, &error);
|
it = wp_pipewire_object_enum_params_finish (pwobj, res, &error);
|
||||||
g_value_init (&vals[0], WP_TYPE_ITERATOR);
|
|
||||||
g_value_set_boxed (&vals[0], it);
|
|
||||||
if (!it) {
|
if (!it) {
|
||||||
g_value_init (&vals[1], G_TYPE_STRING);
|
g_value_init (&val, G_TYPE_STRING);
|
||||||
g_value_set_string (&vals[1], error->message);
|
g_value_set_string (&val, error->message);
|
||||||
n_vals = 2;
|
n_vals = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_clear_pointer (&it, wp_iterator_unref);
|
g_clear_pointer (&it, wp_iterator_unref);
|
||||||
g_closure_invoke (closure, NULL, n_vals, vals, NULL);
|
g_closure_invoke (closure, NULL, n_vals, &val, NULL);
|
||||||
|
g_value_unset (&val);
|
||||||
g_value_unset (&vals[0]);
|
|
||||||
g_value_unset (&vals[1]);
|
|
||||||
g_closure_invalidate (closure);
|
g_closure_invalidate (closure);
|
||||||
g_closure_unref (closure);
|
g_closure_unref (closure);
|
||||||
}
|
}
|
||||||
|
|
@ -1609,22 +1575,11 @@ pipewire_object_set_param (lua_State *L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
pipewire_object_get_property (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
|
|
||||||
const char *key = luaL_checkstring (L, 2);
|
|
||||||
const char *val = wp_pipewire_object_get_property (pwobj, key);
|
|
||||||
lua_pushstring (L, val);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const luaL_Reg pipewire_object_methods[] = {
|
static const luaL_Reg pipewire_object_methods[] = {
|
||||||
{ "enum_params", pipewire_object_enum_params },
|
{ "enum_params", pipewire_object_enum_params },
|
||||||
{ "iterate_params", pipewire_object_iterate_params },
|
{ "iterate_params", pipewire_object_iterate_params },
|
||||||
{ "set_param" , pipewire_object_set_param },
|
{ "set_param" , pipewire_object_set_param },
|
||||||
{ "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
|
{ "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
|
||||||
{ "get_property", pipewire_object_get_property },
|
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1651,14 +1606,9 @@ static int
|
||||||
state_save (lua_State *L)
|
state_save (lua_State *L)
|
||||||
{
|
{
|
||||||
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
||||||
g_autoptr (WpProperties) props = NULL;
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
|
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
|
||||||
g_autoptr (GError) error = NULL;
|
g_autoptr (GError) error = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
|
||||||
props = wplua_table_to_properties (L, 2);
|
|
||||||
else
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
gboolean saved = wp_state_save (state, props, &error);
|
gboolean saved = wp_state_save (state, props, &error);
|
||||||
lua_pushboolean (L, saved);
|
lua_pushboolean (L, saved);
|
||||||
lua_pushstring (L, error ? error->message : "");
|
lua_pushstring (L, error ? error->message : "");
|
||||||
|
|
@ -1669,13 +1619,8 @@ static int
|
||||||
state_save_after_timeout (lua_State *L)
|
state_save_after_timeout (lua_State *L)
|
||||||
{
|
{
|
||||||
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
||||||
g_autoptr (WpProperties) props = NULL;
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
|
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
|
||||||
if (lua_istable (L, 2))
|
|
||||||
props = wplua_table_to_properties (L, 2);
|
|
||||||
else
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
wp_state_save_after_timeout (state, get_wp_core (L), props);
|
wp_state_save_after_timeout (state, get_wp_core (L), props);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1684,8 +1629,8 @@ static int
|
||||||
state_load (lua_State *L)
|
state_load (lua_State *L)
|
||||||
{
|
{
|
||||||
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
||||||
WpProperties *props = wp_state_load (state);
|
g_autoptr (WpProperties) props = wp_state_load (state);
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
wplua_properties_to_table (L, props);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1703,18 +1648,17 @@ static int
|
||||||
impl_module_new (lua_State *L)
|
impl_module_new (lua_State *L)
|
||||||
{
|
{
|
||||||
const char *name, *args = NULL;
|
const char *name, *args = NULL;
|
||||||
g_autoptr (WpProperties) properties = NULL;
|
WpProperties *properties = NULL;
|
||||||
|
|
||||||
name = luaL_checkstring (L, 1);
|
name = luaL_checkstring (L, 1);
|
||||||
|
|
||||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
|
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
|
||||||
args = luaL_checkstring (L, 2);
|
args = luaL_checkstring (L, 2);
|
||||||
|
|
||||||
if (lua_istable (L, 3))
|
if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, 3, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, 3);
|
properties = wplua_table_to_properties (L, 3);
|
||||||
else if (!lua_isnone (L, 3) && !lua_isnil (L, 3))
|
}
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 3,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
|
WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
|
||||||
name, args, properties);
|
name, args, properties);
|
||||||
|
|
@ -1737,10 +1681,9 @@ conf_new (lua_State *L)
|
||||||
WpProperties *p = NULL;
|
WpProperties *p = NULL;
|
||||||
WpConf *conf = NULL;
|
WpConf *conf = NULL;
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
if (lua_istable (L, 2)) {
|
||||||
p = wplua_table_to_properties (L, 2);
|
p = wplua_table_to_properties (L, 2);
|
||||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
}
|
||||||
p = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
conf = wp_conf_new (path, p);
|
conf = wp_conf_new (path, p);
|
||||||
if (conf) {
|
if (conf) {
|
||||||
|
|
@ -1778,7 +1721,7 @@ conf_get_section_as_properties (lua_State *L)
|
||||||
const char *section = NULL;
|
const char *section = NULL;
|
||||||
g_autoptr (WpConf) conf = NULL;
|
g_autoptr (WpConf) conf = NULL;
|
||||||
g_autoptr (WpSpaJson) s = NULL;
|
g_autoptr (WpSpaJson) s = NULL;
|
||||||
WpProperties *props = NULL;
|
g_autoptr (WpProperties) props = NULL;
|
||||||
int argi = 1;
|
int argi = 1;
|
||||||
|
|
||||||
/* check if called as method on object */
|
/* check if called as method on object */
|
||||||
|
|
@ -1793,8 +1736,6 @@ conf_get_section_as_properties (lua_State *L)
|
||||||
|
|
||||||
if (lua_istable (L, argi))
|
if (lua_istable (L, argi))
|
||||||
props = wplua_table_to_properties (L, argi);
|
props = wplua_table_to_properties (L, argi);
|
||||||
else if (!lua_isnone (L, argi) && !lua_isnil (L, argi))
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, argi, WP_TYPE_PROPERTIES));
|
|
||||||
else
|
else
|
||||||
props = wp_properties_new_empty ();
|
props = wp_properties_new_empty ();
|
||||||
|
|
||||||
|
|
@ -1803,7 +1744,7 @@ conf_get_section_as_properties (lua_State *L)
|
||||||
if (s && wp_spa_json_is_object (s))
|
if (s && wp_spa_json_is_object (s))
|
||||||
wp_properties_update_from_json (props, s);
|
wp_properties_update_from_json (props, s);
|
||||||
}
|
}
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
wplua_properties_to_table (L, props);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1960,12 +1901,10 @@ json_utils_match_rules (lua_State *L)
|
||||||
gboolean res;
|
gboolean res;
|
||||||
|
|
||||||
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
|
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
luaL_checktype (L, 3, LUA_TFUNCTION);
|
luaL_checktype (L, 3, LUA_TFUNCTION);
|
||||||
|
|
||||||
if (lua_istable (L, 2))
|
properties = wplua_table_to_properties (L, 2);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
|
||||||
else
|
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
|
res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
|
||||||
L, &error);
|
L, &error);
|
||||||
|
|
@ -1981,21 +1920,17 @@ json_utils_match_rules (lua_State *L)
|
||||||
static int
|
static int
|
||||||
json_utils_match_rules_update_properties (lua_State *L)
|
json_utils_match_rules_update_properties (lua_State *L)
|
||||||
{
|
{
|
||||||
WpProperties *properties = NULL;
|
g_autoptr (WpProperties) properties = NULL;
|
||||||
WpSpaJson *json;
|
WpSpaJson *json;
|
||||||
int count;
|
int count;
|
||||||
|
|
||||||
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
|
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
|
||||||
|
luaL_checktype (L, 2, LUA_TTABLE);
|
||||||
if (lua_istable (L, 2))
|
properties = wplua_table_to_properties (L, 2);
|
||||||
properties = wplua_table_to_properties (L, 2);
|
|
||||||
else
|
|
||||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
|
||||||
WP_TYPE_PROPERTIES));
|
|
||||||
|
|
||||||
count = wp_json_utils_match_rules_update_properties (json, properties);
|
count = wp_json_utils_match_rules_update_properties (json, properties);
|
||||||
|
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, properties);
|
wplua_properties_to_table (L, properties);
|
||||||
lua_pushinteger (L, count);
|
lua_pushinteger (L, count);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
@ -2077,108 +2012,6 @@ static const luaL_Reg proc_utils_funcs[] = {
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Properties */
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_new (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props;
|
|
||||||
|
|
||||||
if (lua_istable (L, 1))
|
|
||||||
props = wplua_table_to_properties (L, 1);
|
|
||||||
else if (!lua_isnone (L, 1) && !lua_isnil (L, 1))
|
|
||||||
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
|
|
||||||
else
|
|
||||||
props = wp_properties_new_empty ();
|
|
||||||
|
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_get_boolean (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
const char *key = luaL_checkstring (L, 2);
|
|
||||||
const char *val = wp_properties_get (props, key);
|
|
||||||
if (val)
|
|
||||||
lua_pushboolean (L, spa_atob (val));
|
|
||||||
else
|
|
||||||
lua_pushnil (L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_get_int (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
const char *key = luaL_checkstring (L, 2);
|
|
||||||
const char *val = wp_properties_get (props, key);
|
|
||||||
if (val) {
|
|
||||||
gint64 int_val = 0;
|
|
||||||
if (spa_atoi64 (val, &int_val, 10))
|
|
||||||
lua_pushinteger (L, int_val);
|
|
||||||
else
|
|
||||||
lua_pushnil (L);
|
|
||||||
} else {
|
|
||||||
lua_pushnil (L);
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_get_float (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
const char *key = luaL_checkstring (L, 2);
|
|
||||||
const char *val = wp_properties_get (props, key);
|
|
||||||
if (val) {
|
|
||||||
double d_val = 0;
|
|
||||||
if (spa_atod (val, &d_val))
|
|
||||||
lua_pushnumber (L, d_val);
|
|
||||||
else
|
|
||||||
lua_pushnil (L);
|
|
||||||
} else {
|
|
||||||
lua_pushnil (L);
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_get_count (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
lua_pushinteger (L, wp_properties_get_count (props));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_copy (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
WpProperties *copy = wp_properties_copy (props);
|
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, copy);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_parse (lua_State *L)
|
|
||||||
{
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
wplua_properties_to_table (L, props);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const luaL_Reg properties_funcs[] = {
|
|
||||||
{ "get_boolean", properties_get_boolean },
|
|
||||||
{ "get_int", properties_get_int },
|
|
||||||
{ "get_float", properties_get_float },
|
|
||||||
{ "get_count", properties_get_count },
|
|
||||||
{ "copy", properties_copy },
|
|
||||||
{ "parse", properties_parse },
|
|
||||||
{ NULL, NULL }
|
|
||||||
};
|
|
||||||
|
|
||||||
/* WpSettings */
|
/* WpSettings */
|
||||||
|
|
||||||
static int
|
static int
|
||||||
|
|
@ -2472,8 +2305,8 @@ static int
|
||||||
event_get_properties (lua_State *L)
|
event_get_properties (lua_State *L)
|
||||||
{
|
{
|
||||||
WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
|
WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
|
||||||
WpProperties *props = wp_event_get_properties (event);
|
g_autoptr (WpProperties) props = wp_event_get_properties (event);
|
||||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
wplua_properties_to_table (L, props);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2595,11 +2428,10 @@ event_dispatcher_push_event (lua_State *L)
|
||||||
lua_pop (L, 1);
|
lua_pop (L, 1);
|
||||||
|
|
||||||
lua_pushliteral (L, "properties");
|
lua_pushliteral (L, "properties");
|
||||||
if (lua_istable (L, -1))
|
if (lua_gettable (L, 1) != LUA_TNIL) {
|
||||||
|
luaL_checktype (L, -1, LUA_TTABLE);
|
||||||
properties = wplua_table_to_properties (L, -1);
|
properties = wplua_table_to_properties (L, -1);
|
||||||
else if (!lua_isnil (L, -1) && !lua_isnone (L, -1) && !lua_isstring (L, -1))
|
}
|
||||||
properties = wp_properties_ref (
|
|
||||||
wplua_checkboxed (L, -1, WP_TYPE_PROPERTIES));
|
|
||||||
lua_pop (L, 1);
|
lua_pop (L, 1);
|
||||||
|
|
||||||
lua_pushliteral (L, "source");
|
lua_pushliteral (L, "source");
|
||||||
|
|
@ -2629,132 +2461,6 @@ static const luaL_Reg event_dispatcher_funcs[] = {
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
/* WpPermissionManager */
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_new (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wp_permission_manager_new (get_wp_core (L));
|
|
||||||
wplua_pushobject (L, pm);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_set_default_permissions (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1,
|
|
||||||
WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
guint32 perms = PW_PERM_ALL;
|
|
||||||
|
|
||||||
if (lua_isinteger (L, 2)) {
|
|
||||||
perms = luaL_checkinteger (L, 2);
|
|
||||||
} else if (lua_isstring (L, 2)) {
|
|
||||||
const gchar *perms_str = luaL_checkstring (L, 2);
|
|
||||||
if (!client_parse_permissions (perms_str, &perms))
|
|
||||||
luaL_error (L, "invalid permission string: '%s'", perms_str);
|
|
||||||
} else {
|
|
||||||
luaL_error (L, "invalid permission argument");
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_permission_manager_set_default_permissions (pm, perms);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_set_core_permissions (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1,
|
|
||||||
WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
guint32 perms = PW_PERM_ALL;
|
|
||||||
|
|
||||||
if (lua_isinteger (L, 2)) {
|
|
||||||
perms = luaL_checkinteger (L, 2);
|
|
||||||
} else if (lua_isstring (L, 2)) {
|
|
||||||
const gchar *perms_str = luaL_checkstring (L, 2);
|
|
||||||
if (!client_parse_permissions (perms_str, &perms))
|
|
||||||
luaL_error (L, "invalid permission string: '%s'", perms_str);
|
|
||||||
} else {
|
|
||||||
luaL_error (L, "invalid permission argument");
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_permission_manager_set_core_permissions (pm, perms);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_add_interest_match (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1,
|
|
||||||
WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
GClosure * closure = wplua_function_to_closure (L, 2);
|
|
||||||
WpObjectInterest *interest = wplua_checkboxed (L, 3, WP_TYPE_OBJECT_INTEREST);
|
|
||||||
guint32 id;
|
|
||||||
|
|
||||||
id = wp_permission_manager_add_interest_match_closure (pm, closure,
|
|
||||||
wp_object_interest_ref (interest));
|
|
||||||
lua_pushinteger (L, id);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_add_interest_match_simple (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1,
|
|
||||||
WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
guint32 perms = luaL_checkinteger (L, 2);
|
|
||||||
WpObjectInterest *interest = wplua_checkboxed (L, 3, WP_TYPE_OBJECT_INTEREST);
|
|
||||||
guint32 id;
|
|
||||||
|
|
||||||
id = wp_permission_manager_add_interest_match_simple (pm, perms,
|
|
||||||
wp_object_interest_ref (interest));
|
|
||||||
lua_pushinteger (L, id);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_add_rules_match (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1, WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
WpSpaJson *rules = wplua_checkboxed (L, 2, WP_TYPE_SPA_JSON);
|
|
||||||
guint32 id;
|
|
||||||
|
|
||||||
id = wp_permission_manager_add_rules_match (pm, wp_spa_json_ref (rules));
|
|
||||||
lua_pushinteger (L, id);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_remove_match (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1,
|
|
||||||
WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
guint interest_id = luaL_checkinteger (L, 2);
|
|
||||||
|
|
||||||
wp_permission_manager_remove_match (pm, interest_id);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
permission_manager_update_permissions (lua_State *L)
|
|
||||||
{
|
|
||||||
WpPermissionManager *pm = wplua_checkobject (L, 1,
|
|
||||||
WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
|
|
||||||
wp_permission_manager_update_permissions (pm);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const luaL_Reg permission_manager_funcs[] = {
|
|
||||||
{ "set_default_permissions", permission_manager_set_default_permissions },
|
|
||||||
{ "set_core_permissions", permission_manager_set_core_permissions },
|
|
||||||
{ "add_interest_match", permission_manager_add_interest_match },
|
|
||||||
{ "add_interest_match_simple", permission_manager_add_interest_match_simple },
|
|
||||||
{ "add_rules_match", permission_manager_add_rules_match },
|
|
||||||
{ "remove_match", permission_manager_remove_match },
|
|
||||||
{ "update_permissions", permission_manager_update_permissions },
|
|
||||||
{ NULL, NULL }
|
|
||||||
};
|
|
||||||
|
|
||||||
/* WpEventHook */
|
/* WpEventHook */
|
||||||
|
|
||||||
static int
|
static int
|
||||||
|
|
@ -3278,12 +2984,6 @@ wp_lua_scripting_api_init (lua_State *L)
|
||||||
conf_new, conf_methods);
|
conf_new, conf_methods);
|
||||||
wplua_register_type_methods (L, WP_TYPE_PROC_INFO,
|
wplua_register_type_methods (L, WP_TYPE_PROC_INFO,
|
||||||
NULL, proc_info_funcs);
|
NULL, proc_info_funcs);
|
||||||
wplua_register_type_methods (L, WP_TYPE_ITERATOR,
|
|
||||||
NULL, iterator_funcs);
|
|
||||||
wplua_register_type_methods (L, WP_TYPE_PROPERTIES,
|
|
||||||
properties_new, properties_funcs);
|
|
||||||
wplua_register_type_methods (L, WP_TYPE_PERMISSION_MANAGER,
|
|
||||||
permission_manager_new, permission_manager_funcs);
|
|
||||||
|
|
||||||
if (!wplua_load_uri (L, URI_API, &error) ||
|
if (!wplua_load_uri (L, URI_API, &error) ||
|
||||||
!wplua_pcall (L, 0, 0, &error)) {
|
!wplua_pcall (L, 0, 0, &error)) {
|
||||||
|
|
|
||||||
|
|
@ -184,28 +184,6 @@ local Feature = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
PERM_R_VAL = 0400
|
|
||||||
PERM_W_VAL = 0200
|
|
||||||
PERM_X_VAL = 0100
|
|
||||||
PERM_M_VAL = 0010
|
|
||||||
PERM_L_VAL = 0020
|
|
||||||
|
|
||||||
local Perm = {
|
|
||||||
NONE = 0,
|
|
||||||
R = PERM_R_VAL,
|
|
||||||
W = PERM_W_VAL,
|
|
||||||
X = PERM_X_VAL,
|
|
||||||
M = PERM_M_VAL,
|
|
||||||
L = PERM_L_VAL,
|
|
||||||
RW = (PERM_R_VAL | PERM_W_VAL),
|
|
||||||
RX = (PERM_R_VAL | PERM_X_VAL),
|
|
||||||
WX = (PERM_W_VAL | PERM_X_VAL),
|
|
||||||
RWX = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL),
|
|
||||||
RWXM = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL | PERM_M_VAL),
|
|
||||||
RWXML = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL | PERM_M_VAL | PERM_L_VAL),
|
|
||||||
ALL = (PERM_R_VAL | PERM_W_VAL | PERM_X_VAL | PERM_M_VAL),
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Allow calling Conf() to instantiate a new WpConf
|
-- Allow calling Conf() to instantiate a new WpConf
|
||||||
WpConf["__new"] = WpConf_new
|
WpConf["__new"] = WpConf_new
|
||||||
|
|
||||||
|
|
@ -214,7 +192,6 @@ SANDBOX_EXPORT = {
|
||||||
Id = Id,
|
Id = Id,
|
||||||
Features = Features,
|
Features = Features,
|
||||||
Feature = Feature,
|
Feature = Feature,
|
||||||
Perm = Perm,
|
|
||||||
GLib = GLib,
|
GLib = GLib,
|
||||||
I18n = I18n,
|
I18n = I18n,
|
||||||
Log = WpLog,
|
Log = WpLog,
|
||||||
|
|
@ -240,8 +217,6 @@ SANDBOX_EXPORT = {
|
||||||
Conf = WpConf,
|
Conf = WpConf,
|
||||||
JsonUtils = JsonUtils,
|
JsonUtils = JsonUtils,
|
||||||
ProcUtils = ProcUtils,
|
ProcUtils = ProcUtils,
|
||||||
Properties = WpProperties_new,
|
|
||||||
PermissionManager = WpPermissionManager_new,
|
|
||||||
SimpleEventHook = WpSimpleEventHook_new,
|
SimpleEventHook = WpSimpleEventHook_new,
|
||||||
AsyncEventHook = WpAsyncEventHook_new,
|
AsyncEventHook = WpAsyncEventHook_new,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -300,54 +300,40 @@ spa_json_object_new (lua_State *L)
|
||||||
{
|
{
|
||||||
g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object ();
|
g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object ();
|
||||||
|
|
||||||
if (lua_istable (L, 1)) {
|
luaL_checktype (L, 1, LUA_TTABLE);
|
||||||
luaL_checktype (L, 1, LUA_TTABLE);
|
|
||||||
|
|
||||||
lua_pushnil (L);
|
lua_pushnil (L);
|
||||||
while (lua_next (L, -2)) {
|
while (lua_next (L, -2)) {
|
||||||
/* We only add table values with string keys */
|
/* We only add table values with string keys */
|
||||||
if (lua_type (L, -2) == LUA_TSTRING) {
|
if (lua_type (L, -2) == LUA_TSTRING) {
|
||||||
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
|
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
|
||||||
|
|
||||||
switch (lua_type (L, -1)) {
|
switch (lua_type (L, -1)) {
|
||||||
case LUA_TBOOLEAN:
|
case LUA_TBOOLEAN:
|
||||||
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
|
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
|
||||||
break;
|
break;
|
||||||
case LUA_TNUMBER:
|
case LUA_TNUMBER:
|
||||||
if (lua_isinteger (L, -1))
|
if (lua_isinteger (L, -1))
|
||||||
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
|
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
|
||||||
else
|
else
|
||||||
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
|
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
|
||||||
break;
|
break;
|
||||||
case LUA_TSTRING:
|
case LUA_TSTRING:
|
||||||
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
|
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
|
||||||
break;
|
break;
|
||||||
case LUA_TUSERDATA: {
|
case LUA_TUSERDATA: {
|
||||||
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
|
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
|
||||||
wp_spa_json_builder_add_json (builder, json);
|
wp_spa_json_builder_add_json (builder, json);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
default:
|
|
||||||
luaL_error (L, "Json does not support lua type %s",
|
|
||||||
lua_typename(L, lua_type(L, -1)));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
luaL_error (L, "Json does not support lua type %s",
|
||||||
|
lua_typename(L, lua_type(L, -1)));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lua_pop (L, 1);
|
lua_pop (L, 1);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
|
|
||||||
g_autoptr (WpIterator) it = NULL;
|
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
|
||||||
for (it = wp_properties_new_iterator (props); wp_iterator_next (it, &item);
|
|
||||||
g_value_unset (&item)) {
|
|
||||||
WpPropertiesItem *pi = g_value_get_boxed (&item);
|
|
||||||
const gchar *key = wp_properties_item_get_key (pi);
|
|
||||||
const gchar *value = wp_properties_item_get_value (pi);
|
|
||||||
wp_spa_json_builder_add_property (builder, key);
|
|
||||||
wp_spa_json_builder_add_string (builder, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));
|
wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,8 @@ _wplua_gboxed___index (lua_State *L)
|
||||||
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
|
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
|
||||||
luaL_argcheck (L, obj_v != NULL, 1,
|
luaL_argcheck (L, obj_v != NULL, 1,
|
||||||
"expected userdata storing GValue<GBoxed>");
|
"expected userdata storing GValue<GBoxed>");
|
||||||
const gchar *key = luaL_tolstring (L, 2, NULL);
|
const gchar *key = luaL_checkstring (L, 2);
|
||||||
GType type = G_VALUE_TYPE (obj_v);
|
GType type = G_VALUE_TYPE (obj_v);
|
||||||
GType boxed_type = type;
|
|
||||||
lua_CFunction func = NULL;
|
lua_CFunction func = NULL;
|
||||||
GHashTable *vtables;
|
GHashTable *vtables;
|
||||||
|
|
||||||
|
|
@ -54,104 +53,6 @@ _wplua_gboxed___index (lua_State *L)
|
||||||
lua_pushcfunction (L, func);
|
lua_pushcfunction (L, func);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If WpProperties type, just return the property value for that key */
|
|
||||||
if (boxed_type == WP_TYPE_PROPERTIES) {
|
|
||||||
WpProperties * props = g_value_get_boxed (obj_v);
|
|
||||||
const gchar *val = wp_properties_get (props, key);
|
|
||||||
lua_pushstring (L, val);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
_wplua_gboxed___newindex (lua_State *L)
|
|
||||||
{
|
|
||||||
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
|
|
||||||
luaL_argcheck (L, obj_v != NULL, 1,
|
|
||||||
"expected userdata storing GValue<GBoxed>");
|
|
||||||
const gchar *key = luaL_tolstring (L, 2, NULL);
|
|
||||||
GType type = G_VALUE_TYPE (obj_v);
|
|
||||||
|
|
||||||
/* Set property value */
|
|
||||||
if (type == WP_TYPE_PROPERTIES) {
|
|
||||||
WpProperties * props = g_value_dup_boxed (obj_v);
|
|
||||||
g_autofree gchar *val = NULL;
|
|
||||||
luaL_checkany (L, 3);
|
|
||||||
|
|
||||||
switch (lua_type (L, 3)) {
|
|
||||||
case LUA_TNIL:
|
|
||||||
break;
|
|
||||||
case LUA_TUSERDATA: {
|
|
||||||
if (wplua_gvalue_userdata_type (L, 3) != G_TYPE_INVALID) {
|
|
||||||
GValue *v = lua_touserdata (L, 3);
|
|
||||||
gpointer p = g_value_peek_pointer (v);
|
|
||||||
val = g_strdup_printf ("%p", p);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
val = g_strdup (luaL_tolstring (L, 3, NULL));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
val = g_strdup (luaL_tolstring (L, 3, NULL));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
props = wp_properties_ensure_unique_owner (props);
|
|
||||||
wp_properties_set (props, key, val);
|
|
||||||
g_value_take_boxed (obj_v, props);
|
|
||||||
} else {
|
|
||||||
luaL_error (L, "cannot assign property '%s' to boxed type %s",
|
|
||||||
key, g_type_name (type));
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
properties_iterator_next (lua_State *L)
|
|
||||||
{
|
|
||||||
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
|
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
|
||||||
if (wp_iterator_next (it, &item)) {
|
|
||||||
WpPropertiesItem *si = g_value_get_boxed (&item);
|
|
||||||
const gchar *k = wp_properties_item_get_key (si);
|
|
||||||
const gchar *v = wp_properties_item_get_value (si);
|
|
||||||
lua_pushstring (L, k);
|
|
||||||
lua_pushstring (L, v);
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
lua_pushnil (L);
|
|
||||||
lua_pushnil (L);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
push_properties_wpiterator (lua_State *L, WpIterator *it)
|
|
||||||
{
|
|
||||||
lua_pushcfunction (L, properties_iterator_next);
|
|
||||||
wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
_wplua_gboxed___pairs (lua_State *L)
|
|
||||||
{
|
|
||||||
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
|
|
||||||
luaL_argcheck (L, obj_v != NULL, 1,
|
|
||||||
"expected userdata storing GValue<GBoxed>");
|
|
||||||
GType type = G_VALUE_TYPE (obj_v);
|
|
||||||
|
|
||||||
if (type == WP_TYPE_PROPERTIES) {
|
|
||||||
WpProperties * props = g_value_get_boxed (obj_v);
|
|
||||||
WpIterator *it = wp_properties_new_iterator (props);
|
|
||||||
return push_properties_wpiterator (L, it);
|
|
||||||
} else {
|
|
||||||
luaL_error (L, "cannot do pairs of boxed type %s", g_type_name (type));
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,8 +69,6 @@ _wplua_init_gboxed (lua_State *L)
|
||||||
{ "__gc", _wplua_gvalue_userdata___gc },
|
{ "__gc", _wplua_gvalue_userdata___gc },
|
||||||
{ "__eq", _wplua_gboxed___eq },
|
{ "__eq", _wplua_gboxed___eq },
|
||||||
{ "__index", _wplua_gboxed___index },
|
{ "__index", _wplua_gboxed___index },
|
||||||
{ "__newindex", _wplua_gboxed___newindex },
|
|
||||||
{ "__pairs", _wplua_gboxed___pairs },
|
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ WpProperties *
|
||||||
wplua_table_to_properties (lua_State *L, int idx)
|
wplua_table_to_properties (lua_State *L, int idx)
|
||||||
{
|
{
|
||||||
WpProperties *p = wp_properties_new_empty ();
|
WpProperties *p = wp_properties_new_empty ();
|
||||||
|
const gchar *key, *value;
|
||||||
int table = lua_absindex (L, idx);
|
int table = lua_absindex (L, idx);
|
||||||
|
|
||||||
if (lua_type (L, table) != LUA_TTABLE) {
|
if (lua_type (L, table) != LUA_TTABLE) {
|
||||||
|
|
@ -23,34 +24,11 @@ wplua_table_to_properties (lua_State *L, int idx)
|
||||||
|
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
while (lua_next (L, table) != 0) {
|
while (lua_next (L, table) != 0) {
|
||||||
const gchar *key = luaL_tolstring (L, -2, NULL);
|
|
||||||
g_autofree gchar *value = NULL;
|
|
||||||
|
|
||||||
/* copy key & value to convert them to string */
|
/* copy key & value to convert them to string */
|
||||||
luaL_checkany (L, -2);
|
key = luaL_tolstring (L, -2, NULL);
|
||||||
switch (lua_type (L, -2)) {
|
value = luaL_tolstring (L, -2, NULL);
|
||||||
case LUA_TNIL:
|
|
||||||
lua_pop (L, 2);
|
|
||||||
break;
|
|
||||||
case LUA_TUSERDATA: {
|
|
||||||
if (wplua_gvalue_userdata_type(L, -2) != G_TYPE_INVALID) {
|
|
||||||
GValue *v = lua_touserdata (L, -2);
|
|
||||||
gpointer p = g_value_peek_pointer (v);
|
|
||||||
value = g_strdup_printf ("%p", p);
|
|
||||||
lua_pop (L, 2);
|
|
||||||
} else {
|
|
||||||
value = g_strdup (luaL_tolstring (L, -2, NULL));
|
|
||||||
lua_pop (L, 3);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
value = g_strdup (luaL_tolstring (L, -2, NULL));
|
|
||||||
lua_pop (L, 3);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_properties_set (p, key, value);
|
wp_properties_set (p, key, value);
|
||||||
|
lua_pop (L, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sort, because the lua table has a random order and it's too messy to read */
|
/* sort, because the lua table has a random order and it's too messy to read */
|
||||||
|
|
@ -335,7 +313,10 @@ wplua_gvalue_to_lua (lua_State *L, const GValue *v)
|
||||||
lua_pushlightuserdata (L, g_value_get_pointer (v));
|
lua_pushlightuserdata (L, g_value_get_pointer (v));
|
||||||
break;
|
break;
|
||||||
case G_TYPE_BOXED:
|
case G_TYPE_BOXED:
|
||||||
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
|
if (G_VALUE_TYPE (v) == WP_TYPE_PROPERTIES)
|
||||||
|
wplua_properties_to_table (L, g_value_get_boxed (v));
|
||||||
|
else
|
||||||
|
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
|
||||||
break;
|
break;
|
||||||
case G_TYPE_OBJECT:
|
case G_TYPE_OBJECT:
|
||||||
case G_TYPE_INTERFACE: {
|
case G_TYPE_INTERFACE: {
|
||||||
|
|
|
||||||
|
|
@ -103,15 +103,14 @@ static void
|
||||||
bind_call (GObject * obj, GAsyncResult * res, gpointer data)
|
bind_call (GObject * obj, GAsyncResult * res, gpointer data)
|
||||||
{
|
{
|
||||||
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
|
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
|
||||||
g_autoptr (GError) err = NULL;
|
GError *err = NULL;
|
||||||
GDBusProxy *call;
|
GDBusProxy *call;
|
||||||
g_autoptr (GVariant) prop = NULL;
|
GVariant *prop;
|
||||||
gint init_state;
|
gint init_state;
|
||||||
|
|
||||||
call = g_dbus_proxy_new_finish (res, &err);
|
call = g_dbus_proxy_new_finish (res, &err);
|
||||||
if (call == NULL) {
|
if (call == NULL) {
|
||||||
g_prefix_error (&err, "Failed to get call: ");
|
wp_warning_object (wpmm, "Failed to get call");
|
||||||
wp_warning_object (wpmm, "%s", err->message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +122,8 @@ bind_call (GObject * obj, GAsyncResult * res, gpointer data)
|
||||||
|
|
||||||
if (is_active_state (init_state))
|
if (is_active_state (init_state))
|
||||||
active_calls_inc (wpmm);
|
active_calls_inc (wpmm);
|
||||||
|
|
||||||
|
g_variant_unref (prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
wpmm->calls = g_list_prepend (wpmm->calls, call);
|
wpmm->calls = g_list_prepend (wpmm->calls, call);
|
||||||
|
|
@ -164,7 +165,7 @@ on_voice_signal (GDBusProxy * iface,
|
||||||
g_object_get (wpmm->dbus, "connection", &conn, NULL);
|
g_object_get (wpmm->dbus, "connection", &conn, NULL);
|
||||||
|
|
||||||
if (!g_strcmp0 (signal, "CallAdded")) {
|
if (!g_strcmp0 (signal, "CallAdded")) {
|
||||||
g_variant_get (params, "(&o)", &path);
|
g_variant_get (params, "(o)", &path);
|
||||||
g_dbus_proxy_new (conn,
|
g_dbus_proxy_new (conn,
|
||||||
G_DBUS_PROXY_FLAGS_NONE,
|
G_DBUS_PROXY_FLAGS_NONE,
|
||||||
NULL,
|
NULL,
|
||||||
|
|
@ -174,8 +175,9 @@ on_voice_signal (GDBusProxy * iface,
|
||||||
NULL,
|
NULL,
|
||||||
bind_call,
|
bind_call,
|
||||||
wpmm);
|
wpmm);
|
||||||
|
g_free (path);
|
||||||
} else if (!g_strcmp0 (signal, "CallDeleted")) {
|
} else if (!g_strcmp0 (signal, "CallDeleted")) {
|
||||||
g_variant_get (params, "(&o)", &path);
|
g_variant_get (params, "(o)", &path);
|
||||||
|
|
||||||
// The user shouldn't have hundreds of calls, so just linear search.
|
// The user shouldn't have hundreds of calls, so just linear search.
|
||||||
deleted = g_list_find_custom (wpmm->calls, path, match_call_path);
|
deleted = g_list_find_custom (wpmm->calls, path, match_call_path);
|
||||||
|
|
@ -183,6 +185,8 @@ on_voice_signal (GDBusProxy * iface,
|
||||||
g_object_unref (deleted->data);
|
g_object_unref (deleted->data);
|
||||||
wpmm->calls = g_list_delete_link (wpmm->calls, deleted);
|
wpmm->calls = g_list_delete_link (wpmm->calls, deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g_free (path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,23 +196,22 @@ list_calls_done (GObject * obj,
|
||||||
gpointer data)
|
gpointer data)
|
||||||
{
|
{
|
||||||
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
|
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
|
||||||
g_autoptr (GVariant) params = NULL;
|
GVariant *params;
|
||||||
g_autoptr (GVariantIter) calls = NULL;
|
GVariantIter *calls;
|
||||||
gchar *path;
|
gchar *path;
|
||||||
g_autoptr (GError) err = NULL;
|
GError *err = NULL;
|
||||||
g_autoptr (GDBusConnection) conn = NULL;
|
g_autoptr (GDBusConnection) conn = NULL;
|
||||||
|
|
||||||
params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err);
|
params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err);
|
||||||
if (params == NULL) {
|
if (params == NULL) {
|
||||||
g_prefix_error (&err, "Failed to list active calls on startup: ");
|
g_prefix_error (&err, "Failed to list active calls on startup: ");
|
||||||
wp_warning_object (wpmm, "%s", err->message);
|
wp_warning_object (wpmm, "%s", err->message);
|
||||||
|
g_clear_object (&err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_object_get (wpmm->dbus, "connection", &conn, NULL);
|
|
||||||
|
|
||||||
g_variant_get (params, "(ao)", &calls);
|
g_variant_get (params, "(ao)", &calls);
|
||||||
while (g_variant_iter_loop (calls, "&o", &path)) {
|
while (g_variant_iter_loop (calls, "o", &path)) {
|
||||||
g_dbus_proxy_new (conn,
|
g_dbus_proxy_new (conn,
|
||||||
G_DBUS_PROXY_FLAGS_NONE,
|
G_DBUS_PROXY_FLAGS_NONE,
|
||||||
NULL,
|
NULL,
|
||||||
|
|
@ -219,6 +222,9 @@ list_calls_done (GObject * obj,
|
||||||
bind_call,
|
bind_call,
|
||||||
wpmm);
|
wpmm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g_variant_iter_free (calls);
|
||||||
|
g_variant_unref (params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -347,7 +353,7 @@ static void
|
||||||
wp_modem_manager_enable (WpPlugin * self, WpTransition * transition)
|
wp_modem_manager_enable (WpPlugin * self, WpTransition * transition)
|
||||||
{
|
{
|
||||||
WpModemManager *wpmm = WP_MODEM_MANAGER (self);
|
WpModemManager *wpmm = WP_MODEM_MANAGER (self);
|
||||||
g_autoptr (WpCore) core = NULL;
|
WpCore *core;
|
||||||
GError *err;
|
GError *err;
|
||||||
g_autoptr (GDBusConnection) conn = NULL;
|
g_autoptr (GDBusConnection) conn = NULL;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,8 @@ static void item_free (gpointer data)
|
||||||
{
|
{
|
||||||
Item *item = data;
|
Item *item = data;
|
||||||
|
|
||||||
g_clear_pointer (&item->desktop_entry, g_free);
|
free(item->desktop_entry);
|
||||||
g_clear_pointer (&item->flatpak_app_id, g_free);
|
free(item);
|
||||||
g_clear_pointer (&item->flatpak_instance_id, g_free);
|
|
||||||
g_free (item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Players *players_new (GDBusConnection *conn)
|
static Players *players_new (GDBusConnection *conn)
|
||||||
|
|
@ -130,7 +128,7 @@ static void players_unref (Players *players)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
g_mutex_clear (&players->lock);
|
g_mutex_clear (&players->lock);
|
||||||
g_clear_pointer (&players->items, g_hash_table_unref);
|
g_clear_object (&players->items);
|
||||||
g_clear_object (&players->conn);
|
g_clear_object (&players->conn);
|
||||||
g_clear_object (&players->cancellable);
|
g_clear_object (&players->cancellable);
|
||||||
g_free (players);
|
g_free (players);
|
||||||
|
|
@ -238,7 +236,7 @@ static void item_desktop_entry_cb (GObject *source_object, GAsyncResult* res, gp
|
||||||
}
|
}
|
||||||
|
|
||||||
g_variant_get (result, "(v)", &value);
|
g_variant_get (result, "(v)", &value);
|
||||||
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
|
if (!g_str_equal(g_variant_get_type_string (value), "s")) {
|
||||||
wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name);
|
wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -428,8 +426,6 @@ wp_mpris_plugin_operation_finalize (GObject *object)
|
||||||
WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object);
|
WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object);
|
||||||
|
|
||||||
g_clear_object (&self->conn);
|
g_clear_object (&self->conn);
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_mpris_plugin_operation_parent_class)->finalize (object);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -533,7 +529,9 @@ wp_mpris_plugin_disable (WpPlugin * plugin)
|
||||||
static gpointer
|
static gpointer
|
||||||
wp_mpris_plugin_get_players (WpMprisPlugin *self)
|
wp_mpris_plugin_get_players (WpMprisPlugin *self)
|
||||||
{
|
{
|
||||||
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("av"));
|
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
|
||||||
|
|
||||||
|
g_variant_builder_init (&b, G_VARIANT_TYPE ("av"));
|
||||||
|
|
||||||
if (self->players) {
|
if (self->players) {
|
||||||
g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock);
|
g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,10 @@ si_audio_adapter_get_default_clock_rate (WpSiAudioAdapter * self)
|
||||||
static gboolean
|
static gboolean
|
||||||
is_unpositioned (struct spa_audio_info_raw *info)
|
is_unpositioned (struct spa_audio_info_raw *info)
|
||||||
{
|
{
|
||||||
uint32_t i, n_pos;
|
uint32_t i;
|
||||||
if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
|
if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
|
||||||
return TRUE;
|
return TRUE;
|
||||||
n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
|
for (i = 0; i < info->channels; i++)
|
||||||
for (i = 0; i < n_pos; i++)
|
|
||||||
if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
|
if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
|
||||||
info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
|
info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
@ -198,7 +197,7 @@ si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node,
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (position == NULL ||
|
if (position == NULL ||
|
||||||
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_N_ELEMENTS(raw_format.position)))
|
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS))
|
||||||
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
||||||
|
|
||||||
if (mono) {
|
if (mono) {
|
||||||
|
|
@ -350,8 +349,7 @@ format_audio_raw_build (const struct spa_audio_info_raw *info)
|
||||||
if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
|
if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
|
||||||
/* Build the position array spa pod */
|
/* Build the position array spa pod */
|
||||||
g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
|
g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
|
||||||
guint n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
|
for (guint i = 0; i < info->channels; i++)
|
||||||
for (guint i = 0; i < n_pos; i++)
|
|
||||||
wp_spa_pod_builder_add_id (position_builder, info->position[i]);
|
wp_spa_pod_builder_add_id (position_builder, info->position[i]);
|
||||||
|
|
||||||
/* Add the position property */
|
/* Add the position property */
|
||||||
|
|
|
||||||
|
|
@ -195,16 +195,12 @@ on_link_activated (WpObject * proxy, GAsyncResult * res,
|
||||||
{
|
{
|
||||||
WpSiStandardLink *self = wp_transition_get_source_object (transition);
|
WpSiStandardLink *self = wp_transition_get_source_object (transition);
|
||||||
guint len = self->node_links ? self->node_links->len : 0;
|
guint len = self->node_links ? self->node_links->len : 0;
|
||||||
g_autoptr (GError) error = NULL;
|
|
||||||
|
|
||||||
/* Count the number of failed and active links */
|
/* Count the number of failed and active links */
|
||||||
if (wp_object_activate_finish (proxy, res, &error)) {
|
if (wp_object_activate_finish (proxy, res, NULL))
|
||||||
self->n_active_links++;
|
self->n_active_links++;
|
||||||
} else {
|
else
|
||||||
self->n_failed_links++;
|
self->n_failed_links++;
|
||||||
wp_info_object (self, "Failed to activate link %p: %s", proxy,
|
|
||||||
error->message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wait for all links to finish activation */
|
/* Wait for all links to finish activation */
|
||||||
if (self->n_failed_links + self->n_active_links != len)
|
if (self->n_failed_links + self->n_active_links != len)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ typedef enum {
|
||||||
typedef enum {
|
typedef enum {
|
||||||
RESCAN_CONTEXT_LINKING,
|
RESCAN_CONTEXT_LINKING,
|
||||||
RESCAN_CONTEXT_DEFAULT_NODES,
|
RESCAN_CONTEXT_DEFAULT_NODES,
|
||||||
RESCAN_CONTEXT_MEDIA_ROLE_VOLUME,
|
|
||||||
N_RESCAN_CONTEXTS,
|
N_RESCAN_CONTEXTS,
|
||||||
} RescanContext;
|
} RescanContext;
|
||||||
|
|
||||||
|
|
@ -49,7 +48,6 @@ rescan_context_get_type (void)
|
||||||
static const GEnumValue values[] = {
|
static const GEnumValue values[] = {
|
||||||
{ RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" },
|
{ RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" },
|
||||||
{ RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" },
|
{ RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" },
|
||||||
{ RESCAN_CONTEXT_MEDIA_ROLE_VOLUME, "RESCAN_CONTEXT_MEDIA_ROLE_VOLUME", "media-role-volume" },
|
|
||||||
{ 0, NULL, NULL }
|
{ 0, NULL, NULL }
|
||||||
};
|
};
|
||||||
if (g_once_init_enter (>ype_id)) {
|
if (g_once_init_enter (>ype_id)) {
|
||||||
|
|
@ -159,14 +157,10 @@ get_default_event_priority (const gchar *event_type)
|
||||||
if (g_str_has_prefix(event_type, "select-") ||
|
if (g_str_has_prefix(event_type, "select-") ||
|
||||||
g_str_has_prefix(event_type, "create-"))
|
g_str_has_prefix(event_type, "create-"))
|
||||||
return 500;
|
return 500;
|
||||||
if (g_str_has_prefix(event_type, "autoswitch-"))
|
|
||||||
return 400;
|
|
||||||
else if (!g_strcmp0 (event_type, "rescan-for-default-nodes"))
|
else if (!g_strcmp0 (event_type, "rescan-for-default-nodes"))
|
||||||
return -490;
|
return -490;
|
||||||
else if (!g_strcmp0 (event_type, "rescan-for-linking"))
|
else if (!g_strcmp0 (event_type, "rescan-for-linking"))
|
||||||
return -500;
|
return -500;
|
||||||
else if (!g_strcmp0 (event_type, "rescan-for-media-role-volume"))
|
|
||||||
return -510;
|
|
||||||
else if (!g_strcmp0 (event_type, "node-state-changed"))
|
else if (!g_strcmp0 (event_type, "node-state-changed"))
|
||||||
return 50;
|
return 50;
|
||||||
else if (!g_strcmp0 (event_type, "metadata-changed"))
|
else if (!g_strcmp0 (event_type, "metadata-changed"))
|
||||||
|
|
@ -211,8 +205,7 @@ static gboolean
|
||||||
is_it_local_event (const gchar *event_type)
|
is_it_local_event (const gchar *event_type)
|
||||||
{
|
{
|
||||||
if (g_str_has_prefix(event_type, "select-") ||
|
if (g_str_has_prefix(event_type, "select-") ||
|
||||||
g_str_has_prefix(event_type, "create-") ||
|
g_str_has_prefix(event_type, "create-"))
|
||||||
g_str_has_prefix(event_type, "autoswitch-"))
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
|
||||||
308
po/bg.po
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 "Цел за възстановяване"
|
|
||||||
|
|
|
||||||
22
po/conf.pot
22
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"
|
||||||
|
|
@ -133,16 +123,6 @@ msgstr ""
|
||||||
msgid "Ducking level"
|
msgid "Ducking level"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Automatically detect channel count and positions for HDMI devices (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Automatically detect HDMI channels (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "The camera discovery timeout in milliseconds"
|
msgid "The camera discovery timeout in milliseconds"
|
||||||
|
|
@ -175,7 +155,7 @@ msgstr ""
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/description
|
#. /wireplumber.settings.schema/node.features.audio.mono/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "Configure all audio device sink nodes in MONO"
|
msgid "Configure all audio nodes in MONO"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/name
|
#. /wireplumber.settings.schema/node.features.audio.mono/name
|
||||||
|
|
|
||||||
285
po/ka.po
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 "Мақсатты қалпына келтіру"
|
|
||||||
|
|
|
||||||
68
po/sl.po
68
po/sl.po
|
|
@ -2,22 +2,24 @@
|
||||||
# Copyright (C) 2024 WirePlumber's COPYRIGHT HOLDER
|
# Copyright (C) 2024 WirePlumber's COPYRIGHT HOLDER
|
||||||
# This file is distributed under the same license as the WirePlumber package.
|
# This file is distributed under the same license as the WirePlumber package.
|
||||||
#
|
#
|
||||||
# Martin <miles@filmsi.net>, 2024, 2025.
|
# Martin <miles@filmsi.net>, 2024, 2025.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WirePlumber master\n"
|
"Project-Id-Version: WirePlumber master\n"
|
||||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
|
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
||||||
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
|
"issues\n"
|
||||||
"PO-Revision-Date: 2025-12-15 23:31+0100\n"
|
"POT-Creation-Date: 2025-08-21 03:57+0000\n"
|
||||||
|
"PO-Revision-Date: 2025-08-21 15:45+0200\n"
|
||||||
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
|
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
|
||||||
"Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n"
|
"Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n"
|
||||||
"Language: sl\n"
|
"Language: sl\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n"
|
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n"
|
||||||
"X-Generator: Poedit 3.8\n"
|
"%100==4 ? 3 : 0);\n"
|
||||||
|
"X-Generator: Poedit 2.2.1\n"
|
||||||
|
|
||||||
#. WirePlumber
|
#. WirePlumber
|
||||||
#.
|
#.
|
||||||
|
|
@ -47,7 +49,7 @@ msgstr "Razdeli %s"
|
||||||
#. also sanitize nick, replace ':' with ' '
|
#. also sanitize nick, replace ':' with ' '
|
||||||
#. ensure the node has a description
|
#. ensure the node has a description
|
||||||
#. also sanitize description, replace ':' with ' '
|
#. also sanitize description, replace ':' with ' '
|
||||||
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
|
#. add api.alsa.card.* properties for rule matching purposes
|
||||||
#. add cpu.vm.name for rule matching purposes
|
#. add cpu.vm.name for rule matching purposes
|
||||||
#. apply properties from rules defined in JSON .conf file
|
#. apply properties from rules defined in JSON .conf file
|
||||||
#. handle split HW node
|
#. handle split HW node
|
||||||
|
|
@ -73,7 +75,6 @@ msgstr "Modem"
|
||||||
#. form factor -> icon
|
#. form factor -> icon
|
||||||
#. apply properties from rules defined in JSON .conf file
|
#. apply properties from rules defined in JSON .conf file
|
||||||
#. override the device factory to use ACP
|
#. override the device factory to use ACP
|
||||||
#. use HDMI channel detection if enabled in settings
|
|
||||||
#. use device reservation, if available
|
#. use device reservation, if available
|
||||||
#. unlike pipewire-media-session, this logic here keeps the device
|
#. unlike pipewire-media-session, this logic here keeps the device
|
||||||
#. acquired at all times and destroys it if someone else acquires
|
#. acquired at all times and destroys it if someone else acquires
|
||||||
|
|
@ -200,34 +201,6 @@ msgstr "Privzeta glasnost za zvočne vire"
|
||||||
msgid "Default source volume"
|
msgid "Default source volume"
|
||||||
msgstr "Privzeta izvorna glasnost"
|
msgstr "Privzeta izvorna glasnost"
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically mute all audio devices when active wired headphones/speakers "
|
|
||||||
"are disconnected to prevent unintended sound output"
|
|
||||||
msgstr ""
|
|
||||||
"Samodejno utišaj vse zvočne naprave, ko so aktivne žične slušalke/zvočniki "
|
|
||||||
"odklopljeni, za preprečitev nenamernega zvočnega izhoda"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Auto-mute on wired audio disconnect"
|
|
||||||
msgstr "Samodejna utišaj zvok pri prekinitvi žične zvokovne povezave"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically mute all audio devices when active Bluetooth headphones/"
|
|
||||||
"speakers are disconnected to prevent unintended sound output"
|
|
||||||
msgstr ""
|
|
||||||
"Samodejno utišaj vse zvočne naprave, ko so aktivne slušalke/zvočniki "
|
|
||||||
"Bluetooth odklopljeni, da preprečite nenamerni izhod zvoka"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Auto-mute on Bluetooth audio disconnect"
|
|
||||||
msgstr "Samodejni utišaj pri prekinitvi zvokovne povezave Bluetooth"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
|
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "Streams may be moved by adding PipeWire metadata at runtime"
|
msgid "Streams may be moved by adding PipeWire metadata at runtime"
|
||||||
|
|
@ -274,19 +247,6 @@ msgstr ""
|
||||||
msgid "Ducking level"
|
msgid "Ducking level"
|
||||||
msgstr "Stopnja umikanja"
|
msgstr "Stopnja umikanja"
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically detect channel count and positions for HDMI devices "
|
|
||||||
"(experimental)"
|
|
||||||
msgstr ""
|
|
||||||
"Samodejno zaznaj število kanalov in položaje za naprave HDMI (poskusno)"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Automatically detect HDMI channels (experimental)"
|
|
||||||
msgstr "Samodejno zaznaj kanale HDMI (poskusno)"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "The camera discovery timeout in milliseconds"
|
msgid "The camera discovery timeout in milliseconds"
|
||||||
|
|
@ -317,16 +277,6 @@ msgstr "Omogoči vrata nadzornih zvočnikov na zvočnih vozliščih"
|
||||||
msgid "Monitor ports"
|
msgid "Monitor ports"
|
||||||
msgstr "Vrata zvočnih monitorjev"
|
msgstr "Vrata zvočnih monitorjev"
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Configure all audio device sink nodes in MONO"
|
|
||||||
msgstr "Prilagodi vsa vozlišča zvokovnih ponorov v MONO"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Mono"
|
|
||||||
msgstr "Mono"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
|
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "Do not convert audio to F32 format"
|
msgid "Do not convert audio to F32 format"
|
||||||
|
|
|
||||||
392
po/sr.po
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"
|
|
||||||
|
|
|
||||||
398
po/tr.po
398
po/tr.po
|
|
@ -1,25 +1,26 @@
|
||||||
# Turkish translation for WirePlumber.
|
# Turkish translation for PipeWire.
|
||||||
# Copyright (C) 2025 WirePlumber's COPYRIGHT HOLDER
|
# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
|
||||||
# This file is distributed under the same license as the WirePlumber package.
|
# This file is distributed under the same license as the PipeWire package.
|
||||||
#
|
# Necdet Yücel <necdetyucel@gmail.com>, 2014.
|
||||||
# Sabri Ünal <yakushabb@gmail.com>, 2025.
|
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
|
||||||
# Emin Tufan Çetin <etcetin@gmail.com>, 2025
|
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
|
||||||
|
# Oğuz Ersen <oguzersen@protonmail.com>, 2021.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WirePlumber master\n"
|
"Project-Id-Version: PipeWire master\n"
|
||||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
|
||||||
"issues\n"
|
"issues/new\n"
|
||||||
"POT-Creation-Date: 2025-11-09 04:07+0000\n"
|
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
|
||||||
"PO-Revision-Date: 2025-11-09 08:00+0300\n"
|
"PO-Revision-Date: 2021-12-06 21:31+0300\n"
|
||||||
"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
|
"Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n"
|
||||||
"Language-Team: Turkish <takim@gnome.org.tr>\n"
|
"Language-Team: Turkish <tr>\n"
|
||||||
"Language: tr\n"
|
"Language: tr\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Generator: Poedit 3.8\n"
|
"X-Generator: Weblate 4.4.2\n"
|
||||||
|
|
||||||
#. WirePlumber
|
#. WirePlumber
|
||||||
#.
|
#.
|
||||||
|
|
@ -27,19 +28,13 @@ msgstr ""
|
||||||
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
#.
|
#.
|
||||||
#. SPDX-License-Identifier: MIT
|
#. SPDX-License-Identifier: MIT
|
||||||
#. unique device/node name tables
|
#. Receive script arguments from config.lua
|
||||||
#. SPA ids to node names: name = id_name_table[device_id][node_id]
|
#. ensure config.properties is not nil
|
||||||
#. create the underlying hidden ALSA node
|
#. preprocess rules and create Interest objects
|
||||||
#. not suitable for loopback
|
#. applies properties from config.rules when asked to
|
||||||
#: src/scripts/monitors/alsa.lua:106
|
|
||||||
#, lua-format
|
|
||||||
msgid "Split %s"
|
|
||||||
msgstr "Bölük %s"
|
|
||||||
|
|
||||||
#. Connect ObjectConfig events to the right node
|
|
||||||
#. set the device id and spa factory name; REQUIRED, do not change
|
#. set the device id and spa factory name; REQUIRED, do not change
|
||||||
#. set the default pause-on-idle setting
|
#. set the default pause-on-idle setting
|
||||||
#. try to negotiate the max amount of channels
|
#. try to negotiate the max ammount of channels
|
||||||
#. set priority
|
#. set priority
|
||||||
#. ensure the node has a media class
|
#. ensure the node has a media class
|
||||||
#. ensure the node has a name
|
#. ensure the node has a name
|
||||||
|
|
@ -50,360 +45,15 @@ msgstr "Bölük %s"
|
||||||
#. ensure the node has a description
|
#. ensure the node has a description
|
||||||
#. also sanitize description, replace ':' with ' '
|
#. also sanitize description, replace ':' with ' '
|
||||||
#. add api.alsa.card.* properties for rule matching purposes
|
#. add api.alsa.card.* properties for rule matching purposes
|
||||||
#. add cpu.vm.name for rule matching purposes
|
#. apply properties from config.rules
|
||||||
#. apply properties from rules defined in JSON .conf file
|
|
||||||
#. handle split HW node
|
|
||||||
#. create split PCM node
|
|
||||||
#. create the node
|
#. create the node
|
||||||
#. ensure the device has an appropriate name
|
#. ensure the device has an appropriate name
|
||||||
#. deduplicate devices with the same name
|
#. deduplicate devices with the same name
|
||||||
#. ensure the device has a description
|
#. ensure the device has a description
|
||||||
#: src/scripts/monitors/alsa.lua:438
|
#: src/scripts/monitors/alsa.lua:222
|
||||||
msgid "Loopback"
|
|
||||||
msgstr "Geri Döngü"
|
|
||||||
|
|
||||||
#: src/scripts/monitors/alsa.lua:440
|
|
||||||
msgid "Built-in Audio"
|
msgid "Built-in Audio"
|
||||||
msgstr "Yerleşik Ses"
|
msgstr "Dahili Ses"
|
||||||
|
|
||||||
#: src/scripts/monitors/alsa.lua:442
|
#: src/scripts/monitors/alsa.lua:224
|
||||||
msgid "Modem"
|
msgid "Modem"
|
||||||
msgstr "Modem"
|
msgstr "Modem"
|
||||||
|
|
||||||
#. ensure the device has a nick
|
|
||||||
#. set the icon name
|
|
||||||
#. form factor -> icon
|
|
||||||
#. apply properties from rules defined in JSON .conf file
|
|
||||||
#. override the device factory to use ACP
|
|
||||||
#. use HDMI channel detection if enabled in settings
|
|
||||||
#. use device reservation, if available
|
|
||||||
#. unlike pipewire-media-session, this logic here keeps the device
|
|
||||||
#. acquired at all times and destroys it if someone else acquires
|
|
||||||
#. create the device
|
|
||||||
#. attempt to acquire again
|
|
||||||
#. destroy the device
|
|
||||||
#. create the device
|
|
||||||
#. handle create-object to prepare device
|
|
||||||
#. handle object-removed to destroy device reservations and recycle device name
|
|
||||||
#. reset the name tables to make sure names are recycled
|
|
||||||
#. activate monitor
|
|
||||||
#. if the reserve-device plugin is enabled, at the point of script execution
|
|
||||||
#. it is expected to be connected. if it is not, assume the d-bus connection
|
|
||||||
#. has failed and continue without it
|
|
||||||
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
|
|
||||||
#. case D-Bus service is restarted
|
|
||||||
#. create the monitor
|
|
||||||
#. WirePlumber
|
|
||||||
#.
|
|
||||||
#. Copyright © 2022 Pauli Virtanen
|
|
||||||
#. @author Pauli Virtanen
|
|
||||||
#.
|
|
||||||
#. SPDX-License-Identifier: MIT
|
|
||||||
#. unique device/node name tables
|
|
||||||
#. set the node description
|
|
||||||
#. sanitize description, replace ':' with ' '
|
|
||||||
#. set the node name
|
|
||||||
#. sanitize name
|
|
||||||
#. deduplicate nodes with the same name
|
|
||||||
#. apply properties from the rules in the configuration file
|
|
||||||
#. create the node
|
|
||||||
#. it doesn't necessarily need to be a local node,
|
|
||||||
#. the other Bluetooth parts run in the local process,
|
|
||||||
#. so it's consistent to have also this here
|
|
||||||
#. reset the name tables to make sure names are recycled
|
|
||||||
#: src/scripts/monitors/bluez-midi.lua:114
|
|
||||||
#, lua-format
|
|
||||||
msgid "BLE MIDI %d"
|
|
||||||
msgstr "BLE MIDI %d"
|
|
||||||
|
|
||||||
#. if logind support is enabled, activate
|
|
||||||
#. the monitor only when the seat is active
|
|
||||||
#. WirePlumber
|
|
||||||
#.
|
|
||||||
#. Copyright © 2023 Collabora Ltd.
|
|
||||||
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
|
||||||
#.
|
|
||||||
#. SPDX-License-Identifier: MIT
|
|
||||||
#. set the device id and spa factory name; REQUIRED, do not change
|
|
||||||
#. set the default pause-on-idle setting
|
|
||||||
#. set the node name
|
|
||||||
#. sanitize name
|
|
||||||
#. deduplicate nodes with the same name
|
|
||||||
#. set the node description
|
|
||||||
#: src/scripts/monitors/libcamera/name-node.lua:61
|
|
||||||
msgid "Built-in Front Camera"
|
|
||||||
msgstr "Yerleşik Ön Kamera"
|
|
||||||
|
|
||||||
#: src/scripts/monitors/libcamera/name-node.lua:63
|
|
||||||
msgid "Built-in Back Camera"
|
|
||||||
msgstr "Yerleşik Arka Kamera"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Always show microphone for Bluetooth headsets, and switch to headset mode "
|
|
||||||
"when recording"
|
|
||||||
msgstr ""
|
|
||||||
"Bluetooth kulaklıklar için her zaman mikrofonu göster ve kayıt sırasında "
|
|
||||||
"kulaklık kipine geç"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Auto-switch to headset profile"
|
|
||||||
msgstr "Kulaklık profiline kendiliğinden geç"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Remember and restore Bluetooth headset mode status"
|
|
||||||
msgstr "Bluetooth kulaklık kipi durumunu anımsa ve geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Persistent storage"
|
|
||||||
msgstr "Kalıcı depolama"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.restore-profile/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Remember and restore device profiles"
|
|
||||||
msgstr "Aygıt profillerini anımsa ve geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.restore-profile/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Restore profile"
|
|
||||||
msgstr "Profili geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.restore-routes/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Remember and restore device routes"
|
|
||||||
msgstr "Aygıt rotalarını anımsa ve geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.restore-routes/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Restore routes"
|
|
||||||
msgstr "Rotaları geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "The default volume for audio sinks"
|
|
||||||
msgstr "Ses alıcıları için öntanımlı ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Default sink volume"
|
|
||||||
msgstr "Öntanımlı alıcı ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "The default volume for audio sources"
|
|
||||||
msgstr "Ses kaynakları için öntanımlı ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Default source volume"
|
|
||||||
msgstr "Öntanımlı kaynak ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically mute all audio devices when active wired headphones/speakers "
|
|
||||||
"are disconnected to prevent unintended sound output"
|
|
||||||
msgstr ""
|
|
||||||
"İstenmeyen ses çıktısını önlemek için etkin kablolu kulaklık/hoparlör "
|
|
||||||
"bağlantısı kesildiğinde tüm ses aygıtlarını kendiliğinden sustur"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Auto-mute on wired audio disconnect"
|
|
||||||
msgstr "Kablolu ses bağlantısı kesildiğinde kendiliğinden sustur"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically mute all audio devices when active Bluetooth headphones/"
|
|
||||||
"speakers are disconnected to prevent unintended sound output"
|
|
||||||
msgstr ""
|
|
||||||
"İstenmeyen ses çıktısını önlemek için etkin Bluetooth kulaklık/hoparlör "
|
|
||||||
"bağlantısı kesildiğinde tüm ses aygıtlarını kendiliğinden sustur"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Auto-mute on Bluetooth audio disconnect"
|
|
||||||
msgstr "Bluetooth ses bağlantısı kesildiğinde kendiliğinden sustur"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Streams may be moved by adding PipeWire metadata at runtime"
|
|
||||||
msgstr "Akışlar, çalışma zamanında PipeWire üst verileri eklenerek taşınabilir"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Allow moving streams"
|
|
||||||
msgstr "Akışları taşımaya izin ver"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.follow-default-target/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Streams connected to the default device follow when default changes"
|
|
||||||
msgstr "Öntanımlı aygıta bağlı akışlar öntanımlı değiştiğinde izler"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.follow-default-target/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Follow default target"
|
|
||||||
msgstr "Öntanımlı hedefi izle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.pause-playback/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Pause media players if their target sink is removed"
|
|
||||||
msgstr "Hedef alıcıları kaldırılırsa ortam oynatıcıları duraklat"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.pause-playback/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Pause playback if output removed"
|
|
||||||
msgstr "Çıktı kaldırılırsa oynatmayı duraklat"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"The volume level to apply when ducking (= reducing volume for a higher "
|
|
||||||
"priority stream to be audible) in the role-based linking policy"
|
|
||||||
msgstr ""
|
|
||||||
"Rol tabanlı bağlantı ilkesinde eğilirken (= daha öncelikli akışın "
|
|
||||||
"duyulabilmesi için ses düzeyinin azaltılması) uygulanacak ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Ducking level"
|
|
||||||
msgstr "Eğilme düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically detect channel count and positions for HDMI devices "
|
|
||||||
"(experimental)"
|
|
||||||
msgstr ""
|
|
||||||
"HDMI aygıtları için kanal sayısını ve konumlarını kendiliğinden algıla "
|
|
||||||
"(deneysel)"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Automatically detect HDMI channels (experimental)"
|
|
||||||
msgstr "HDMI kanallarını kendiliğinden algıla (deneysel)"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "The camera discovery timeout in milliseconds"
|
|
||||||
msgstr "Kamera keşif zaman aşımı, saniye türünden"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Discovery timeout"
|
|
||||||
msgstr "Keşif zaman aşımı"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.control-port/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Enable control ports on audio nodes"
|
|
||||||
msgstr "Ses düğümlerinde denetim bağlantı noktalarını etkinleştir"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.control-port/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Control ports"
|
|
||||||
msgstr "Denetim bağlantı noktaları"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Enable monitor ports on audio nodes"
|
|
||||||
msgstr "Ses düğümlerinde izleme bağlantı noktalarını etkinleştir"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Monitor ports"
|
|
||||||
msgstr "İzleme bağlantı noktaları"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Configure all audio nodes in MONO"
|
|
||||||
msgstr "Tüm ses düğümlerini MONO olarak yapılandır"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Mono"
|
|
||||||
msgstr "Mono"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Do not convert audio to F32 format"
|
|
||||||
msgstr "Sesi F32 biçimine dönüştürme"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "No DSP"
|
|
||||||
msgstr "DSP yok"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.filter.forward-format/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Forward format on filter nodes or not"
|
|
||||||
msgstr "Süzgeç düğümlerinde biçimi ilet ya da iletme"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.filter.forward-format/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Forward format"
|
|
||||||
msgstr "Biçimi ilet"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.restore-default-targets/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Remember and restore default audio/video input/output devices"
|
|
||||||
msgstr "Öntanımlı ses/video girdi/çıktı aygıtlarını anımsa ve geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.restore-default-targets/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Restore default target"
|
|
||||||
msgstr "Öntanımlı hedefi geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "The default volume for capture nodes"
|
|
||||||
msgstr "Yakalama düğümleri için öntanımlı ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Default capture volume"
|
|
||||||
msgstr "Öntanımlı yakalama ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.default-media-role/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Default media.role to assign on streams that do not specify it"
|
|
||||||
msgstr "Belirtilmeyen akışlarda atanacak öntanımlı ortam rolü"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.default-media-role/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Default media role"
|
|
||||||
msgstr "Öntanımlı ortam rolü"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "The default volume for playback nodes"
|
|
||||||
msgstr "Oynatma düğümleri için öntanımlı ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Default playback volume"
|
|
||||||
msgstr "Öntanımlı oynatma ses düzeyi"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.restore-props/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Remember and restore properties of streams"
|
|
||||||
msgstr "Akışların özelliklerini anımsa ve geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.restore-props/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Restore properties"
|
|
||||||
msgstr "Özellikleri geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.restore-target/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Remember and restore stream targets"
|
|
||||||
msgstr "Akış hedeflerini anımsa ve geri yükle"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.stream.restore-target/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Restore target"
|
|
||||||
msgstr "Hedefi geri yükle"
|
|
||||||
|
|
|
||||||
23
po/zh_CN.po
23
po/zh_CN.po
|
|
@ -13,8 +13,8 @@ msgstr ""
|
||||||
"Project-Id-Version: pipewire.master-tx\n"
|
"Project-Id-Version: pipewire.master-tx\n"
|
||||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
||||||
"issues\n"
|
"issues\n"
|
||||||
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
|
"POT-Creation-Date: 2025-10-01 16:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-12-16 10:10+0800\n"
|
"PO-Revision-Date: 2025-10-02 07:57+0800\n"
|
||||||
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
|
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
|
||||||
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
|
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
|
||||||
"Language: zh_CN\n"
|
"Language: zh_CN\n"
|
||||||
|
|
@ -53,7 +53,7 @@ msgstr "分离 %s"
|
||||||
#. also sanitize nick, replace ':' with ' '
|
#. also sanitize nick, replace ':' with ' '
|
||||||
#. ensure the node has a description
|
#. ensure the node has a description
|
||||||
#. also sanitize description, replace ':' with ' '
|
#. also sanitize description, replace ':' with ' '
|
||||||
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
|
#. add api.alsa.card.* properties for rule matching purposes
|
||||||
#. add cpu.vm.name for rule matching purposes
|
#. add cpu.vm.name for rule matching purposes
|
||||||
#. apply properties from rules defined in JSON .conf file
|
#. apply properties from rules defined in JSON .conf file
|
||||||
#. handle split HW node
|
#. handle split HW node
|
||||||
|
|
@ -79,7 +79,6 @@ msgstr "调制解调器"
|
||||||
#. form factor -> icon
|
#. form factor -> icon
|
||||||
#. apply properties from rules defined in JSON .conf file
|
#. apply properties from rules defined in JSON .conf file
|
||||||
#. override the device factory to use ACP
|
#. override the device factory to use ACP
|
||||||
#. use HDMI channel detection if enabled in settings
|
|
||||||
#. use device reservation, if available
|
#. use device reservation, if available
|
||||||
#. unlike pipewire-media-session, this logic here keeps the device
|
#. unlike pipewire-media-session, this logic here keeps the device
|
||||||
#. acquired at all times and destroys it if someone else acquires
|
#. acquired at all times and destroys it if someone else acquires
|
||||||
|
|
@ -274,18 +273,6 @@ msgstr ""
|
||||||
msgid "Ducking level"
|
msgid "Ducking level"
|
||||||
msgstr "回避级别"
|
msgstr "回避级别"
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid ""
|
|
||||||
"Automatically detect channel count and positions for HDMI devices "
|
|
||||||
"(experimental)"
|
|
||||||
msgstr "自动检测 HDMI 设备的声道数量和位置(实验性)"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
|
|
||||||
#: wireplumber.conf
|
|
||||||
msgid "Automatically detect HDMI channels (experimental)"
|
|
||||||
msgstr "自动检测 HDMI 声道(实验性)"
|
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "The camera discovery timeout in milliseconds"
|
msgid "The camera discovery timeout in milliseconds"
|
||||||
|
|
@ -318,8 +305,8 @@ msgstr "监视器端口"
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/description
|
#. /wireplumber.settings.schema/node.features.audio.mono/description
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
msgid "Configure all audio device sink nodes in MONO"
|
msgid "Configure all audio nodes in MONO"
|
||||||
msgstr "在单声道中配置所有音频设备信宿节点"
|
msgstr "在单声道中配置所有音频节点"
|
||||||
|
|
||||||
#. /wireplumber.settings.schema/node.features.audio.mono/name
|
#. /wireplumber.settings.schema/node.features.audio.mono/name
|
||||||
#: wireplumber.conf
|
#: wireplumber.conf
|
||||||
|
|
|
||||||
|
|
@ -470,9 +470,6 @@ wireplumber.components = [
|
||||||
{
|
{
|
||||||
name = monitors/libcamera/create-node.lua, type = script/lua
|
name = monitors/libcamera/create-node.lua, type = script/lua
|
||||||
provides = hooks.monitor.libcamera-create-node
|
provides = hooks.monitor.libcamera-create-node
|
||||||
requires = [ support.export-core,
|
|
||||||
pw.client-node,
|
|
||||||
pw.node-factory.spa ]
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = monitors/libcamera/enumerate-device.lua, type = script/lua
|
name = monitors/libcamera/enumerate-device.lua, type = script/lua
|
||||||
|
|
@ -488,43 +485,23 @@ wireplumber.components = [
|
||||||
|
|
||||||
## Client access configuration hooks
|
## Client access configuration hooks
|
||||||
{
|
{
|
||||||
name = client/select-access.lua, type = script/lua
|
name = client/access-default.lua, type = script/lua
|
||||||
provides = script.client.select-access
|
provides = script.client.access-default
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = client/find-config-access.lua, type = script/lua
|
name = client/access-portal.lua, type = script/lua
|
||||||
provides = script.client.access-config
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = client/find-flatpak-access.lua, type = script/lua
|
|
||||||
provides = script.client.access-flatpak
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = client/find-snap-access.lua, type = script/lua
|
|
||||||
provides = script.client.access-snap
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = client/find-portal-access.lua, type = script/lua
|
|
||||||
provides = script.client.access-portal
|
provides = script.client.access-portal
|
||||||
requires = [ support.portal-permissionstore ]
|
requires = [ support.portal-permissionstore ]
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = client/find-default-access.lua, type = script/lua
|
name = client/access-snap.lua, type = script/lua
|
||||||
provides = script.client.access-default
|
provides = script.client.access-snap
|
||||||
}
|
|
||||||
{
|
|
||||||
name = client/apply-access.lua, type = script/lua
|
|
||||||
provides = script.client.apply-access
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
type = virtual, provides = policy.client.access
|
type = virtual, provides = policy.client.access
|
||||||
requires = [ script.client.select-access,
|
wants = [ script.client.access-default,
|
||||||
script.client.access-config,
|
script.client.access-portal,
|
||||||
script.client.access-default,
|
script.client.access-snap ]
|
||||||
script.client.apply-access ]
|
|
||||||
wants = [ script.client.access-flatpak,
|
|
||||||
script.client.access-snap,
|
|
||||||
script.client.access-portal ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## Device profile selection hooks
|
## Device profile selection hooks
|
||||||
|
|
@ -650,17 +627,12 @@ wireplumber.components = [
|
||||||
name = node/filter-forward-format.lua, type = script/lua
|
name = node/filter-forward-format.lua, type = script/lua
|
||||||
provides = hooks.filter.forward-format
|
provides = hooks.filter.forward-format
|
||||||
}
|
}
|
||||||
{
|
|
||||||
name = node/filter-graph.lua, type = script/lua
|
|
||||||
provides = hooks.filter.graph
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
type = virtual, provides = policy.node
|
type = virtual, provides = policy.node
|
||||||
requires = [ hooks.node.create-session-item ]
|
requires = [ hooks.node.create-session-item ]
|
||||||
wants = [ hooks.node.suspend
|
wants = [ hooks.node.suspend
|
||||||
hooks.stream.state
|
hooks.stream.state
|
||||||
hooks.filter.forward-format
|
hooks.filter.forward-format ]
|
||||||
hooks.filter.graph ]
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = node/software-dsp.lua, type = script/lua
|
name = node/software-dsp.lua, type = script/lua
|
||||||
|
|
@ -676,11 +648,6 @@ wireplumber.components = [
|
||||||
name = linking/rescan.lua, type = script/lua
|
name = linking/rescan.lua, type = script/lua
|
||||||
provides = hooks.linking.rescan
|
provides = hooks.linking.rescan
|
||||||
}
|
}
|
||||||
{
|
|
||||||
name = linking/rescan-on-linkable.lua, type = script/lua
|
|
||||||
provides = hooks.linking.rescan-on-linkable
|
|
||||||
requires = [ hooks.linking.rescan ]
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
name = linking/find-media-role-target.lua, type = script/lua
|
name = linking/find-media-role-target.lua, type = script/lua
|
||||||
provides = hooks.linking.target.find-media-role
|
provides = hooks.linking.target.find-media-role
|
||||||
|
|
@ -734,8 +701,7 @@ wireplumber.components = [
|
||||||
requires = [ hooks.linking.rescan,
|
requires = [ hooks.linking.rescan,
|
||||||
hooks.linking.target.prepare-link,
|
hooks.linking.target.prepare-link,
|
||||||
hooks.linking.target.link ]
|
hooks.linking.target.link ]
|
||||||
wants = [ hooks.linking.rescan-on-linkable,
|
wants = [ hooks.linking.target.find-media-role,
|
||||||
hooks.linking.target.find-media-role,
|
|
||||||
hooks.linking.target.find-defined,
|
hooks.linking.target.find-defined,
|
||||||
hooks.linking.target.find-audio-group,
|
hooks.linking.target.find-audio-group,
|
||||||
hooks.linking.target.find-filter,
|
hooks.linking.target.find-filter,
|
||||||
|
|
@ -751,21 +717,10 @@ wireplumber.components = [
|
||||||
provides = hooks.linking.role-based.rescan
|
provides = hooks.linking.role-based.rescan
|
||||||
requires = [ api.mixer ]
|
requires = [ api.mixer ]
|
||||||
}
|
}
|
||||||
{
|
|
||||||
name = node/find-media-role-default-volume.lua, type = script/lua
|
|
||||||
provides = hooks.node.role-based.default-volume
|
|
||||||
requires = [ hooks.linking.role-based.rescan ]
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = linking/find-media-role-sink-target.lua, type = script/lua
|
|
||||||
provides = hooks.linking.target.find-media-role-sink
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
type = virtual, provides = policy.linking.role-based
|
type = virtual, provides = policy.linking.role-based
|
||||||
requires = [ policy.linking.standard,
|
requires = [ policy.linking.standard,
|
||||||
hooks.linking.role-based.rescan,
|
hooks.linking.role-based.rescan ]
|
||||||
hooks.node.role-based.default-volume,
|
|
||||||
hooks.linking.target.find-media-role-sink ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## Standard policy definition
|
## Standard policy definition
|
||||||
|
|
@ -876,12 +831,6 @@ wireplumber.settings.schema = {
|
||||||
type = "bool"
|
type = "bool"
|
||||||
default = true
|
default = true
|
||||||
}
|
}
|
||||||
bluetooth.profile-preference = {
|
|
||||||
name = "Bluetooth profile preference"
|
|
||||||
description = "Prefer better quality or better latency when auto-selecting profiles (only 'quality' or 'latency' values are accepted)"
|
|
||||||
type = "string"
|
|
||||||
default = "quality"
|
|
||||||
}
|
|
||||||
|
|
||||||
## Device
|
## Device
|
||||||
device.restore-profile = {
|
device.restore-profile = {
|
||||||
|
|
@ -963,12 +912,6 @@ wireplumber.settings.schema = {
|
||||||
min = 0
|
min = 0
|
||||||
max = 60000
|
max = 60000
|
||||||
}
|
}
|
||||||
monitor.alsa.autodetect-hdmi-channels = {
|
|
||||||
name = "Automatically detect HDMI channels (experimental)"
|
|
||||||
description = "Automatically detect channel count and positions for HDMI devices (experimental)"
|
|
||||||
type = "bool"
|
|
||||||
default = false
|
|
||||||
}
|
|
||||||
|
|
||||||
## Node
|
## Node
|
||||||
node.features.audio.no-dsp = {
|
node.features.audio.no-dsp = {
|
||||||
|
|
@ -991,7 +934,7 @@ wireplumber.settings.schema = {
|
||||||
}
|
}
|
||||||
node.features.audio.mono = {
|
node.features.audio.mono = {
|
||||||
name = "Mono"
|
name = "Mono"
|
||||||
description = "Configure all audio device sink nodes in MONO"
|
description = "Configure all audio nodes in MONO"
|
||||||
type = "bool"
|
type = "bool"
|
||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
node.filter-graph.rules = [
|
|
||||||
## The list of filter graph rules
|
|
||||||
|
|
||||||
## This rule example creates two filter graphs for each audio source node
|
|
||||||
# {
|
|
||||||
# matches = [
|
|
||||||
# {
|
|
||||||
# ## This matches all audio source nodes
|
|
||||||
# media.class = "Audio/Source"
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# actions = {
|
|
||||||
# create-filter-graph = [
|
|
||||||
# ## Multiple filter graphs can be defined here.
|
|
||||||
# ## The syntax is the same as the pipewire filter-chain conf files.
|
|
||||||
#
|
|
||||||
# ## This is an example of a bultin passthrough filter
|
|
||||||
# {
|
|
||||||
# nodes = [
|
|
||||||
# {
|
|
||||||
# type = builtin
|
|
||||||
# label = copy
|
|
||||||
# name = passthrough
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# ## This is an example of a LADSPA rnnoise filter
|
|
||||||
# {
|
|
||||||
# nodes = [
|
|
||||||
# {
|
|
||||||
# type = ladspa
|
|
||||||
# name = rnnoise
|
|
||||||
# plugin = librnnoise_ladspa
|
|
||||||
# label = noise_suppressor_stereo
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
@ -150,7 +150,6 @@ wireplumber.components = [
|
||||||
policy.role-based.priority = 100
|
policy.role-based.priority = 100
|
||||||
policy.role-based.action.same-priority = "mix"
|
policy.role-based.action.same-priority = "mix"
|
||||||
policy.role-based.action.lower-priority = "cork"
|
policy.role-based.action.lower-priority = "cork"
|
||||||
policy.role-based.preferred-target = "Speaker"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
provides = loopback.sink.role.alert
|
provides = loopback.sink.role.alert
|
||||||
|
|
|
||||||
|
|
@ -19,25 +19,5 @@ monitor.alsa.rules = [
|
||||||
api.alsa.headroom = 2048
|
api.alsa.headroom = 2048
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
# VMware & VirtualBox on Windows hosts require more headroom to
|
|
||||||
# avoid stuttering.
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
node.name = "~alsa_input.pci.*"
|
|
||||||
cpu.vm.name = "~^(vmware)|(oracle)$"
|
|
||||||
}
|
|
||||||
{
|
|
||||||
node.name = "~alsa_output.pci.*"
|
|
||||||
cpu.vm.name = "~^(vmware)|(oracle)$"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
api.alsa.period-size = 1024
|
|
||||||
api.alsa.headroom = 8192
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
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()
|
|
||||||
|
|
@ -24,9 +24,6 @@ SimpleEventHook {
|
||||||
|
|
||||||
local om = source:call ("get-object-manager", "metadata")
|
local om = source:call ("get-object-manager", "metadata")
|
||||||
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||||
if metadata == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if selected_node then
|
if selected_node then
|
||||||
local key = "default." .. def_node_type
|
local key = "default." .. def_node_type
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
cutils = require ("common-utils")
|
cutils = require ("common-utils")
|
||||||
log = Log.open_topic ("s-automute-alsa-routes")
|
log = Log.open_topic ("s-automute-alsa-routes")
|
||||||
hooks_registered = false
|
|
||||||
|
|
||||||
function setRoute (device, route, mute)
|
function setRoute (device, route, mute)
|
||||||
local param = Pod.Object {
|
local param = Pod.Object {
|
||||||
|
|
@ -195,19 +194,17 @@ evaluate_mute_on_node_removed_hook = SimpleEventHook {
|
||||||
function toggleState ()
|
function toggleState ()
|
||||||
local mute_alsa = Settings.get_boolean ("device.routes.mute-on-alsa-playback-removed")
|
local mute_alsa = Settings.get_boolean ("device.routes.mute-on-alsa-playback-removed")
|
||||||
local mute_bluez = Settings.get_boolean ("device.routes.mute-on-bluetooth-playback-removed")
|
local mute_bluez = Settings.get_boolean ("device.routes.mute-on-bluetooth-playback-removed")
|
||||||
if (mute_alsa or mute_bluez) and not hooks_registered then
|
if mute_alsa or mute_bluez then
|
||||||
nodes_info = {}
|
nodes_info = {}
|
||||||
mute_alsa_devices_hook:register ()
|
mute_alsa_devices_hook:register ()
|
||||||
update_nodes_info_hook:register ()
|
update_nodes_info_hook:register ()
|
||||||
evaluate_mute_on_device_route_changed_hook:register ()
|
evaluate_mute_on_device_route_changed_hook:register ()
|
||||||
evaluate_mute_on_node_removed_hook:register ()
|
evaluate_mute_on_node_removed_hook:register ()
|
||||||
hooks_registered = true
|
else
|
||||||
elseif not mute_alsa and not mute_bluez and hooks_registered then
|
|
||||||
mute_alsa_devices_hook:remove ()
|
mute_alsa_devices_hook:remove ()
|
||||||
update_nodes_info_hook:remove ()
|
update_nodes_info_hook:remove ()
|
||||||
evaluate_mute_on_device_route_changed_hook:remove ()
|
evaluate_mute_on_device_route_changed_hook:remove ()
|
||||||
evaluate_mute_on_node_removed_hook:remove ()
|
evaluate_mute_on_node_removed_hook:remove ()
|
||||||
hooks_registered = false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,25 +28,43 @@
|
||||||
lutils = require ("linking-utils")
|
lutils = require ("linking-utils")
|
||||||
cutils = require ("common-utils")
|
cutils = require ("common-utils")
|
||||||
log = Log.open_topic ("s-device")
|
log = Log.open_topic ("s-device")
|
||||||
persistent_storage_hooks_registered = false
|
|
||||||
autoswitch_hooks_registered = false
|
|
||||||
|
|
||||||
local PROFILE_RESTORE_TIMEOUT_MSEC = 2000
|
state = nil
|
||||||
local PROFILE_SWITCH_TIMEOUT_MSEC = 500
|
headset_profiles = nil
|
||||||
|
|
||||||
local state = nil
|
local profile_restore_timeout_msec = 2000
|
||||||
local headset_profiles = {}
|
local profile_switch_timeout_msec = 500
|
||||||
local non_headset_profiles = {}
|
|
||||||
local capture_stream_links = {}
|
local INVALID = -1
|
||||||
local restore_timeout_source = {}
|
local restore_timeout_source = {}
|
||||||
local switch_timeout_source = {}
|
local switch_timeout_source = {}
|
||||||
|
|
||||||
function saveHeadsetProfile (device, profile_name, persistent)
|
local last_profiles = {}
|
||||||
|
|
||||||
|
local active_streams = {}
|
||||||
|
local previous_streams = {}
|
||||||
|
|
||||||
|
function handlePersistentSetting (enable)
|
||||||
|
if enable and state == nil then
|
||||||
|
-- the state storage
|
||||||
|
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
|
||||||
|
and State ("bluetooth-autoswitch") or nil
|
||||||
|
headset_profiles = state and state:load () or {}
|
||||||
|
else
|
||||||
|
state = nil
|
||||||
|
headset_profiles = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||||
|
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
|
||||||
|
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
function saveHeadsetProfile (device, profile_name)
|
||||||
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||||
headset_profiles [key] = profile_name
|
headset_profiles [key] = profile_name
|
||||||
if state ~= nil and persistent then
|
state:save_after_timeout (headset_profiles)
|
||||||
state:save_after_timeout (headset_profiles)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function getSavedHeadsetProfile (device)
|
function getSavedHeadsetProfile (device)
|
||||||
|
|
@ -54,38 +72,87 @@ function getSavedHeadsetProfile (device)
|
||||||
return headset_profiles [key]
|
return headset_profiles [key]
|
||||||
end
|
end
|
||||||
|
|
||||||
function saveNonHeadsetProfile (device, profile_name)
|
function saveLastProfile (device, profile_name)
|
||||||
non_headset_profiles [device.properties ["device.name"]] = profile_name
|
last_profiles [device.properties ["device.name"]] = profile_name
|
||||||
end
|
end
|
||||||
|
|
||||||
function getSavedNonHeadsetProfile (device)
|
function getSavedLastProfile (device)
|
||||||
return non_headset_profiles [device.properties ["device.name"]]
|
return last_profiles [device.properties ["device.name"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
function isSwitchedToHeadsetProfile (device)
|
||||||
|
return getSavedLastProfile (device) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function findProfile (device, index, name)
|
function findProfile (device, index, name)
|
||||||
for p in device:iterate_params ("EnumProfile") do
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
local profile = cutils.parseParam (p, "EnumProfile")
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
if profile ~= nil then
|
if not profile then
|
||||||
if (index ~= nil and profile.index == index) or
|
goto skip_enum_profile
|
||||||
(name ~= nil and profile.name == name) then
|
|
||||||
return profile
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
log:debug ("Profile name: " .. profile.name .. ", priority: "
|
||||||
|
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
|
||||||
|
if (index ~= nil and profile.index == index) or
|
||||||
|
(name ~= nil and profile.name == name) then
|
||||||
|
return profile.priority, profile.index, profile.name
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_enum_profile::
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return INVALID, INVALID, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function getCurrentProfile (device)
|
function getCurrentProfile (device)
|
||||||
for p in device:iterate_params ("Profile") do
|
for p in device:iterate_params ("Profile") do
|
||||||
local profile = cutils.parseParam (p, "Profile")
|
local profile = cutils.parseParam (p, "Profile")
|
||||||
if profile then
|
if profile then
|
||||||
return profile
|
return profile.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function highestPrioProfileWithInputRoute (device)
|
||||||
|
local profile_priority = INVALID
|
||||||
|
local profile_index = INVALID
|
||||||
|
local profile_name = nil
|
||||||
|
|
||||||
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
-- Parse pod
|
||||||
|
if not route then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
if route.direction ~= "Input" then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
log:debug ("Route with index: " .. tostring (route.index) .. ", direction: "
|
||||||
|
.. route.direction .. ", name: " .. route.name .. ", description: "
|
||||||
|
.. route.description .. ", priority: " .. route.priority)
|
||||||
|
if route.profiles then
|
||||||
|
for _, v in pairs (route.profiles) do
|
||||||
|
local priority, index, name = findProfile (device, v)
|
||||||
|
if priority ~= INVALID then
|
||||||
|
if profile_priority < priority then
|
||||||
|
profile_priority = priority
|
||||||
|
profile_index = index
|
||||||
|
profile_name = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_enum_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
return profile_priority, profile_index, profile_name
|
||||||
|
end
|
||||||
|
|
||||||
function hasProfileInputRoute (device, profile_index)
|
function hasProfileInputRoute (device, profile_index)
|
||||||
for p in device:iterate_params ("EnumRoute") do
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
local route = cutils.parseParam (p, "EnumRoute")
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
|
@ -100,51 +167,6 @@ function hasProfileInputRoute (device, profile_index)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function isHeadsetProfile (device, profile)
|
|
||||||
if hasProfileInputRoute (device, profile.index) and
|
|
||||||
(string.find (profile.name, "^headset%-head%-unit") or profile.name == "bap-duplex") then
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function highestPrioHeadsetProfile (device)
|
|
||||||
local found_profile = nil
|
|
||||||
for p in device:iterate_params ("EnumRoute") do
|
|
||||||
local route = cutils.parseParam (p, "EnumRoute")
|
|
||||||
if route ~= nil and route.profiles ~= nil and route.direction == "Input" then
|
|
||||||
for _, v in pairs (route.profiles) do
|
|
||||||
local p = findProfile (device, v)
|
|
||||||
if p ~= nil and isHeadsetProfile (device, p) then
|
|
||||||
if found_profile == nil or found_profile.priority < p.priority then
|
|
||||||
found_profile = p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return found_profile
|
|
||||||
end
|
|
||||||
|
|
||||||
function highestPrioNonHeadsetProfile (device)
|
|
||||||
local found_profile = nil
|
|
||||||
for p in device:iterate_params ("EnumRoute") do
|
|
||||||
local route = cutils.parseParam (p, "EnumRoute")
|
|
||||||
if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
|
|
||||||
for _, v in pairs (route.profiles) do
|
|
||||||
local p = findProfile (device, v)
|
|
||||||
if p ~= nil and not isHeadsetProfile (device, p) then
|
|
||||||
if found_profile == nil or found_profile.priority < p.priority then
|
|
||||||
found_profile = p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return found_profile
|
|
||||||
end
|
|
||||||
|
|
||||||
function switchDeviceToHeadsetProfile (dev_id, device_om)
|
function switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
-- Find the actual device
|
-- Find the actual device
|
||||||
local device = device_om:lookup {
|
local device = device_om:lookup {
|
||||||
|
|
@ -155,43 +177,49 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Do not switch if the current profile is already a headset profile
|
local cur_profile_name = getCurrentProfile (device)
|
||||||
local cur_profile = getCurrentProfile (device)
|
local priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||||
if cur_profile ~= nil and isHeadsetProfile (device, cur_profile) then
|
if hasProfileInputRoute (device, index) then
|
||||||
log:info (device,
|
log:info ("Current profile has input route, not switching")
|
||||||
"Current profile is already a headset profile, no need to switch")
|
|
||||||
return
|
|
||||||
elseif cur_profile == nil then
|
|
||||||
log:info (device, "Could not get current profile, not switching")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get saved headset profile if any, otherwise find the highest priority one
|
if isSwitchedToHeadsetProfile (device) then
|
||||||
local profile = nil
|
log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||||
local profile_name = getSavedHeadsetProfile (device)
|
return
|
||||||
if profile_name ~= nil then
|
end
|
||||||
profile = findProfile (device, nil, profile_name)
|
|
||||||
if profile ~= nil and not isHeadsetProfile (device, profile) then
|
local saved_headset_profile = getSavedHeadsetProfile (device)
|
||||||
saveHeadsetProfile (device, nil, false)
|
|
||||||
profile = nil
|
index = INVALID
|
||||||
|
if saved_headset_profile then
|
||||||
|
priority, index, name = findProfile (device, nil, saved_headset_profile)
|
||||||
|
if index ~= INVALID and not hasProfileInputRoute (device, index) then
|
||||||
|
index = INVALID
|
||||||
|
saveHeadsetProfile (device, nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if profile == nil then
|
if index == INVALID then
|
||||||
profile = highestPrioHeadsetProfile (device)
|
priority, index, name = highestPrioProfileWithInputRoute (device)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Switch if headset profile was found
|
if index ~= INVALID then
|
||||||
if profile ~= nil then
|
|
||||||
local pod = Pod.Object {
|
local pod = Pod.Object {
|
||||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||||
index = profile.index,
|
index = index
|
||||||
save = false
|
|
||||||
}
|
}
|
||||||
log:info (device, "Switching profile from: " .. cur_profile.name
|
|
||||||
.. " to: " .. profile.name)
|
-- store the current profile (needed when restoring)
|
||||||
|
saveLastProfile (device, cur_profile_name)
|
||||||
|
|
||||||
|
-- switch to headset profile
|
||||||
|
log:info ("Setting profile of '"
|
||||||
|
.. device.properties ["device.description"]
|
||||||
|
.. "' from: " .. cur_profile_name
|
||||||
|
.. " to: " .. name)
|
||||||
device:set_params ("Profile", pod)
|
device:set_params ("Profile", pod)
|
||||||
else
|
else
|
||||||
log:warning ("Could not find valid headset profile, not switching")
|
log:warning ("Got invalid index when switching profile")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -205,139 +233,142 @@ function restoreProfile (dev_id, device_om)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Do not restore if the current profile is already a non-headset profile
|
if not isSwitchedToHeadsetProfile (device) then
|
||||||
local cur_profile = getCurrentProfile (device)
|
log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
|
||||||
if cur_profile ~= nil and not isHeadsetProfile (device, cur_profile) then
|
|
||||||
log:info (device,
|
|
||||||
"Current profile is already a non-headset profile, no need to restore")
|
|
||||||
return
|
|
||||||
elseif cur_profile == nil then
|
|
||||||
log:info (device, "Could not get current profile, not switching")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get saved non-headset profile if any, otherwise find the highest priority one
|
local profile_name = getSavedLastProfile (device)
|
||||||
local profile = nil
|
local cur_profile_name = getCurrentProfile (device)
|
||||||
local profile_name = getSavedNonHeadsetProfile (device)
|
local priority, index, name
|
||||||
if profile_name ~= nil then
|
|
||||||
profile = findProfile (device, nil, profile_name)
|
if cur_profile_name then
|
||||||
if profile ~= nil and isHeadsetProfile (device, profile) then
|
priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||||
saveNonHeadsetProfile (device, nil)
|
|
||||||
profile = nil
|
if index ~= INVALID and hasProfileInputRoute (device, index) then
|
||||||
|
log:info ("Setting saved headset profile to: " .. cur_profile_name)
|
||||||
|
saveHeadsetProfile (device, cur_profile_name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if profile == nil then
|
|
||||||
profile = highestPrioNonHeadsetProfile (device)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Restore if non-headset profile was found
|
if profile_name then
|
||||||
if profile ~= nil then
|
priority, index, name = findProfile (device, nil, profile_name)
|
||||||
local pod = Pod.Object {
|
|
||||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
if index ~= INVALID then
|
||||||
index = profile.index,
|
local pod = Pod.Object {
|
||||||
save = false
|
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||||
}
|
index = index
|
||||||
log:info (device, "Restoring profile from: " .. cur_profile.name
|
}
|
||||||
.. " to: " .. profile.name)
|
|
||||||
device:set_params ("Profile", pod)
|
-- clear last profile as we will restore it now
|
||||||
else
|
saveLastProfile (device, nil)
|
||||||
log:warning ("Could not find valid non-headset profile, not switching")
|
|
||||||
|
-- restore previous profile
|
||||||
|
log:info ("Restoring profile of '"
|
||||||
|
.. device.properties ["device.description"]
|
||||||
|
.. "' from: " .. cur_profile_name
|
||||||
|
.. " to: " .. name)
|
||||||
|
device:set_params ("Profile", pod)
|
||||||
|
else
|
||||||
|
log:warning ("Failed to restore profile")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function triggerSwitchDeviceToHeadsetProfile (source, dev_id)
|
function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
-- Always clear any pending restore/switch callbacks when triggering a new switch
|
-- Always clear any pending restore/switch callbacks when triggering a new switch
|
||||||
if restore_timeout_source[dev_id] ~= nil then
|
if restore_timeout_source[dev_id] ~= nil then
|
||||||
restore_timeout_source[dev_id]:destroy ()
|
restore_timeout_source[dev_id]:destroy ()
|
||||||
restore_timeout_source[dev_id] = nil
|
restore_timeout_source[dev_id] = nil
|
||||||
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
|
|
||||||
end
|
end
|
||||||
if switch_timeout_source[dev_id] ~= nil then
|
if switch_timeout_source[dev_id] ~= nil then
|
||||||
switch_timeout_source[dev_id]:destroy ()
|
switch_timeout_source[dev_id]:destroy ()
|
||||||
switch_timeout_source[dev_id] = nil
|
switch_timeout_source[dev_id] = nil
|
||||||
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create new switch callback
|
-- create new switch callback
|
||||||
log:info ("Triggering profile switch on device " .. tostring (dev_id))
|
switch_timeout_source[dev_id] = Core.timeout_add (profile_switch_timeout_msec, function ()
|
||||||
switch_timeout_source[dev_id] = Core.timeout_add (PROFILE_SWITCH_TIMEOUT_MSEC, function ()
|
|
||||||
switch_timeout_source[dev_id] = nil
|
switch_timeout_source[dev_id] = nil
|
||||||
|
switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
local e = source:call ("create-event", "autoswitch-bluez-headset-profile", nil, nil)
|
|
||||||
e:set_data ("device-id", dev_id)
|
|
||||||
EventDispatcher.push_event (e)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function triggerRestoreProfile (source, dev_id)
|
function triggerRestoreProfile (dev_id, device_om)
|
||||||
|
-- we never restore the device profiles if there are active streams
|
||||||
|
for _, v in pairs (active_streams) do
|
||||||
|
if v == dev_id then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Always clear any pending restore/switch callbacks when triggering a new restore
|
-- Always clear any pending restore/switch callbacks when triggering a new restore
|
||||||
if switch_timeout_source[dev_id] ~= nil then
|
if switch_timeout_source[dev_id] ~= nil then
|
||||||
switch_timeout_source[dev_id]:destroy ()
|
switch_timeout_source[dev_id]:destroy ()
|
||||||
switch_timeout_source[dev_id] = nil
|
switch_timeout_source[dev_id] = nil
|
||||||
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
|
|
||||||
end
|
end
|
||||||
if restore_timeout_source[dev_id] ~= nil then
|
if restore_timeout_source[dev_id] ~= nil then
|
||||||
restore_timeout_source[dev_id]:destroy ()
|
restore_timeout_source[dev_id]:destroy ()
|
||||||
restore_timeout_source[dev_id] = nil
|
restore_timeout_source[dev_id] = nil
|
||||||
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create new restore callback
|
-- create new restore callback
|
||||||
log:info ("Triggering profile restore on device " .. tostring (dev_id))
|
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||||
restore_timeout_source[dev_id] = Core.timeout_add (PROFILE_RESTORE_TIMEOUT_MSEC, function ()
|
|
||||||
restore_timeout_source[dev_id] = nil
|
restore_timeout_source[dev_id] = nil
|
||||||
|
restoreProfile (dev_id, device_om)
|
||||||
local e = source:call ("create-event", "autoswitch-bluez-a2dp-profile", nil, nil)
|
|
||||||
e:set_data ("device-id", dev_id)
|
|
||||||
EventDispatcher.push_event (e)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups)
|
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||||
local stream_id = stream["bound-id"]
|
-- source filter
|
||||||
|
function checkStreamStatus (stream, node_om, visited_link_groups)
|
||||||
-- Make sure the node is linked
|
-- check if the stream is linked to a bluetooth loopback source
|
||||||
local link = link_om:lookup {
|
local stream_id = tonumber(stream["bound-id"])
|
||||||
Constraint { "link.input.node", "=", stream_id, type = "pw-global"}
|
local peer_id = lutils.getNodePeerId (stream_id)
|
||||||
}
|
if peer_id ~= nil then
|
||||||
if link == nil then
|
local bt_node = node_om:lookup {
|
||||||
return nil
|
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||||
end
|
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||||
local peer_id = link.properties["link.output.node"]
|
|
||||||
|
|
||||||
-- If the peer node is the BT loopback source node, return its Id.
|
|
||||||
-- Otherwise recursively advance in the graph if it is linked to a filter.
|
|
||||||
local bt_node = node_om:lookup {
|
|
||||||
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
|
||||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
|
||||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
|
||||||
}
|
|
||||||
if bt_node ~= nil then
|
|
||||||
return bt_node
|
|
||||||
else
|
|
||||||
local filter_main_node = node_om:lookup {
|
|
||||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
|
||||||
Constraint { "node.link-group", "+", type = "pw" }
|
|
||||||
}
|
}
|
||||||
if filter_main_node ~= nil then
|
if bt_node ~= nil then
|
||||||
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
local dev_id = bt_node.properties["device.id"]
|
||||||
if visited_link_groups == nil then
|
if dev_id ~= nil then
|
||||||
visited_link_groups = {}
|
-- If a stream we previously saw stops running, we consider it
|
||||||
|
-- inactive, because some applications (Teams) just cork input
|
||||||
|
-- streams, but don't close them.
|
||||||
|
if previous_streams [stream.id] == dev_id and
|
||||||
|
stream.state ~= "running" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return dev_id
|
||||||
end
|
end
|
||||||
if visited_link_groups [filter_link_group] then
|
else
|
||||||
return nil
|
-- Check if it is linked to a filter main node, and recursively advance if so
|
||||||
else
|
local filter_main_node = node_om:lookup {
|
||||||
visited_link_groups [filter_link_group] = true
|
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||||
end
|
Constraint { "node.link-group", "+", type = "pw" }
|
||||||
for filter_stream_node in node_om:iterate {
|
}
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
if filter_main_node ~= nil then
|
||||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
-- Now check all stream nodes for this filter
|
||||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||||
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
if visited_link_groups == nil then
|
||||||
} do
|
visited_link_groups = {}
|
||||||
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_node, node_om, link_om, visited_link_groups)
|
end
|
||||||
if bt_node ~= nil then
|
if visited_link_groups [filter_link_group] then
|
||||||
return bt_node
|
return nil
|
||||||
|
else
|
||||||
|
visited_link_groups [filter_link_group] = true
|
||||||
|
end
|
||||||
|
for filter_stream_node in node_om:iterate {
|
||||||
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||||
|
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
||||||
|
} do
|
||||||
|
local dev_id = checkStreamStatus (filter_stream_node, node_om, visited_link_groups)
|
||||||
|
if dev_id ~= nil then
|
||||||
|
return dev_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -346,118 +377,60 @@ function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
|
function handleStream (stream, node_om, device_om)
|
||||||
local bt_node_id = bt_node["bound-id"]
|
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local dev_id = checkStreamStatus (stream, node_om)
|
||||||
|
if dev_id ~= nil then
|
||||||
|
active_streams [stream.id] = dev_id
|
||||||
|
previous_streams [stream.id] = dev_id
|
||||||
|
triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
|
||||||
|
else
|
||||||
|
dev_id = active_streams [stream.id]
|
||||||
|
active_streams [stream.id] = nil
|
||||||
|
if dev_id ~= nil then
|
||||||
|
triggerRestoreProfile (dev_id, device_om)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function handleAllStreams (node_om, device_om)
|
||||||
for stream in node_om:iterate {
|
for stream in node_om:iterate {
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
Constraint { "node.link-group", "-", type = "pw" },
|
Constraint { "node.link-group", "-", type = "pw" },
|
||||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||||
} do
|
} do
|
||||||
local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om)
|
handleStream (stream, node_om, device_om)
|
||||||
if linked_bt_node ~= nil then
|
|
||||||
local linked_bt_node_id = linked_bt_node ["bound-id"]
|
|
||||||
if tonumber (linked_bt_node_id) == tonumber (bt_node_id) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local switch_profile_hook = AsyncEventHook {
|
SimpleEventHook {
|
||||||
name = "switch-profile@autoswitch-bluetooth-profile",
|
name = "node-removed@autoswitch-bluetooth-profile",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "autoswitch-bluez-headset-profile" },
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
},
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
},
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||||
steps = {
|
|
||||||
start = {
|
|
||||||
next = "none",
|
|
||||||
execute = function (event, transition)
|
|
||||||
local source = event:get_source ()
|
|
||||||
local device_om = source:call ("get-object-manager", "device")
|
|
||||||
local device_id = event:get_data ("device-id")
|
|
||||||
|
|
||||||
-- Switch profile
|
|
||||||
switchDeviceToHeadsetProfile (device_id, device_om)
|
|
||||||
|
|
||||||
-- Wait until the profile is applied
|
|
||||||
Core.sync (function ()
|
|
||||||
transition:advance ()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local restore_profile_hook = AsyncEventHook {
|
|
||||||
name = "restore-profile@autoswitch-bluetooth-profile",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "autoswitch-bluez-a2dp-profile" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
steps = {
|
|
||||||
start = {
|
|
||||||
next = "none",
|
|
||||||
execute = function (event, transition)
|
|
||||||
local source = event:get_source ()
|
|
||||||
local device_om = source:call ("get-object-manager", "device")
|
|
||||||
local device_id = event:get_data ("device-id")
|
|
||||||
|
|
||||||
-- Restore profile
|
|
||||||
restoreProfile (device_id, device_om)
|
|
||||||
|
|
||||||
-- Wait until the profile is applied
|
|
||||||
Core.sync (function ()
|
|
||||||
transition:advance ()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
|
|
||||||
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "evaluate-bluetooth-profiles" },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
|
local stream = event:get_subject ()
|
||||||
local source = event:get_source ()
|
local source = event:get_source ()
|
||||||
local node_om = source:call ("get-object-manager", "node")
|
local device_om = source:call ("get-object-manager", "device")
|
||||||
local link_om = source:call ("get-object-manager", "link")
|
|
||||||
|
|
||||||
-- Evaluate all bluetooth loopback source nodes, and switch to headset
|
local dev_id = active_streams[stream.id]
|
||||||
-- profile only if the node is running and linked to a stream that is not a
|
active_streams[stream.id] = nil
|
||||||
-- monitor, otherwise just restore the profile.
|
previous_streams[stream.id] = nil
|
||||||
--
|
if dev_id ~= nil then
|
||||||
-- If the bluetooth node is linked to a stream that is a monitor, its state
|
triggerRestoreProfile (dev_id, device_om)
|
||||||
-- will be 'running', so we cannot just rely on the state to know if we
|
|
||||||
-- have to switch or not, we also need to check if the node is linked to
|
|
||||||
-- a stream that is not a monitor.
|
|
||||||
for bt_node in node_om:iterate {
|
|
||||||
Constraint { "media.class", "matches", "Audio/Source" },
|
|
||||||
Constraint { "device.id", "+" },
|
|
||||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
|
||||||
} do
|
|
||||||
local bt_node_state = bt_node["state"]
|
|
||||||
local bt_dev_id = bt_node.properties ["device.id"]
|
|
||||||
|
|
||||||
if bt_node_state == "running" and
|
|
||||||
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
|
|
||||||
triggerSwitchDeviceToHeadsetProfile (source, bt_dev_id)
|
|
||||||
else
|
|
||||||
triggerRestoreProfile (source, bt_dev_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
}
|
}:register ()
|
||||||
|
|
||||||
local link_added_hook = SimpleEventHook {
|
SimpleEventHook {
|
||||||
name = "link-added@autoswitch-bluetooth-profile",
|
name = "link-added@autoswitch-bluetooth-profile",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
|
|
@ -465,164 +438,46 @@ local link_added_hook = SimpleEventHook {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
|
local link = event:get_subject ()
|
||||||
local source = event:get_source ()
|
local source = event:get_source ()
|
||||||
local node_om = source:call ("get-object-manager", "node")
|
local node_om = source:call ("get-object-manager", "node")
|
||||||
local link = event:get_subject ()
|
local device_om = source:call ("get-object-manager", "device")
|
||||||
local in_stream_id = link.properties["link.input.node"]
|
local link_props = link.properties
|
||||||
|
|
||||||
-- Only evaluate bluetooth profiles if a capture stream was linked
|
for stream in node_om:iterate {
|
||||||
local stream = node_om:lookup {
|
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
Constraint { "node.link-group", "-", type = "pw" },
|
Constraint { "node.link-group", "-", type = "pw" },
|
||||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||||
Constraint { "bound-id", "=", in_stream_id, type = "gobject" },
|
} do
|
||||||
}
|
local in_id = tonumber(link_props["link.input.node"])
|
||||||
if stream ~= nil then
|
local stream_id = tonumber(stream["bound-id"])
|
||||||
capture_stream_links [link.id] = true
|
if in_id == stream_id then
|
||||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
handleStream (stream, node_om, device_om)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
}
|
}:register ()
|
||||||
|
|
||||||
local link_removed_hook = SimpleEventHook {
|
SimpleEventHook {
|
||||||
name = "link-removed@autoswitch-bluetooth-profile",
|
name = "bluez-device-added@autoswitch-bluetooth-profile",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "link-removed" },
|
Constraint { "event.type", "=", "device-added" },
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local source = event:get_source ()
|
|
||||||
local link = event:get_subject ()
|
|
||||||
|
|
||||||
-- Only evaluate bluetooth profiles if a capture stream was unlinked
|
|
||||||
if capture_stream_links [link.id] then
|
|
||||||
capture_stream_links [link.id] = nil
|
|
||||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
local state_changed_hook = SimpleEventHook {
|
|
||||||
name = "bluez-loopback-state-changed@autoswitch-bluetooth-profile",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "node-state-changed" },
|
|
||||||
Constraint { "media.class", "matches", "Audio/Source" },
|
|
||||||
Constraint { "device.id", "+" },
|
|
||||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local source = event:get_source ()
|
|
||||||
local node = event:get_subject ()
|
|
||||||
local old_state = event:get_properties ()["event.subject.old-state"]
|
|
||||||
local new_state = event:get_properties ()["event.subject.new-state"]
|
|
||||||
|
|
||||||
log:info (node, "state changed from '" .. old_state .. "' to '" .. new_state .. "'")
|
|
||||||
|
|
||||||
-- Dont evaluate if the state changed from idle to suspended
|
|
||||||
if old_state == "idle" and new_state == "suspended" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
local node_added_hook = SimpleEventHook {
|
|
||||||
name = "bluez-loopback-added@autoswitch-bluetooth-profile",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "node-added" },
|
|
||||||
Constraint { "media.class", "matches", "Audio/Source" },
|
|
||||||
Constraint { "device.id", "+" },
|
|
||||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local source = event:get_source ()
|
|
||||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
local device_profile_changed_hook = SimpleEventHook {
|
|
||||||
name = "bluez-profile-changed@autoswitch-bluetooth-profile",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "device-params-changed" },
|
|
||||||
Constraint { "event.subject.param-id", "=", "Profile" },
|
|
||||||
Constraint { "device.api", "=", "bluez5" },
|
Constraint { "device.api", "=", "bluez5" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
local device = event:get_subject ()
|
local device = event:get_subject ()
|
||||||
|
local source = event:get_source ()
|
||||||
|
local node_om = source:call ("get-object-manager", "node")
|
||||||
|
local device_om = source:call ("get-object-manager", "device")
|
||||||
|
|
||||||
-- Always save the current profile when it changes
|
-- Devices are unswitched initially
|
||||||
local cur_profile = getCurrentProfile (device)
|
saveLastProfile (device, nil)
|
||||||
if cur_profile ~= nil then
|
|
||||||
if isHeadsetProfile (device, cur_profile) then
|
-- Handle all streams when BT device is added
|
||||||
log:info (device, "Saving headset profile " .. cur_profile.name)
|
handleAllStreams (node_om, device_om)
|
||||||
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
|
|
||||||
else
|
|
||||||
log:info (device, "Saving non-headset profile " .. cur_profile.name)
|
|
||||||
saveNonHeadsetProfile (device, cur_profile.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
}
|
}:register ()
|
||||||
|
|
||||||
function evaluatePersistentStorage ()
|
|
||||||
if Settings.get_boolean ("bluetooth.use-persistent-storage") and
|
|
||||||
not persistent_storage_hooks_registered then
|
|
||||||
state = State ("bluetooth-autoswitch")
|
|
||||||
headset_profiles = state:load ()
|
|
||||||
persistent_storage_hooks_registered = true
|
|
||||||
elseif persistent_storage_hooks_registered then
|
|
||||||
state = nil
|
|
||||||
headset_profiles = {}
|
|
||||||
persistent_storage_hooks_registered = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function evaluateAutoswitch ()
|
|
||||||
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") and
|
|
||||||
not autoswitch_hooks_registered then
|
|
||||||
capture_stream_links = {}
|
|
||||||
restore_timeout_source = {}
|
|
||||||
switch_timeout_source = {}
|
|
||||||
switch_profile_hook:register ()
|
|
||||||
restore_profile_hook:register ()
|
|
||||||
evaluate_bluetooth_profiles_hook:register ()
|
|
||||||
link_added_hook:register ()
|
|
||||||
link_removed_hook:register ()
|
|
||||||
state_changed_hook:register ()
|
|
||||||
node_added_hook:register ()
|
|
||||||
device_profile_changed_hook:register ()
|
|
||||||
autoswitch_hooks_registered = true
|
|
||||||
elseif autoswitch_hooks_registered then
|
|
||||||
capture_stream_links = nil
|
|
||||||
restore_timeout_source = nil
|
|
||||||
switch_timeout_source = nil
|
|
||||||
switch_profile_hook:remove ()
|
|
||||||
restore_profile_hook:remove ()
|
|
||||||
evaluate_bluetooth_profiles_hook:remove ()
|
|
||||||
link_added_hook:remove ()
|
|
||||||
link_removed_hook:remove ()
|
|
||||||
state_changed_hook:remove ()
|
|
||||||
node_added_hook:remove ()
|
|
||||||
device_profile_changed_hook:remove ()
|
|
||||||
autoswitch_hooks_registered = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
|
|
||||||
evaluatePersistentStorage ()
|
|
||||||
end)
|
|
||||||
evaluatePersistentStorage ()
|
|
||||||
|
|
||||||
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
|
|
||||||
evaluateAutoswitch ()
|
|
||||||
end)
|
|
||||||
evaluateAutoswitch ()
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ SimpleEventHook {
|
||||||
local device = event:get_subject ()
|
local device = event:get_subject ()
|
||||||
local event_properties = event:get_properties ()
|
local event_properties = event:get_properties ()
|
||||||
local active_ids = event_properties ["profile.active-device-ids"]
|
local active_ids = event_properties ["profile.active-device-ids"]
|
||||||
local selected_routes = event:get_data ("selected-routes") or Properties()
|
local selected_routes = event:get_data ("selected-routes") or {}
|
||||||
|
|
||||||
local dev_info = devinfo:get_device_info (device)
|
local dev_info = devinfo:get_device_info (device)
|
||||||
assert (dev_info)
|
assert (dev_info)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,9 @@ find_stored_profile_hook = SimpleEventHook {
|
||||||
end
|
end
|
||||||
|
|
||||||
local device = event:get_subject ()
|
local device = event:get_subject ()
|
||||||
local device_props = device.properties
|
local dev_name = device.properties["device.name"]
|
||||||
local dev_name = device_props["device.name"]
|
|
||||||
local dont_restore_off_profile = cutils.parseBool (
|
|
||||||
device_props["session.dont-restore-off-profile"])
|
|
||||||
if not dev_name then
|
if not dev_name then
|
||||||
log:warning (device, "invalid device.name")
|
log:critical (device, "invalid device.name")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -48,8 +45,7 @@ find_stored_profile_hook = SimpleEventHook {
|
||||||
if profile_name then
|
if profile_name then
|
||||||
for p in device:iterate_params ("EnumProfile") do
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
local profile = cutils.parseParam (p, "EnumProfile")
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
if profile.name == profile_name and profile.available ~= "no" and
|
if profile.name == profile_name and profile.available ~= "no" then
|
||||||
(not dont_restore_off_profile or profile.index ~= 0) then
|
|
||||||
selected_profile = profile
|
selected_profile = profile
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
@ -91,7 +87,7 @@ function updateStoredProfile (device, profile)
|
||||||
local index = nil
|
local index = nil
|
||||||
|
|
||||||
if not dev_name then
|
if not dev_name then
|
||||||
log:warning (device, "invalid device.name")
|
log:critical (device, "invalid device.name")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ find_stored_routes_hook = SimpleEventHook {
|
||||||
local event_properties = event:get_properties ()
|
local event_properties = event:get_properties ()
|
||||||
local profile_name = event_properties ["profile.name"]
|
local profile_name = event_properties ["profile.name"]
|
||||||
local active_ids = event_properties ["profile.active-device-ids"]
|
local active_ids = event_properties ["profile.active-device-ids"]
|
||||||
local selected_routes = event:get_data ("selected-routes") or Properties()
|
local selected_routes = event:get_data ("selected-routes") or {}
|
||||||
|
|
||||||
local dev_info = devinfo:get_device_info (device)
|
local dev_info = devinfo:get_device_info (device)
|
||||||
assert (dev_info)
|
assert (dev_info)
|
||||||
|
|
@ -108,13 +108,13 @@ apply_route_props_hook = SimpleEventHook {
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
local device = event:get_subject ()
|
local device = event:get_subject ()
|
||||||
local selected_routes = event:get_data ("selected-routes") or Properties()
|
local selected_routes = event:get_data ("selected-routes") or {}
|
||||||
local new_selected_routes = {}
|
local new_selected_routes = {}
|
||||||
|
|
||||||
local dev_info = devinfo:get_device_info (device)
|
local dev_info = devinfo:get_device_info (device)
|
||||||
assert (dev_info)
|
assert (dev_info)
|
||||||
|
|
||||||
if selected_routes:get_count () == 0 then
|
if next (selected_routes) == nil then
|
||||||
log:info (device, "No routes selected to set on " .. dev_info.name)
|
log:info (device, "No routes selected to set on " .. dev_info.name)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -159,126 +159,130 @@ store_or_restore_routes_hook = AsyncEventHook {
|
||||||
},
|
},
|
||||||
steps = {
|
steps = {
|
||||||
start = {
|
start = {
|
||||||
next = "none",
|
next = "evaluate",
|
||||||
execute = function (event, transition)
|
execute = function (event, transition)
|
||||||
local source = event:get_source ()
|
|
||||||
local device = event:get_subject ()
|
|
||||||
|
|
||||||
-- Make sure the routes are always updated before evaluating them.
|
-- Make sure the routes are always updated before evaluating them.
|
||||||
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762
|
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762
|
||||||
device:enum_params ("EnumRoute", function (enum_route_it, e)
|
local device = event:get_subject ()
|
||||||
local selected_routes = {}
|
device:enum_params ("EnumRoute", function (_, e)
|
||||||
local push_select_routes = false
|
|
||||||
|
|
||||||
-- check for error
|
|
||||||
if e then
|
if e then
|
||||||
transition:return_error ("failed to enum routes: "
|
transition:return_error ("failed to enum routes: "
|
||||||
.. tostring (e));
|
.. tostring (e));
|
||||||
return
|
else
|
||||||
end
|
|
||||||
|
|
||||||
-- Make sure the device is still valid
|
|
||||||
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
|
|
||||||
transition:advance ()
|
transition:advance ()
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local dev_info = devinfo:get_device_info (device)
|
|
||||||
if not dev_info then
|
|
||||||
transition:advance ()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local new_route_infos = {}
|
|
||||||
|
|
||||||
-- look at all the routes and update/reset cached information
|
|
||||||
for p in enum_route_it:iterate() do
|
|
||||||
-- parse pod
|
|
||||||
local route = cutils.parseParam (p, "EnumRoute")
|
|
||||||
if not route then
|
|
||||||
goto skip_enum_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- find cached route information
|
|
||||||
local route_info = devinfo.find_route_info (dev_info, route, true)
|
|
||||||
if not route_info then
|
|
||||||
goto skip_enum_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- update properties
|
|
||||||
route_info.prev_active = route_info.active
|
|
||||||
route_info.active = false
|
|
||||||
route_info.save = false
|
|
||||||
|
|
||||||
-- store
|
|
||||||
new_route_infos [route.index] = route_info
|
|
||||||
|
|
||||||
::skip_enum_route::
|
|
||||||
end
|
|
||||||
|
|
||||||
-- update route_infos with new prev_active, active and save changes
|
|
||||||
dev_info.route_infos = new_route_infos
|
|
||||||
new_route_infos = nil
|
|
||||||
|
|
||||||
-- check for changes in the active routes
|
|
||||||
for p in device:iterate_params ("Route") do
|
|
||||||
local route = cutils.parseParam (p, "Route")
|
|
||||||
if not route then
|
|
||||||
goto skip_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get cached route info and at the same time
|
|
||||||
-- ensure that the route is also in EnumRoute
|
|
||||||
local route_info = devinfo.find_route_info (dev_info, route, false)
|
|
||||||
if not route_info then
|
|
||||||
goto skip_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- update route_info state
|
|
||||||
route_info.active = true
|
|
||||||
route_info.save = route.save
|
|
||||||
|
|
||||||
if not route_info.prev_active then
|
|
||||||
-- a new route is now active, restore the volume and
|
|
||||||
-- make sure we save this as a preferred route
|
|
||||||
log:info (device,
|
|
||||||
string.format ("new active route(%s) found of device(%s)",
|
|
||||||
route.name, dev_info.name))
|
|
||||||
route_info.prev_active = true
|
|
||||||
route_info.active = true
|
|
||||||
|
|
||||||
selected_routes [tostring (route.device)] =
|
|
||||||
Json.Object { index = route_info.index }:to_string ()
|
|
||||||
push_select_routes = true
|
|
||||||
|
|
||||||
elseif route.available ~= "no" and route.save and route.props then
|
|
||||||
-- just save route properties
|
|
||||||
log:info (device,
|
|
||||||
string.format ("storing route(%s) props of device(%s)",
|
|
||||||
route.name, dev_info.name))
|
|
||||||
|
|
||||||
saveRouteProps (dev_info, route)
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_route::
|
|
||||||
end
|
|
||||||
|
|
||||||
-- save selected routes for the active profile
|
|
||||||
for p in device:iterate_params ("Profile") do
|
|
||||||
local profile = cutils.parseParam (p, "Profile")
|
|
||||||
saveProfileRoutes (dev_info, profile.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- push a select-routes event to re-apply the routes with new properties
|
|
||||||
if push_select_routes then
|
|
||||||
local e = source:call ("create-event", "select-routes", device, nil)
|
|
||||||
e:set_data ("selected-routes", selected_routes)
|
|
||||||
EventDispatcher.push_event (e)
|
|
||||||
end
|
|
||||||
|
|
||||||
transition:advance ()
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
},
|
||||||
|
evaluate = {
|
||||||
|
next = "none",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local source = event:get_source ()
|
||||||
|
local selected_routes = {}
|
||||||
|
local push_select_routes = false
|
||||||
|
|
||||||
|
-- Make sure the device is still valid
|
||||||
|
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
|
||||||
|
transition:advance ()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
if not dev_info then
|
||||||
|
transition:advance ()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_route_infos = {}
|
||||||
|
|
||||||
|
-- look at all the routes and update/reset cached information
|
||||||
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
|
-- parse pod
|
||||||
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
if not route then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find cached route information
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route, true)
|
||||||
|
if not route_info then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update properties
|
||||||
|
route_info.prev_active = route_info.active
|
||||||
|
route_info.active = false
|
||||||
|
route_info.save = false
|
||||||
|
|
||||||
|
-- store
|
||||||
|
new_route_infos [route.index] = route_info
|
||||||
|
|
||||||
|
::skip_enum_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update route_infos with new prev_active, active and save changes
|
||||||
|
dev_info.route_infos = new_route_infos
|
||||||
|
new_route_infos = nil
|
||||||
|
|
||||||
|
-- check for changes in the active routes
|
||||||
|
for p in device:iterate_params ("Route") do
|
||||||
|
local route = cutils.parseParam (p, "Route")
|
||||||
|
if not route then
|
||||||
|
goto skip_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- get cached route info and at the same time
|
||||||
|
-- ensure that the route is also in EnumRoute
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route, false)
|
||||||
|
if not route_info then
|
||||||
|
goto skip_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update route_info state
|
||||||
|
route_info.active = true
|
||||||
|
route_info.save = route.save
|
||||||
|
|
||||||
|
if not route_info.prev_active then
|
||||||
|
-- a new route is now active, restore the volume and
|
||||||
|
-- make sure we save this as a preferred route
|
||||||
|
log:info (device,
|
||||||
|
string.format ("new active route(%s) found of device(%s)",
|
||||||
|
route.name, dev_info.name))
|
||||||
|
route_info.prev_active = true
|
||||||
|
route_info.active = true
|
||||||
|
|
||||||
|
selected_routes [tostring (route.device)] =
|
||||||
|
Json.Object { index = route_info.index }:to_string ()
|
||||||
|
push_select_routes = true
|
||||||
|
|
||||||
|
elseif route.available ~= "no" and route.save and route.props then
|
||||||
|
-- just save route properties
|
||||||
|
log:info (device,
|
||||||
|
string.format ("storing route(%s) props of device(%s)",
|
||||||
|
route.name, dev_info.name))
|
||||||
|
|
||||||
|
saveRouteProps (dev_info, route)
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- save selected routes for the active profile
|
||||||
|
for p in device:iterate_params ("Profile") do
|
||||||
|
local profile = cutils.parseParam (p, "Profile")
|
||||||
|
saveProfileRoutes (dev_info, profile.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- push a select-routes event to re-apply the routes with new properties
|
||||||
|
if push_select_routes then
|
||||||
|
local e = source:call ("create-event", "select-routes", device, nil)
|
||||||
|
e:set_data ("selected-routes", selected_routes)
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
end
|
||||||
|
|
||||||
|
transition:advance ()
|
||||||
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Hooks
|
||||||
|
|
||||||
The hooks in this section are organized in 3 sub-categories. The first category
|
The hooks in this section are organized in 3 sub-categories. The first category
|
||||||
includes hooks that are triggered by changes in the graph. Some of them are tasked
|
includes hooks that are triggered by changes in the graph. Some of them are tasked
|
||||||
to schedule a "rescan-for-linking" event, which is the lowest priority linking event and
|
to schedule a "rescan-for-linking" event, which is the lowest priority event and
|
||||||
its purpose is to scan through all the linkable session items and link them
|
its purpose is to scan through all the linkable session items and link them
|
||||||
to a particular target. The "rescan-for-linking" event is always scheduled to run
|
to a particular target. The "rescan-for-linking" event is always scheduled to run
|
||||||
once for all the graph changes in a cycle. This is achieved by flagging the event
|
once for all the graph changes in a cycle. This is achieved by flagging the event
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ SimpleEventHook {
|
||||||
name = "linking/find-default-target",
|
name = "linking/find-default-target",
|
||||||
after = { "linking/find-defined-target",
|
after = { "linking/find-defined-target",
|
||||||
"linking/find-filter-target",
|
"linking/find-filter-target",
|
||||||
"linking/find-media-role-target",
|
"linking/find-media-role-target" },
|
||||||
"linking/find-media-role-sink-target" },
|
|
||||||
before = "linking/prepare-link",
|
before = "linking/prepare-link",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2025 Phosh.mobi e.V.
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
--
|
|
||||||
-- Pick up a preferred target node for the output stream of role-based loopbacks
|
|
||||||
|
|
||||||
lutils = require ("linking-utils")
|
|
||||||
cutils = require ("common-utils")
|
|
||||||
|
|
||||||
log = Log.open_topic ("s-linking")
|
|
||||||
|
|
||||||
SimpleEventHook {
|
|
||||||
name = "linking/find-media-role-sink-target",
|
|
||||||
after = { "linking/find-defined-target",
|
|
||||||
"linking/find-media-role-target" },
|
|
||||||
before = "linking/prepare-link",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "select-target" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local _, om, si, si_props, _, target =
|
|
||||||
lutils:unwrap_select_target_event (event)
|
|
||||||
|
|
||||||
local node_name = si_props["node.name"]
|
|
||||||
local target_direction = cutils.getTargetDirection (si_props)
|
|
||||||
local media_class = si_props["media.class"]
|
|
||||||
local link_group = si_props["node.link-group"]
|
|
||||||
local is_virtual = si_props["node.virtual"]
|
|
||||||
|
|
||||||
log:info (si, string.format ("Lookup for '%s' (%s) / '%s' / '%s'",
|
|
||||||
node_name, tostring (si_props ["node.id"]), media_class, link_group))
|
|
||||||
|
|
||||||
--- bypass the hook if the target is already set or there's no link group
|
|
||||||
if target or media_class ~= "Stream/Output/Audio" or not is_virtual or link_group == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
--- We link the output node but the relevant properties are on the input node
|
|
||||||
--- of the link group
|
|
||||||
local input_node = om:lookup {
|
|
||||||
type = "SiLinkable",
|
|
||||||
Constraint { "media.class", "=", "Audio/Sink" },
|
|
||||||
Constraint { "node.link-group", "=", link_group },
|
|
||||||
}
|
|
||||||
|
|
||||||
if input_node == nil then
|
|
||||||
log:warning (si, string.format("No input node for %s found", link_group))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local target_name = input_node.properties["policy.role-based.preferred-target"]
|
|
||||||
--- no preferred target
|
|
||||||
if target_name == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local si_target = om:lookup {
|
|
||||||
type = "SiLinkable",
|
|
||||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
|
||||||
Constraint { "node.name", "=", target_name },
|
|
||||||
}
|
|
||||||
if si_target == nil then
|
|
||||||
si_target = om:lookup {
|
|
||||||
type = "SiLinkable",
|
|
||||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
|
||||||
Constraint { "node.nick", "=", target_name },
|
|
||||||
}
|
|
||||||
end
|
|
||||||
if si_target then
|
|
||||||
log:info (si,
|
|
||||||
string.format ("... role based sink target picked: %s (%s)",
|
|
||||||
tostring (si_target.properties ["node.name"]),
|
|
||||||
tostring (si_target.properties ["node.id"])))
|
|
||||||
event:set_data ("target", si_target)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}:register ()
|
|
||||||
|
|
@ -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 ()
|
|
||||||
|
|
@ -26,7 +26,7 @@ function checkFilter (si, om, handle_nonstreams)
|
||||||
|
|
||||||
-- always return true if this is not a filter
|
-- always return true if this is not a filter
|
||||||
local node = si:get_associated_proxy ("node")
|
local node = si:get_associated_proxy ("node")
|
||||||
local link_group = node:get_property ("node.link-group")
|
local link_group = node.properties["node.link-group"]
|
||||||
if link_group == nil then
|
if link_group == nil then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
@ -43,34 +43,36 @@ function checkFilter (si, om, handle_nonstreams)
|
||||||
end
|
end
|
||||||
|
|
||||||
function checkLinkable (si, om, handle_nonstreams)
|
function checkLinkable (si, om, handle_nonstreams)
|
||||||
|
local si_props = si.properties
|
||||||
|
|
||||||
-- For the rest of them, only handle stream session items
|
-- For the rest of them, only handle stream session items
|
||||||
if si:get_property ("item.node.type") ~= "stream" and
|
if not si_props or (si_props ["item.node.type"] ~= "stream"
|
||||||
not handle_nonstreams then
|
and not handle_nonstreams) then
|
||||||
return false
|
return false, si_props
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check filters
|
-- check filters
|
||||||
if not checkFilter (si, om, handle_nonstreams) then
|
if not checkFilter (si, om, handle_nonstreams) then
|
||||||
return false
|
return false, si_props
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true, si_props
|
||||||
end
|
end
|
||||||
|
|
||||||
function unhandleLinkable (si, om)
|
function unhandleLinkable (si, om)
|
||||||
if not checkLinkable (si, om, true) then
|
local si_id = si.id
|
||||||
|
local valid, si_props = checkLinkable (si, om, true)
|
||||||
|
if not valid then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local si_id = si.id
|
|
||||||
log:info (si, string.format ("unhandling item %d", si_id))
|
log:info (si, string.format ("unhandling item %d", si_id))
|
||||||
|
|
||||||
-- iterate over all the links in the graph and
|
-- iterate over all the links in the graph and
|
||||||
-- remove any links associated with this item
|
-- remove any links associated with this item
|
||||||
for silink in om:iterate { type = "SiLink" } do
|
for silink in om:iterate { type = "SiLink" } do
|
||||||
local silink_props = silink.properties
|
local out_id = tonumber (silink.properties ["out.item.id"])
|
||||||
local out_id = silink_props:get_int ("out.item.id")
|
local in_id = tonumber (silink.properties ["in.item.id"])
|
||||||
local in_id = silink_props:get_int ("in.item.id")
|
|
||||||
|
|
||||||
if out_id == si_id or in_id == si_id then
|
if out_id == si_id or in_id == si_id then
|
||||||
local in_flags = lutils:get_flags (in_id)
|
local in_flags = lutils:get_flags (in_id)
|
||||||
|
|
@ -82,7 +84,7 @@ function unhandleLinkable (si, om)
|
||||||
out_flags.peer_id = nil
|
out_flags.peer_id = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if silink_props:get_boolean ("is.role.policy.link") then
|
if cutils.parseBool (silink.properties["is.role.policy.link"]) then
|
||||||
lutils.clearPriorityMediaRoleLink(silink)
|
lutils.clearPriorityMediaRoleLink(silink)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -111,67 +113,17 @@ SimpleEventHook {
|
||||||
end
|
end
|
||||||
}:register ()
|
}:register ()
|
||||||
|
|
||||||
-- Handle newly added linkable immediately without waiting for full rescan
|
|
||||||
-- Only for simple cases where we know it won't affect other parts of the graph
|
|
||||||
SimpleEventHook {
|
|
||||||
name = "linking/linkable-added-immediate",
|
|
||||||
before = "linking/rescan-trigger",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "session-item-added" },
|
|
||||||
Constraint { "event.session-item.interface", "=", "linkable" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local si = event:get_subject ()
|
|
||||||
local source = event:get_source ()
|
|
||||||
local om = source:call ("get-object-manager", "session-item")
|
|
||||||
|
|
||||||
if not checkLinkable (si, om, false) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Don't handle immediately if this is a smart filter that could affect other nodes
|
|
||||||
local node = si:get_associated_proxy ("node")
|
|
||||||
local link_group = node:get_property ("node.link-group")
|
|
||||||
if link_group then
|
|
||||||
local direction = cutils.getTargetDirection (si.properties)
|
|
||||||
if futils.is_filter_smart (direction, link_group) then
|
|
||||||
-- Smart filters need full rescan to handle cascading effects
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Only handle if autoconnect is enabled
|
|
||||||
local autoconnect = si:get_property ("node.autoconnect")
|
|
||||||
if autoconnect ~= "true" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if this is a simple stream (most common case)
|
|
||||||
-- Don't handle device nodes or special nodes that might become default targets
|
|
||||||
if si:get_property ("item.node.type") ~= "stream" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Push select-target event immediately for simple stream case
|
|
||||||
source:call ("push-event", "select-target", si, nil)
|
|
||||||
end
|
|
||||||
}:register ()
|
|
||||||
|
|
||||||
function handleLinkables (source)
|
function handleLinkables (source)
|
||||||
local om = source:call ("get-object-manager", "session-item")
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
|
||||||
for si in om:iterate { type = "SiLinkable" } do
|
for si in om:iterate { type = "SiLinkable" } do
|
||||||
if not checkLinkable (si, om) then
|
local valid, si_props = checkLinkable (si, om)
|
||||||
|
if not valid then
|
||||||
goto skip_linkable
|
goto skip_linkable
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get properties
|
|
||||||
local si_props = si.properties
|
|
||||||
|
|
||||||
-- check if we need to link this node at all
|
-- check if we need to link this node at all
|
||||||
local autoconnect = si_props:get_boolean ("node.autoconnect")
|
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
|
||||||
if not autoconnect then
|
if not autoconnect then
|
||||||
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
|
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
|
||||||
goto skip_linkable
|
goto skip_linkable
|
||||||
|
|
@ -203,7 +155,7 @@ SimpleEventHook {
|
||||||
Constraint { "node.link-group", "+" },
|
Constraint { "node.link-group", "+" },
|
||||||
} do
|
} do
|
||||||
local node = si:get_associated_proxy ("node")
|
local node = si:get_associated_proxy ("node")
|
||||||
local link_group = node:get_property ("node.link-group")
|
local link_group = node.properties["node.link-group"]
|
||||||
local direction = cutils.getTargetDirection (si.properties)
|
local direction = cutils.getTargetDirection (si.properties)
|
||||||
if futils.is_filter_smart (direction, link_group) and
|
if futils.is_filter_smart (direction, link_group) and
|
||||||
futils.is_filter_disabled (direction, link_group) then
|
futils.is_filter_disabled (direction, link_group) then
|
||||||
|
|
@ -218,6 +170,11 @@ SimpleEventHook {
|
||||||
SimpleEventHook {
|
SimpleEventHook {
|
||||||
name = "linking/rescan-trigger",
|
name = "linking/rescan-trigger",
|
||||||
interests = {
|
interests = {
|
||||||
|
-- on linkable added or removed, where linkable is adapter or plain node
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
|
},
|
||||||
-- on device Routes changed
|
-- on device Routes changed
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "device-params-changed" },
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
|
@ -278,6 +235,7 @@ SimpleEventHook {
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
local si = event:get_subject ()
|
local si = event:get_subject ()
|
||||||
|
local si_props = si.properties
|
||||||
local source = event:get_source ()
|
local source = event:get_source ()
|
||||||
|
|
||||||
-- clear timeout source, if any
|
-- clear timeout source, if any
|
||||||
|
|
|
||||||
|
|
@ -297,9 +297,9 @@ function createNode(parent, id, obj_type, factory, properties)
|
||||||
properties["node.description"] = desc:gsub("(:)", " ")
|
properties["node.description"] = desc:gsub("(:)", " ")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- add api.alsa.card.* and alsa.* properties for rule matching purposes
|
-- add api.alsa.card.* properties for rule matching purposes
|
||||||
for k, v in pairs(dev_props) do
|
for k, v in pairs(dev_props) do
|
||||||
if k:find("^api%.alsa%.card%..*") or k:find("^alsa%..*") then
|
if k:find("^api%.alsa%.card%..*") then
|
||||||
properties[k] = v
|
properties[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -494,11 +494,6 @@ function prepareDevice(parent, id, obj_type, factory, properties)
|
||||||
factory = "api.alsa.acp.device"
|
factory = "api.alsa.acp.device"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- use HDMI channel detection if enabled in settings
|
|
||||||
if Settings.get_boolean ("monitor.alsa.autodetect-hdmi-channels") then
|
|
||||||
properties["api.acp.use-eld-channels"] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- use device reservation, if available
|
-- use device reservation, if available
|
||||||
if rd_plugin and properties["api.alsa.card"] then
|
if rd_plugin and properties["api.alsa.card"] then
|
||||||
local rd_name = "Audio" .. properties["api.alsa.card"]
|
local rd_name = "Audio" .. properties["api.alsa.card"]
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ end
|
||||||
|
|
||||||
function createMonitor()
|
function createMonitor()
|
||||||
local monitor_props = {}
|
local monitor_props = {}
|
||||||
for k, v in pairs(config.properties or Properties()) do
|
for k, v in pairs(config.properties or {}) do
|
||||||
monitor_props[k] = v
|
monitor_props[k] = v
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
COMBINE_OFFSET = 64
|
COMBINE_OFFSET = 64
|
||||||
LOOPBACK_SOURCE_ID = 128
|
LOOPBACK_SOURCE_ID = 128
|
||||||
DEVICE_SOURCE_ID = 0
|
DEVICE_SOURCE_ID = 0
|
||||||
DEVICE_SINK_ID = 1
|
|
||||||
|
|
||||||
cutils = require ("common-utils")
|
cutils = require ("common-utils")
|
||||||
log = Log.open_topic ("s-monitors")
|
log = Log.open_topic ("s-monitors")
|
||||||
|
|
@ -21,10 +20,13 @@ config.rules = Conf.get_section_as_json ("monitor.bluez.rules", Json.Array {})
|
||||||
-- This is not a setting, it must always be enabled
|
-- This is not a setting, it must always be enabled
|
||||||
config.properties["api.bluez5.connection-info"] = true
|
config.properties["api.bluez5.connection-info"] = true
|
||||||
|
|
||||||
|
-- Properties used for previously creating a SCO source node. key: SPA device id
|
||||||
|
sco_source_node_properties = {}
|
||||||
|
|
||||||
|
|
||||||
devices_om = ObjectManager {
|
devices_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
type = "device",
|
type = "device",
|
||||||
Constraint { "device.api", "=", "bluez5" },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,15 +213,13 @@ function createSetNode(parent, id, type, factory, properties)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local combine_props = properties:parse ()
|
properties["node.virtual"] = false
|
||||||
combine_props["node.virtual"] = false
|
properties["device.api"] = "bluez5"
|
||||||
combine_props["device.api"] = "bluez5"
|
properties["api.bluez5.set.members"] = nil
|
||||||
combine_props["api.bluez5.set.members"] = nil
|
properties["api.bluez5.set.channels"] = nil
|
||||||
combine_props["api.bluez5.set.channels"] = nil
|
properties["api.bluez5.set.leader"] = true
|
||||||
combine_props["api.bluez5.set.leader"] = true
|
properties["audio.position"] = Json.Array (channels)
|
||||||
combine_props["audio.position"] = Json.Array (channels)
|
args["combine.props"] = Json.Object (properties)
|
||||||
|
|
||||||
args["combine.props"] = Json.Object (combine_props)
|
|
||||||
args["stream.props"] = Json.Object {}
|
args["stream.props"] = Json.Object {}
|
||||||
args["stream.rules"] = Json.Array (rules)
|
args["stream.rules"] = Json.Array (rules)
|
||||||
|
|
||||||
|
|
@ -230,12 +230,6 @@ function createSetNode(parent, id, type, factory, properties)
|
||||||
return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties)
|
return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties)
|
||||||
end
|
end
|
||||||
|
|
||||||
function getNodeName (prefix, bt_address, dev_name, node_id)
|
|
||||||
local name = prefix .. "." .. (bt_address or dev_name) .. "." .. tostring(node_id)
|
|
||||||
-- sanitize name
|
|
||||||
return name:gsub("([^%w_%-%.])", "_")
|
|
||||||
end
|
|
||||||
|
|
||||||
function createNode(parent, id, type, factory, properties)
|
function createNode(parent, id, type, factory, properties)
|
||||||
local dev_props = parent.properties
|
local dev_props = parent.properties
|
||||||
local parent_id = parent["bound-id"]
|
local parent_id = parent["bound-id"]
|
||||||
|
|
@ -264,11 +258,25 @@ function createNode(parent, id, type, factory, properties)
|
||||||
-- sanitize description, replace ':' with ' '
|
-- sanitize description, replace ':' with ' '
|
||||||
properties["node.description"] = desc:gsub("(:)", " ")
|
properties["node.description"] = desc:gsub("(:)", " ")
|
||||||
|
|
||||||
-- set the node name
|
|
||||||
local name_prefix = ((factory:find("sink") and "bluez_output") or
|
local name_prefix = ((factory:find("sink") and "bluez_output") or
|
||||||
(factory:find("source") and "bluez_input" or factory))
|
(factory:find("source") and "bluez_input" or factory))
|
||||||
properties["node.name"] = getNodeName (name_prefix,
|
|
||||||
properties["api.bluez5.address"], dev_props["device.name"], id)
|
-- hide the source node because we use the loopback source instead
|
||||||
|
if parent:get_managed_object (LOOPBACK_SOURCE_ID) ~= nil and
|
||||||
|
(factory == "api.bluez5.sco.source" or
|
||||||
|
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"]))) then
|
||||||
|
properties["bluez5.loopback-target"] = true
|
||||||
|
properties["api.bluez5.internal"] = true
|
||||||
|
-- add 'internal' to name prefix to not be confused with loopback node
|
||||||
|
name_prefix = name_prefix .. "_internal"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the node name
|
||||||
|
local name = name_prefix .. "." ..
|
||||||
|
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
|
||||||
|
tostring(id)
|
||||||
|
-- sanitize name
|
||||||
|
properties["node.name"] = name:gsub("([^%w_%-%.])", "_")
|
||||||
|
|
||||||
-- set priority
|
-- set priority
|
||||||
if not properties["priority.driver"] then
|
if not properties["priority.driver"] then
|
||||||
|
|
@ -296,16 +304,10 @@ function createNode(parent, id, type, factory, properties)
|
||||||
parent:set_managed_pending(id)
|
parent:set_managed_pending(id)
|
||||||
else
|
else
|
||||||
log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id))
|
log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id))
|
||||||
|
properties["bluez5.loopback"] = false
|
||||||
-- Set sink/source specific properties
|
if factory == "api.bluez5.sco.source" then
|
||||||
if factory == "api.bluez5.sco.source" or
|
sco_source_node_properties[parent_spa_id] = properties
|
||||||
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"])) then
|
|
||||||
properties["bluez5.loopback"] = false
|
|
||||||
if properties["api.bluez5.profile"] ~= "headset-audio-gateway" then
|
|
||||||
properties["api.bluez5.internal"] = true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local node = LocalNode("adapter", properties)
|
local node = LocalNode("adapter", properties)
|
||||||
node:activate(Feature.Proxy.BOUND)
|
node:activate(Feature.Proxy.BOUND)
|
||||||
parent:store_managed_object(id, node)
|
parent:store_managed_object(id, node)
|
||||||
|
|
@ -315,9 +317,15 @@ end
|
||||||
function removeNode(parent, id)
|
function removeNode(parent, id)
|
||||||
local dev_props = parent.properties
|
local dev_props = parent.properties
|
||||||
local parent_spa_id = tonumber(dev_props["spa.object.id"])
|
local parent_spa_id = tonumber(dev_props["spa.object.id"])
|
||||||
|
local src_properties = sco_source_node_properties[parent_spa_id]
|
||||||
|
|
||||||
log:debug("Remove node: " .. tostring (id))
|
log:debug("Remove node: " .. tostring (id))
|
||||||
|
|
||||||
|
if src_properties ~= nil and id == tonumber(src_properties["spa.object.id"]) then
|
||||||
|
log:debug("Clear old SCO properties")
|
||||||
|
sco_source_node_properties[parent_spa_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Clear also the device set module, if any
|
-- Clear also the device set module, if any
|
||||||
parent:store_managed_object(id + COMBINE_OFFSET, nil)
|
parent:store_managed_object(id + COMBINE_OFFSET, nil)
|
||||||
end
|
end
|
||||||
|
|
@ -391,7 +399,7 @@ function createDevice(parent, id, type, factory, properties)
|
||||||
end
|
end
|
||||||
|
|
||||||
function removeDevice(parent, id)
|
function removeDevice(parent, id)
|
||||||
log:debug("Remove device: " .. tostring (id))
|
sco_source_node_properties[id] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function createMonitor()
|
function createMonitor()
|
||||||
|
|
@ -409,18 +417,7 @@ function createMonitor()
|
||||||
return monitor
|
return monitor
|
||||||
end
|
end
|
||||||
|
|
||||||
function CreateDeviceLoopbackSource (dev_props, dev_id)
|
function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
|
||||||
local dev_name = dev_props["api.bluez5.address"] or dev_props["device.name"]
|
|
||||||
local dec_desc = dev_props["device.description"] or dev_props["device.name"]
|
|
||||||
or dev_props["device.nick"] or dev_props["device.alias"] or "bluetooth-device"
|
|
||||||
local target_object = getNodeName ("bluez_input",
|
|
||||||
dev_props["api.bluez5.address"], dev_props["device.name"], DEVICE_SOURCE_ID)
|
|
||||||
|
|
||||||
-- sanitize description, replace ':' with ' '
|
|
||||||
dec_desc = dec_desc:gsub("(:)", " ")
|
|
||||||
|
|
||||||
log:info("create SCO source loopback node: " .. dev_name)
|
|
||||||
|
|
||||||
local args = Json.Object {
|
local args = Json.Object {
|
||||||
["capture.props"] = Json.Object {
|
["capture.props"] = Json.Object {
|
||||||
["node.name"] = string.format ("bluez_capture_internal.%s", dev_name),
|
["node.name"] = string.format ("bluez_capture_internal.%s", dev_name),
|
||||||
|
|
@ -435,7 +432,6 @@ function CreateDeviceLoopbackSource (dev_props, dev_id)
|
||||||
["node.dont-fallback"] = true,
|
["node.dont-fallback"] = true,
|
||||||
["node.linger"] = true,
|
["node.linger"] = true,
|
||||||
["state.restore-props"] = false,
|
["state.restore-props"] = false,
|
||||||
["target.object"] = target_object,
|
|
||||||
},
|
},
|
||||||
["playback.props"] = Json.Object {
|
["playback.props"] = Json.Object {
|
||||||
["node.name"] = string.format ("bluez_input.%s", dev_name),
|
["node.name"] = string.format ("bluez_input.%s", dev_name),
|
||||||
|
|
@ -446,8 +442,15 @@ function CreateDeviceLoopbackSource (dev_props, dev_id)
|
||||||
["device.id"] = dev_id,
|
["device.id"] = dev_id,
|
||||||
["card.profile.device"] = DEVICE_SOURCE_ID,
|
["card.profile.device"] = DEVICE_SOURCE_ID,
|
||||||
["device.routes"] = "1",
|
["device.routes"] = "1",
|
||||||
|
["priority.driver"] = 2010,
|
||||||
["priority.session"] = 2010,
|
["priority.session"] = 2010,
|
||||||
["bluez5.loopback"] = true,
|
["bluez5.loopback"] = true,
|
||||||
|
["filter.smart"] = true,
|
||||||
|
["filter.smart.target"] = Json.Object {
|
||||||
|
["bluez5.loopback-target"] = true,
|
||||||
|
["bluez5.loopback"] = false,
|
||||||
|
["device.id"] = dev_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||||
|
|
@ -458,6 +461,11 @@ function checkProfiles (dev)
|
||||||
local props = dev.properties
|
local props = dev.properties
|
||||||
local device_spa_id = tonumber(props["spa.object.id"])
|
local device_spa_id = tonumber(props["spa.object.id"])
|
||||||
|
|
||||||
|
-- Don't create loopback source device if autoswitch is disabled
|
||||||
|
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Get the associated BT SpaDevice
|
-- Get the associated BT SpaDevice
|
||||||
local internal_id = tostring (props["spa.object.id"])
|
local internal_id = tostring (props["spa.object.id"])
|
||||||
local spa_device = monitor:get_managed_object (internal_id)
|
local spa_device = monitor:get_managed_object (internal_id)
|
||||||
|
|
@ -465,48 +473,51 @@ function checkProfiles (dev)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if the device supports headset profile
|
-- Ignore devices that don't support both A2DP sink and HSP/HFP profiles
|
||||||
|
local has_a2dpsink_profile = false
|
||||||
local has_headset_profile = false
|
local has_headset_profile = false
|
||||||
for p in dev:iterate_params("EnumProfile") do
|
for p in dev:iterate_params("EnumProfile") do
|
||||||
local profile = cutils.parseParam (p, "EnumProfile")
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
if profile.name:find ("headset") then
|
if profile.name:find ("a2dp") and profile.name:find ("sink") then
|
||||||
|
has_a2dpsink_profile = true
|
||||||
|
elseif profile.name:find ("headset") then
|
||||||
has_headset_profile = true
|
has_headset_profile = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if not has_a2dpsink_profile or not has_headset_profile then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if has_headset_profile then
|
-- Create the loopback device if never created before
|
||||||
-- Always create the source loopback device if autoswitch is enabled.
|
local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||||
-- Otherwise, only create the source loopback device if the current profile
|
if loopback == nil then
|
||||||
-- is headset, and destroy the source loopback deivce if the current profile
|
local dev_name = props["api.bluez5.address"] or props["device.name"]
|
||||||
-- is A2DP.
|
local dec_desc = props["device.description"] or props["device.name"]
|
||||||
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
|
||||||
-- Create source loopback
|
|
||||||
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
|
||||||
if source_loopback == nil and has_headset_profile then
|
|
||||||
source_loopback = CreateDeviceLoopbackSource (props, device_id)
|
|
||||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Check if current profile is headset
|
|
||||||
local is_current_profile_headset = false
|
|
||||||
for p in dev:iterate_params("Profile") do
|
|
||||||
local profile = cutils.parseParam (p, "Profile")
|
|
||||||
if profile.name:find ("headset") then
|
|
||||||
is_current_profile_headset = true
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
if is_current_profile_headset then
|
log:info("create SCO loopback node: " .. dev_name)
|
||||||
-- Create source loopback
|
|
||||||
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
-- sanitize description, replace ':' with ' '
|
||||||
if source_loopback == nil and has_headset_profile then
|
dec_desc = dec_desc:gsub("(:)", " ")
|
||||||
source_loopback = CreateDeviceLoopbackSource (props, device_id)
|
loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
|
||||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
|
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback)
|
||||||
end
|
|
||||||
else
|
-- recreate any sco source node
|
||||||
-- Destroy source loopback
|
local properties = sco_source_node_properties[device_spa_id]
|
||||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, nil)
|
if properties ~= nil then
|
||||||
|
local node_id = tonumber(properties["spa.object.id"])
|
||||||
|
local node = spa_device:get_managed_object (node_id)
|
||||||
|
if node ~= nil then
|
||||||
|
log:info("Recreate node: " .. properties["node.name"] .. ": " ..
|
||||||
|
properties["factory.name"] .. " " .. tostring (node_id))
|
||||||
|
|
||||||
|
spa_device:store_managed_object(node_id, nil)
|
||||||
|
|
||||||
|
properties["bluez5.loopback-target"] = true
|
||||||
|
properties["api.bluez5.internal"] = true
|
||||||
|
node = LocalNode("adapter", properties)
|
||||||
|
node:activate(Feature.Proxy.BOUND)
|
||||||
|
spa_device:store_managed_object(node_id, node)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -515,12 +526,16 @@ end
|
||||||
function onDeviceParamsChanged (dev, param_name)
|
function onDeviceParamsChanged (dev, param_name)
|
||||||
if param_name == "EnumProfile" then
|
if param_name == "EnumProfile" then
|
||||||
checkProfiles (dev)
|
checkProfiles (dev)
|
||||||
elseif param_name == "Profile" then
|
|
||||||
checkProfiles (dev)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
devices_om:connect("object-added", function(_, dev)
|
devices_om:connect("object-added", function(_, dev)
|
||||||
|
-- Ignore all devices that are not BT devices
|
||||||
|
if dev.properties["device.api"] ~= "bluez5" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check available profiles
|
||||||
dev:connect ("params-changed", onDeviceParamsChanged)
|
dev:connect ("params-changed", onDeviceParamsChanged)
|
||||||
checkProfiles (dev)
|
checkProfiles (dev)
|
||||||
end)
|
end)
|
||||||
|
|
@ -551,15 +566,3 @@ end
|
||||||
nodes_om:activate()
|
nodes_om:activate()
|
||||||
devices_om:activate()
|
devices_om:activate()
|
||||||
device_set_nodes_om:activate()
|
device_set_nodes_om:activate()
|
||||||
|
|
||||||
function evaluateAutoswitch ()
|
|
||||||
-- Evaluate loopbacks on all BT devices
|
|
||||||
for dev in devices_om:iterate () do
|
|
||||||
checkProfiles (dev)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
|
|
||||||
evaluateAutoswitch ()
|
|
||||||
end)
|
|
||||||
evaluateAutoswitch ()
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ SimpleEventHook {
|
||||||
-- Create group loopback module if it does not exist
|
-- Create group loopback module if it does not exist
|
||||||
local m = group_loopback_modules [direction][group]
|
local m = group_loopback_modules [direction][group]
|
||||||
if m == nil then
|
if m == nil then
|
||||||
Log.info ("Creating " .. direction .. " loopback for audio group " .. group ..
|
Log.warning ("Creating " .. direction .. " loopback for audio group " .. group ..
|
||||||
(target_object and (" with target object " .. tostring (target_object)) or ""))
|
(target_object and (" with target object " .. tostring (target_object)) or ""))
|
||||||
m = CreateStreamLoopback (stream_props, group, target_object, direction)
|
m = CreateStreamLoopback (stream_props, group, target_object, direction)
|
||||||
group_loopback_modules [direction][group] = m
|
group_loopback_modules [direction][group] = m
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ items = {}
|
||||||
function configProperties (node)
|
function configProperties (node)
|
||||||
local properties = node.properties
|
local properties = node.properties
|
||||||
local media_class = properties ["media.class"] or ""
|
local media_class = properties ["media.class"] or ""
|
||||||
local factory_name = properties ["factory.name"] or ""
|
|
||||||
|
|
||||||
-- ensure a media.type is set
|
-- ensure a media.type is set
|
||||||
if not properties ["media.type"] then
|
if not properties ["media.type"] then
|
||||||
|
|
@ -41,7 +40,6 @@ function configProperties (node)
|
||||||
properties ["item.features.control-port"] =
|
properties ["item.features.control-port"] =
|
||||||
Settings.get_boolean ("node.features.audio.control-port")
|
Settings.get_boolean ("node.features.audio.control-port")
|
||||||
properties ["item.features.mono"] =
|
properties ["item.features.mono"] =
|
||||||
(factory_name == "api.alsa.pcm.sink" or factory_name == "api.bluez5.a2dp.sink") and
|
|
||||||
Settings.get_boolean ("node.features.audio.mono")
|
Settings.get_boolean ("node.features.audio.mono")
|
||||||
properties ["node.id"] = node ["bound-id"]
|
properties ["node.id"] = node ["bound-id"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2025 The WirePlumber project contributors
|
|
||||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
log = Log.open_topic("s-node")
|
|
||||||
|
|
||||||
config = {}
|
|
||||||
config.rules = Conf.get_section_as_json ("node.filter-graph.rules", Json.Array{})
|
|
||||||
|
|
||||||
function setNodeFilterGraphParams (node, graph_params)
|
|
||||||
local pod = Pod.Object {
|
|
||||||
"Spa:Pod:Object:Param:Props", "Props",
|
|
||||||
params = Pod.Struct (graph_params)
|
|
||||||
}
|
|
||||||
node:set_params("Props", pod)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
SimpleEventHook {
|
|
||||||
name = "node/create-filter-graph",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "node-added" },
|
|
||||||
Constraint { "library.name", "=", "audioconvert/libspa-audioconvert", type = "pw" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function(event)
|
|
||||||
local node = event:get_subject()
|
|
||||||
|
|
||||||
JsonUtils.match_rules (config.rules, node.properties, function (action, value)
|
|
||||||
|
|
||||||
if action == "create-filter-graph" then
|
|
||||||
local graphs = value:parse (1)
|
|
||||||
|
|
||||||
local graph_params = {}
|
|
||||||
for idx, val in ipairs (graphs) do
|
|
||||||
local index = tonumber(idx) - 1
|
|
||||||
local key = "audioconvert.filter-graph." .. tostring (index)
|
|
||||||
|
|
||||||
log:info (node, "setting node filter graph param '" .. key .. "' to: " .. val)
|
|
||||||
|
|
||||||
table.insert(graph_params, key)
|
|
||||||
table.insert(graph_params, val)
|
|
||||||
end
|
|
||||||
|
|
||||||
setNodeFilterGraphParams (node, graph_params)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
}:register()
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2025 Phosh.mobi e.V.
|
|
||||||
-- @author Guido Günther <agx@sigxcpu.org>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
--
|
|
||||||
-- Select the media role default volume
|
|
||||||
|
|
||||||
log = Log.open_topic("s-node")
|
|
||||||
|
|
||||||
local cutils = require ("common-utils")
|
|
||||||
|
|
||||||
function findHighestPriorityRoleNode (node_om)
|
|
||||||
local best_role = nil
|
|
||||||
local best_prio = 0
|
|
||||||
|
|
||||||
local default_role = Settings.get ("node.stream.default-media-role")
|
|
||||||
if default_role then
|
|
||||||
default_role = default_role:parse()
|
|
||||||
end
|
|
||||||
|
|
||||||
for ni in node_om:iterate {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "=", "Audio/Sink" },
|
|
||||||
Constraint { "node.name", "#", "input.loopback.sink.role.*" },
|
|
||||||
} do
|
|
||||||
local ni_props = ni.properties
|
|
||||||
local roles = ni_props["device.intended-roles"]
|
|
||||||
local node_name = ni_props ["node.name"]
|
|
||||||
local prio = tonumber(ni_props ["policy.role-based.priority"])
|
|
||||||
|
|
||||||
-- Use the node that handles the default_role as fallback
|
|
||||||
-- when no node is in running state
|
|
||||||
if best_role == nil and roles and default_role then
|
|
||||||
local roles_table = Json.Raw(roles):parse()
|
|
||||||
for i, v in ipairs (roles_table) do
|
|
||||||
if default_role == v then
|
|
||||||
best_role = node_name
|
|
||||||
best_prio = prio
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if ni.state == "running" then
|
|
||||||
if prio > best_prio then
|
|
||||||
best_role = node_name
|
|
||||||
best_prio = prio
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
log:info (string.format ("Volume control is on : '%s', prio %d", best_role, best_prio))
|
|
||||||
local metadata = cutils.get_default_metadata_object ()
|
|
||||||
metadata:set (0, "current.role-based.volume.control", "Spa:String:JSON",
|
|
||||||
Json.Object { ["name"] = best_role }:to_string ())
|
|
||||||
end
|
|
||||||
|
|
||||||
SimpleEventHook {
|
|
||||||
name = "node/rescan-for-media-role-volume",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "rescan-for-media-role-volume" }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local source = event:get_source ()
|
|
||||||
local node_om = source:call ("get-object-manager", "node")
|
|
||||||
findHighestPriorityRoleNode (node_om)
|
|
||||||
end
|
|
||||||
}:register ()
|
|
||||||
|
|
||||||
-- Track best volume control for media role based priorities
|
|
||||||
SimpleEventHook {
|
|
||||||
name = "node/find-media-role-default-volume",
|
|
||||||
interests = {
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "node-added" },
|
|
||||||
Constraint { "media.class", "=", "Audio/Sink" },
|
|
||||||
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
|
|
||||||
},
|
|
||||||
EventInterest {
|
|
||||||
Constraint { "event.type", "=", "node-state-changed" },
|
|
||||||
Constraint { "media.class", "=", "Audio/Sink" },
|
|
||||||
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute = function (event)
|
|
||||||
local source = event:get_source ()
|
|
||||||
local node_om = source:call ("get-object-manager", "node")
|
|
||||||
source:call ("schedule-rescan", "media-role-volume")
|
|
||||||
end
|
|
||||||
}:register ()
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,6 @@ executable('wpctl',
|
||||||
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep, libintl_dep],
|
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep, libintl_dep],
|
||||||
)
|
)
|
||||||
|
|
||||||
install_data('shell-completion/wpctl.bash',
|
|
||||||
install_dir: get_option('datadir') / 'bash-completion/completions',
|
|
||||||
rename: 'wpctl'
|
|
||||||
)
|
|
||||||
|
|
||||||
install_data('shell-completion/wpctl.zsh',
|
install_data('shell-completion/wpctl.zsh',
|
||||||
install_dir: get_option('datadir') / 'zsh/site-functions',
|
install_dir: get_option('datadir') / 'zsh/site-functions',
|
||||||
rename: '_wpctl'
|
rename: '_wpctl'
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
_wpctl_pw_defaults() {
|
|
||||||
local defaults="@DEFAULT_SINK@ @DEFAULT_AUDIO_SINK@ @DEFAULT_SOURCE@
|
|
||||||
@DEFAULT_AUDIO_SOURCE@ @DEFAULT_VIDEO_SOURCE@"
|
|
||||||
COMPREPLY+=($(compgen -W "$defaults" -- "$cur"))
|
|
||||||
}
|
|
||||||
|
|
||||||
_wpctl() {
|
|
||||||
local cur prev words cword
|
|
||||||
local commands="status get-volume inspect set-default set-volume set-mute
|
|
||||||
set-profile set-route clear-default settings set-log-level
|
|
||||||
list"
|
|
||||||
|
|
||||||
_init_completion -n = || return
|
|
||||||
|
|
||||||
if [[ ${#COMP_WORDS[@]} -eq 2 ]]; then
|
|
||||||
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $prev in
|
|
||||||
get-volume | inspect | set-volume | set-mute | set-profile | set-route)
|
|
||||||
_wpctl_pw_defaults
|
|
||||||
;;
|
|
||||||
|
|
||||||
clear-default)
|
|
||||||
COMPREPLY+=($(compgen -W "0 1 2" -- "$cur"))
|
|
||||||
;;
|
|
||||||
|
|
||||||
list)
|
|
||||||
COMPREPLY+=($(compgen -W "audio video" -- "$cur"))
|
|
||||||
;;
|
|
||||||
|
|
||||||
audio|video)
|
|
||||||
if [[ ${COMP_WORDS[COMP_CWORD-2]} == "list" ]]; then
|
|
||||||
COMPREPLY+=($(compgen -W "devices sinks sources" -- "$cur"))
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
complete -F _wpctl wpctl
|
|
||||||
|
|
@ -35,31 +35,12 @@ struct _WpCtl
|
||||||
gint exit_code;
|
gint exit_code;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
LIST_MEDIA_ALL = 0,
|
|
||||||
LIST_MEDIA_AUDIO,
|
|
||||||
LIST_MEDIA_VIDEO,
|
|
||||||
} ListMediaType;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
LIST_OBJECT_ALL = 0,
|
|
||||||
LIST_OBJECT_DEVICES,
|
|
||||||
LIST_OBJECT_SINKS,
|
|
||||||
LIST_OBJECT_SOURCES,
|
|
||||||
} ListObjectType;
|
|
||||||
|
|
||||||
static struct {
|
static struct {
|
||||||
union {
|
union {
|
||||||
struct {
|
struct {
|
||||||
gboolean display_nicknames;
|
gboolean display_nicknames;
|
||||||
gboolean display_names;
|
gboolean display_names;
|
||||||
} status;
|
} status;
|
||||||
|
|
||||||
struct {
|
|
||||||
ListMediaType media_type;
|
|
||||||
ListObjectType object_type;
|
|
||||||
} list;
|
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
guint64 id;
|
guint64 id;
|
||||||
gboolean show_referenced;
|
gboolean show_referenced;
|
||||||
|
|
@ -562,159 +543,6 @@ status_run (WpCtl * self)
|
||||||
g_main_loop_quit (self->loop);
|
g_main_loop_quit (self->loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* list */
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
list_parse_positional (gint argc, gchar ** argv, GError **error)
|
|
||||||
{
|
|
||||||
cmdline.list.media_type = LIST_MEDIA_ALL;
|
|
||||||
cmdline.list.object_type = LIST_OBJECT_ALL;
|
|
||||||
|
|
||||||
if (argc < 3)
|
|
||||||
return TRUE;
|
|
||||||
|
|
||||||
if (g_strcmp0 (argv[2], "audio") == 0) {
|
|
||||||
cmdline.list.media_type = LIST_MEDIA_AUDIO;
|
|
||||||
} else if (g_strcmp0 (argv[2], "video") == 0) {
|
|
||||||
cmdline.list.media_type = LIST_MEDIA_VIDEO;
|
|
||||||
} else {
|
|
||||||
g_set_error (error, wpctl_error_domain_quark(), 0,
|
|
||||||
"'%s' is not a valid list option", argv[2]);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argc < 4)
|
|
||||||
return TRUE;
|
|
||||||
|
|
||||||
if (g_strcmp0 (argv[3], "devices") == 0) {
|
|
||||||
cmdline.list.object_type = LIST_OBJECT_DEVICES;
|
|
||||||
} else if (g_strcmp0 (argv[3], "sinks") == 0) {
|
|
||||||
cmdline.list.object_type = LIST_OBJECT_SINKS;
|
|
||||||
} else if (g_strcmp0 (argv[3], "sources") == 0) {
|
|
||||||
cmdline.list.object_type = LIST_OBJECT_SOURCES;
|
|
||||||
} else {
|
|
||||||
g_set_error (error, wpctl_error_domain_quark(), 0,
|
|
||||||
"'%s' is not a valid list option", argv[3]);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
list_prepare (WpCtl * self, GError ** error)
|
|
||||||
{
|
|
||||||
wp_object_manager_add_interest (self->om, WP_TYPE_DEVICE, NULL);
|
|
||||||
wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
|
|
||||||
wp_object_manager_add_interest (self->om, WP_TYPE_METADATA, NULL);
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct list_context
|
|
||||||
{
|
|
||||||
guint32 default_node;
|
|
||||||
const gchar *media_type;
|
|
||||||
const gchar *object_type;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
list_print_device (const GValue *item, gpointer data)
|
|
||||||
{
|
|
||||||
WpPipewireObject *obj = g_value_get_object (item);
|
|
||||||
struct list_context *context = data;
|
|
||||||
guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
|
|
||||||
const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_NAME);
|
|
||||||
printf ("%u\t%s\t%s/%s\t \n", id, name, context->media_type, context->object_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
list_print_dev_node (const GValue *item, gpointer data)
|
|
||||||
{
|
|
||||||
WpPipewireObject *obj = g_value_get_object (item);
|
|
||||||
struct list_context *context = data;
|
|
||||||
guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
|
|
||||||
gboolean is_default = (context->default_node == id);
|
|
||||||
const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_NAME);
|
|
||||||
printf ("%u\t%s\t%s/%s\t%c\n", id, name, context->media_type, context->object_type, is_default ? '*' : ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct {
|
|
||||||
const gchar *title_name;
|
|
||||||
const gchar *lower_name;
|
|
||||||
ListMediaType value;
|
|
||||||
} list_media_types[] = {
|
|
||||||
{ "Audio", "audio", LIST_MEDIA_AUDIO },
|
|
||||||
{ "Video", "video", LIST_MEDIA_VIDEO },
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
list_run (WpCtl * self)
|
|
||||||
{
|
|
||||||
struct list_context context;
|
|
||||||
g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
|
|
||||||
const ListMediaType media_filter = cmdline.list.media_type;
|
|
||||||
const ListObjectType object_filter = cmdline.list.object_type;
|
|
||||||
|
|
||||||
for (guint i = 0; i < G_N_ELEMENTS (list_media_types); i++) {
|
|
||||||
if (media_filter != LIST_MEDIA_ALL &&
|
|
||||||
media_filter != list_media_types[i].value)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const gchar *media_type = list_media_types[i].title_name;
|
|
||||||
context.media_type = list_media_types[i].lower_name;
|
|
||||||
gchar media_type_glob[16];
|
|
||||||
gchar media_class[24];
|
|
||||||
|
|
||||||
g_snprintf (media_type_glob, sizeof(media_type_glob), "*%s*", media_type);
|
|
||||||
|
|
||||||
/* Devices */
|
|
||||||
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_DEVICES) {
|
|
||||||
context.object_type = "device";
|
|
||||||
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
|
|
||||||
WP_TYPE_DEVICE,
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
|
|
||||||
NULL);
|
|
||||||
wp_iterator_foreach (it, list_print_device, &context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sinks */
|
|
||||||
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_SINKS) {
|
|
||||||
g_snprintf (media_class, sizeof(media_class), "%s/Sink", media_type);
|
|
||||||
context.default_node = -1;
|
|
||||||
context.object_type = "sink";
|
|
||||||
if (def_nodes_api)
|
|
||||||
g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
|
|
||||||
&context.default_node);
|
|
||||||
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
|
|
||||||
WP_TYPE_NODE,
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Sink*",
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
|
|
||||||
NULL);
|
|
||||||
wp_iterator_foreach (it, list_print_dev_node, &context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sources */
|
|
||||||
if (object_filter == LIST_OBJECT_ALL || object_filter == LIST_OBJECT_SOURCES) {
|
|
||||||
g_snprintf (media_class, sizeof(media_class), "%s/Source", media_type);
|
|
||||||
context.default_node = -1;
|
|
||||||
context.object_type = "source";
|
|
||||||
if (def_nodes_api)
|
|
||||||
g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
|
|
||||||
&context.default_node);
|
|
||||||
g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
|
|
||||||
WP_TYPE_NODE,
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Source*",
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
|
|
||||||
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
|
|
||||||
NULL);
|
|
||||||
wp_iterator_foreach (it, list_print_dev_node, &context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g_main_loop_quit (self->loop);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* get-volume */
|
/* get-volume */
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
|
|
@ -1042,8 +870,7 @@ set_default_run (WpCtl * self)
|
||||||
media_class = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (proxy),
|
media_class = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (proxy),
|
||||||
PW_KEY_MEDIA_CLASS);
|
PW_KEY_MEDIA_CLASS);
|
||||||
for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) {
|
for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) {
|
||||||
if (g_str_has_prefix (media_class, DEFAULT_NODE_MEDIA_CLASSES[i]) &&
|
if (!g_strcmp0 (media_class, DEFAULT_NODE_MEDIA_CLASSES[i])) {
|
||||||
!g_str_has_suffix (media_class, "/Internal")) {
|
|
||||||
gboolean res = FALSE;
|
gboolean res = FALSE;
|
||||||
const gchar *name = wp_pipewire_object_get_property (
|
const gchar *name = wp_pipewire_object_get_property (
|
||||||
WP_PIPEWIRE_OBJECT (proxy), PW_KEY_NODE_NAME);
|
WP_PIPEWIRE_OBJECT (proxy), PW_KEY_NODE_NAME);
|
||||||
|
|
@ -1135,7 +962,6 @@ do_set_volume (WpCtl * self, WpPipewireObject *proxy)
|
||||||
GVariant *variant = NULL;
|
GVariant *variant = NULL;
|
||||||
gboolean res = FALSE;
|
gboolean res = FALSE;
|
||||||
gdouble curr_volume = 1.0;
|
gdouble curr_volume = 1.0;
|
||||||
gdouble new_volume = cmdline.set_volume.volume;
|
|
||||||
guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy));
|
guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy));
|
||||||
|
|
||||||
if (cmdline.set_volume.type == 's') {
|
if (cmdline.set_volume.type == 's') {
|
||||||
|
|
@ -1148,19 +974,19 @@ do_set_volume (WpCtl * self, WpPipewireObject *proxy)
|
||||||
g_variant_lookup (variant, "volume", "d", &curr_volume);
|
g_variant_lookup (variant, "volume", "d", &curr_volume);
|
||||||
g_clear_pointer (&variant, g_variant_unref);
|
g_clear_pointer (&variant, g_variant_unref);
|
||||||
|
|
||||||
new_volume += curr_volume;
|
cmdline.set_volume.volume = (cmdline.set_volume.volume + curr_volume);
|
||||||
}
|
}
|
||||||
if (new_volume < 0) {
|
if (cmdline.set_volume.volume < 0) {
|
||||||
new_volume = 0.0;
|
cmdline.set_volume.volume = 0.0;
|
||||||
}
|
}
|
||||||
if (cmdline.set_volume.limit > 0) {
|
if (cmdline.set_volume.limit > 0) {
|
||||||
if (new_volume > cmdline.set_volume.limit) {
|
if (cmdline.set_volume.volume > cmdline.set_volume.limit) {
|
||||||
new_volume = cmdline.set_volume.limit;
|
cmdline.set_volume.volume = cmdline.set_volume.limit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g_variant_builder_add (&b, "{sv}", "volume",
|
g_variant_builder_add (&b, "{sv}", "volume",
|
||||||
g_variant_new_double (new_volume));
|
g_variant_new_double (cmdline.set_volume.volume));
|
||||||
variant = g_variant_builder_end (&b);
|
variant = g_variant_builder_end (&b);
|
||||||
|
|
||||||
g_signal_emit_by_name (mixer_api, "set-volume", id, variant, &res);
|
g_signal_emit_by_name (mixer_api, "set-volume", id, variant, &res);
|
||||||
|
|
@ -1887,16 +1713,6 @@ static const struct subcommand {
|
||||||
.prepare = status_prepare,
|
.prepare = status_prepare,
|
||||||
.run = status_run,
|
.run = status_run,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.name = "list",
|
|
||||||
.positional_args = "[audio|video] [devices|sinks|sources]",
|
|
||||||
.summary = "Displays PipeWire objects, optionally filtered by media and object type",
|
|
||||||
.description = NULL,
|
|
||||||
.entries = { { NULL } },
|
|
||||||
.parse_positional = list_parse_positional,
|
|
||||||
.prepare = list_prepare,
|
|
||||||
.run = list_run,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
.name = "get-volume",
|
.name = "get-volume",
|
||||||
.positional_args = "ID",
|
.positional_args = "ID",
|
||||||
|
|
@ -2085,8 +1901,7 @@ main (gint argc, gchar **argv)
|
||||||
ctl.context = g_option_context_new (
|
ctl.context = g_option_context_new (
|
||||||
"COMMAND [COMMAND_OPTIONS] - WirePlumber Control CLI");
|
"COMMAND [COMMAND_OPTIONS] - WirePlumber Control CLI");
|
||||||
ctl.loop = g_main_loop_new (NULL, FALSE);
|
ctl.loop = g_main_loop_new (NULL, FALSE);
|
||||||
ctl.core = wp_core_new (NULL, NULL, wp_properties_new (PW_KEY_REMOTE_NAME,
|
ctl.core = wp_core_new (NULL, NULL, NULL);
|
||||||
("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"), NULL));
|
|
||||||
ctl.om = wp_object_manager_new ();
|
ctl.om = wp_object_manager_new ();
|
||||||
|
|
||||||
/* find the subcommand */
|
/* find the subcommand */
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue