mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2025-12-24 19:00:04 +01:00
Compare commits
84 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84429b4794 | ||
|
|
58b48c0a8a | ||
|
|
af7a951bd9 | ||
|
|
ded213093d | ||
|
|
3a6f2c1e90 | ||
|
|
1846d75717 | ||
|
|
444bfc04d8 | ||
|
|
3887e1ca82 | ||
|
|
00c272670c | ||
|
|
da831fdc65 | ||
|
|
6a9e977d26 | ||
|
|
80478e7548 | ||
|
|
3fb5b775ee | ||
|
|
a5538f4167 | ||
|
|
bec20fc054 | ||
|
|
beded0214d | ||
|
|
2286152c07 | ||
|
|
94fe1cbfbd | ||
|
|
6ebf81453c | ||
|
|
ceed5dca7c | ||
|
|
84e4752f1a | ||
|
|
9e390f1121 | ||
|
|
133b82e61a | ||
|
|
9045d2439a | ||
|
|
278541f637 | ||
|
|
27b6027649 | ||
|
|
6398bf1bce | ||
|
|
f196d10e87 | ||
|
|
5071a85997 | ||
|
|
15d98f59e5 | ||
|
|
be6f2b2926 | ||
|
|
01eb206460 | ||
|
|
5a4ecceee6 | ||
|
|
a35e40c1d2 | ||
|
|
2712cbb5a9 | ||
|
|
c68eb59017 | ||
|
|
c0e047c241 | ||
|
|
238fd3c067 | ||
|
|
b80a0975c7 | ||
|
|
7ca21699a9 | ||
|
|
551353482a | ||
|
|
f0b224b210 | ||
|
|
0dad52f774 | ||
|
|
27f97f6c45 | ||
|
|
285230af67 | ||
|
|
c095ae5254 | ||
|
|
d2a49e8bc5 | ||
|
|
b2c4993ab5 | ||
|
|
ae30b4f022 | ||
|
|
962be34a2b | ||
|
|
6f5ca5a79d | ||
|
|
5ecfe9f555 | ||
|
|
93377a8b4f | ||
|
|
ee72196500 | ||
|
|
e30c2a7cd9 | ||
|
|
4239055454 | ||
|
|
f188ddfb34 | ||
|
|
fb1738932b | ||
|
|
f8be5a76e6 | ||
|
|
5c6a72e3cf | ||
|
|
7b78078ed2 | ||
|
|
6cfaf3f70d | ||
|
|
2942903d0e | ||
|
|
38a21ea191 | ||
|
|
41b310c2d5 | ||
|
|
499916b996 | ||
|
|
627b003a05 | ||
|
|
385fc83f46 | ||
|
|
f82247c42c | ||
|
|
084b3aab89 | ||
|
|
71f98c40f0 | ||
|
|
2a4aa9281c | ||
|
|
35d63a7847 | ||
|
|
d21ff24ea1 | ||
|
|
68bd93e1ed | ||
|
|
a461d9e738 | ||
|
|
ebd9d2a7d5 | ||
|
|
7eacea9da9 | ||
|
|
dcf083ef3b | ||
|
|
a6c0bb202d | ||
|
|
97d8761914 | ||
|
|
df0136ce0b | ||
|
|
a5e58536dd | ||
|
|
1bde4f2cdf |
71 changed files with 3843 additions and 948 deletions
|
|
@ -154,13 +154,22 @@ include:
|
|||
# Fedora also ships that, but without the test plugins that we need...
|
||||
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
|
||||
https://gitlab.freedesktop.org/pipewire/pipewire.git
|
||||
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
|
||||
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled
|
||||
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
|
||||
-Dvulkan=disabled -Dgstreamer=disabled -Dlibsystemd=disabled
|
||||
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
|
||||
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
|
||||
-Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
||||
# Set build options based on PipeWire version
|
||||
- |
|
||||
case "$PIPEWIRE_HEAD" in
|
||||
1.0|1.2|1.4)
|
||||
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
|
||||
;;
|
||||
*)
|
||||
export PIPEWIRE_BUILD_OPTIONS="-Dlibsystemd=disabled"
|
||||
;;
|
||||
esac
|
||||
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" $PIPEWIRE_BUILD_OPTIONS
|
||||
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled
|
||||
-Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled
|
||||
-Dgstreamer=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled
|
||||
-Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled
|
||||
-Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
||||
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
|
||||
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
|
||||
# misc environment only for wireplumber
|
||||
|
|
@ -236,6 +245,9 @@ build_on_fedora_no_docs:
|
|||
stage: build
|
||||
variables:
|
||||
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
|
||||
parallel:
|
||||
matrix:
|
||||
- PIPEWIRE_HEAD: ['master', '1.4', '1.2', '1.0']
|
||||
|
||||
build_on_ubuntu_with_gir:
|
||||
extends:
|
||||
|
|
|
|||
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
## Building and Testing
|
||||
|
||||
- To compile the project: `meson compile -C build` (compiles everything, no target needed)
|
||||
- To run tests: `meson test -C build`
|
||||
- The build artifacts always live in a directory called `build` or `builddir`.
|
||||
If `build` doesn't exist, use `-C builddir` in the meson commands.
|
||||
|
||||
## Git Workflow
|
||||
|
||||
- Main branch: `master`
|
||||
- Always create feature branches for new work
|
||||
- Use descriptive commit messages following project conventions
|
||||
- Reference GitLab MR/issue numbers in commits where applicable
|
||||
- Never commit build artifacts or temporary files
|
||||
- Use `glab` CLI tool for GitLab interactions (MRs, issues, etc.)
|
||||
|
||||
## Making a release
|
||||
|
||||
- Each release always consists of an entry in NEWS.rst, at the top of the file, which describes
|
||||
the changes between the previous release and the current one. In addition, each release is given
|
||||
a unique version number, which is present:
|
||||
1. on the section header of that NEWS.rst entry
|
||||
2. in the project() command in meson.build
|
||||
3. on the commit message of the commit that introduces the above 2 changes
|
||||
4. on the git tag that marks the above commit
|
||||
- In order to make a release:
|
||||
- Begin by analyzing the git history and the merged MRs from GitLab between the previous release
|
||||
and today. GitLab MRs that are relevant always have the new release's version number set as a
|
||||
"milestone"
|
||||
- Create a new entry in NEWS.rst describing the changes, in a similar style and format as the
|
||||
previous entries. Consolidate the changes to larger work items and also reference the relevant
|
||||
gitlab MR that corresponds to each change and/or the gitlab issues that were addressed by each
|
||||
change.
|
||||
- Make sure to move the "Past releases" section header up, so that the only 2 top-level sections
|
||||
are the new release section and the "Past releases" section.
|
||||
- Edit meson.build to change the project version to the new release number
|
||||
- Do not commit anything to git. Let the user review the changes and commit manually.
|
||||
104
NEWS.rst
104
NEWS.rst
|
|
@ -1,6 +1,105 @@
|
|||
WirePlumber 0.5.11
|
||||
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)
|
||||
|
||||
Past releases
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
WirePlumber 0.5.12
|
||||
..................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added mono audio configuration support via ``node.features.audio.mono``
|
||||
setting that can be changed at runtime with wpctl (!721)
|
||||
|
||||
- Added automatic muting of ALSA devices when a running node is removed,
|
||||
helping prevent loud audio on speakers when headsets are unplugged (!734)
|
||||
|
||||
- Added notifications API module for sending system notifications (!734)
|
||||
|
||||
- Added comprehensive wpctl man page and documentation (!735, #825)
|
||||
|
||||
- Enhanced object interest handling for PipeWire properties on session items (!738)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed race condition during shutdown in the permissions portal module that
|
||||
could cause crashes in GDBus signal handling (!748)
|
||||
|
||||
- Added device validity check in state-routes handling to prevent issues
|
||||
when devices are removed during async operations (!737, #844)
|
||||
|
||||
- Fixed Log.critical undefined function error in device-info-cache (!733)
|
||||
|
||||
- Improved device hook documentation and configuration (!736)
|
||||
|
||||
WirePlumber 0.5.11
|
||||
..................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added modem manager module for tracking voice call status and voice call
|
||||
|
|
@ -39,9 +138,6 @@ Fixes:
|
|||
|
||||
- Fixed GObject introspection closure annotation
|
||||
|
||||
Past releases
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
WirePlumber 0.5.10
|
||||
..................
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'WirePlumber'
|
||||
copyright = '2021-2024, Collabora'
|
||||
author = 'Collabora'
|
||||
copyright = '2020-2025, Collabora & contributors'
|
||||
author = 'The WirePlumber Developers'
|
||||
release = '@VERSION@'
|
||||
version = '@VERSION@'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
smartquotes = False
|
||||
|
||||
# -- Breathe configuration ---------------------------------------------------
|
||||
|
||||
extensions = [
|
||||
|
|
@ -43,3 +47,9 @@ html_css_files = ['custom.css']
|
|||
graphviz_output_format = "svg"
|
||||
|
||||
pygments_style = "friendly"
|
||||
|
||||
# -- Options for manual page output -----------------------------------------
|
||||
|
||||
man_pages = [
|
||||
('tools/wpctl', 'wpctl', 'WirePlumber Control CLI', ['The WirePlumber Developers'], 1)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -102,6 +102,26 @@ if build_doc
|
|||
install_dir: wireplumber_doc_dir,
|
||||
build_by_default: true,
|
||||
)
|
||||
|
||||
# Generate man pages directory with sphinx
|
||||
custom_target('manpages',
|
||||
command: [sphinx_p,
|
||||
'-q', # quiet
|
||||
'-E', # rebuild from scratch
|
||||
'-b', 'man', # man page builder
|
||||
'-d', '@PRIVATE_DIR@', # doctrees dir
|
||||
'-c', '@OUTDIR@', # conf.py dir
|
||||
'@CURRENT_SOURCE_DIR@/rst', # source dir
|
||||
'@OUTDIR@', # output directory
|
||||
],
|
||||
depend_files: [
|
||||
sphinx_conf, sphinx_files,
|
||||
],
|
||||
output: ['wpctl.1'],
|
||||
install: true,
|
||||
install_dir: get_option('mandir') / 'man1',
|
||||
build_by_default: true,
|
||||
)
|
||||
endif
|
||||
|
||||
# Build GObject introspection
|
||||
|
|
|
|||
|
|
@ -140,9 +140,9 @@ Policies
|
|||
for enabling devices, linking streams, granting permissions to clients,
|
||||
etc, as appropriate for a desktop system.
|
||||
|
||||
.. describe:: policy.role-priority-system
|
||||
.. describe:: policy.role-based
|
||||
|
||||
Enables the role priority system policy. This system creates virtual sinks
|
||||
Enables the role based priority system policy. This system creates virtual sinks
|
||||
that group streams based on their ``media.role`` property, and assigns a
|
||||
priority to each role. Depending on the priority configuration, lower
|
||||
priority roles may be corked or ducked when a higher priority role stream
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Synopsis:
|
|||
|
||||
$ meson -Dsession-managers="[ 'wireplumber' ]" build
|
||||
$ ninja -C build
|
||||
$ make run
|
||||
$ make -C build run
|
||||
|
||||
Run independently or without installing
|
||||
---------------------------------------
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ Table of Contents
|
|||
scripting/existing_scripts.rst
|
||||
scripting/custom_scripts.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Tools
|
||||
|
||||
tools/wpctl.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Resources
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ subdir('design')
|
|||
subdir('policies')
|
||||
subdir('library')
|
||||
subdir('scripting')
|
||||
subdir('tools')
|
||||
subdir('resources')
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ assignments:
|
|||
}
|
||||
|
||||
|
||||
After that, once the media class of a device node has been select for a
|
||||
After that, once the media class of a device node has been selected for a
|
||||
particular stream node, and there are more than 1 device node matching such
|
||||
media class, WirePlumber will select one based on a set of priorities:
|
||||
|
||||
|
|
|
|||
3
docs/rst/tools/meson.build
Normal file
3
docs/rst/tools/meson.build
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
sphinx_files += files(
|
||||
'wpctl.rst',
|
||||
)
|
||||
309
docs/rst/tools/wpctl.rst
Normal file
309
docs/rst/tools/wpctl.rst
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
wpctl(1)
|
||||
========
|
||||
|
||||
SYNOPSIS
|
||||
--------
|
||||
|
||||
**wpctl** [*COMMAND*] [*COMMAND_OPTIONS*]
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
|
||||
**wpctl** is a command-line control tool for WirePlumber, the PipeWire session
|
||||
manager. It provides an interface to inspect, control, and configure audio and
|
||||
video devices, nodes, and their properties within a PipeWire media server.
|
||||
|
||||
WirePlumber manages audio and video routing, device configuration, and session
|
||||
policies. **wpctl** allows users to interact with these components, change
|
||||
volume levels, set default devices, inspect object properties, and modify
|
||||
settings.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
|
||||
status
|
||||
^^^^^^
|
||||
|
||||
**wpctl status** [**-k**\|\ **--nick**] [**-n**\|\ **--name**]
|
||||
|
||||
Displays the current state of objects in PipeWire, including devices, sinks,
|
||||
sources, filters, and streams. Shows a hierarchical view of the audio/video
|
||||
system.
|
||||
|
||||
Options:
|
||||
**-k**, **--nick**
|
||||
Display device and node nicknames instead of descriptions
|
||||
**-n**, **--name**
|
||||
Display device and node names instead of descriptions
|
||||
|
||||
get-volume
|
||||
^^^^^^^^^^
|
||||
|
||||
**wpctl get-volume** *ID*
|
||||
|
||||
Displays volume information about the specified node, including current volume
|
||||
level and mute state.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Node ID or special identifier (see `SPECIAL IDENTIFIERS`_)
|
||||
|
||||
inspect
|
||||
^^^^^^^
|
||||
|
||||
**wpctl inspect** *ID* [**-r**\|\ **--referenced**] [**-a**\|\ **--associated**]
|
||||
|
||||
Displays detailed information about the specified object, including all
|
||||
properties and metadata.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Object ID or special identifier
|
||||
|
||||
Options:
|
||||
**-r**, **--referenced**
|
||||
Show objects that are referenced in properties
|
||||
**-a**, **--associated**
|
||||
Show associated objects
|
||||
|
||||
set-default
|
||||
^^^^^^^^^^^
|
||||
|
||||
**wpctl set-default** *ID*
|
||||
|
||||
Sets the specified device node to be the default target of its kind (capture or
|
||||
playback) for new streams that require auto-connection.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Sink or source node ID
|
||||
|
||||
set-volume
|
||||
^^^^^^^^^^
|
||||
|
||||
**wpctl set-volume** *ID* *VOL*\ [**%**]\ [**-**\|\ **+**] [**-p**\|\ **--pid**] [**-l** *LIMIT*\|\ **--limit** *LIMIT*]
|
||||
|
||||
Sets the volume of the specified node.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Node ID, special identifier, or PID (with --pid)
|
||||
*VOL*\ [**%**]\ [**-**\|\ **+**]
|
||||
Volume specification:
|
||||
|
||||
- *VOL* - Set volume to specific value (1.0 = 100%)
|
||||
- *VOL*\ **%** - Set volume to percentage (50% = 0.5)
|
||||
- *VOL*\ **+** - Increase volume by value
|
||||
- *VOL*\ **-** - Decrease volume by value
|
||||
- *VOL*\ **%+** - Increase volume by percentage
|
||||
- *VOL*\ **%-** - Decrease volume by percentage
|
||||
|
||||
Options:
|
||||
**-p**, **--pid**
|
||||
Treat ID as a process ID and affect all nodes associated with it
|
||||
**-l** *LIMIT*, **--limit** *LIMIT*
|
||||
Limit final volume to below this value (floating point, 1.0 = 100%)
|
||||
|
||||
Examples:
|
||||
Set volume to 50%: ``wpctl set-volume @DEFAULT_SINK@ 0.5``
|
||||
|
||||
Increase volume by 10%: ``wpctl set-volume 42 10%+``
|
||||
|
||||
Set volume for all nodes of PID 1234: ``wpctl set-volume --pid 1234 0.8``
|
||||
|
||||
set-mute
|
||||
^^^^^^^^
|
||||
|
||||
**wpctl set-mute** *ID* **1**\|\ **0**\|\ **toggle** [**-p**\|\ **--pid**]
|
||||
|
||||
Changes the mute state of the specified node.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Node ID, special identifier, or PID (with --pid)
|
||||
**1**\|\ **0**\|\ **toggle**
|
||||
Mute state: 1 (mute), 0 (unmute), or toggle current state
|
||||
|
||||
Options:
|
||||
**-p**, **--pid**
|
||||
Treat ID as a process ID and affect all nodes associated with it
|
||||
|
||||
set-profile
|
||||
^^^^^^^^^^^
|
||||
|
||||
**wpctl set-profile** *ID* *INDEX*
|
||||
|
||||
Sets the profile of the specified device to the given index.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Device ID or special identifier
|
||||
*INDEX*
|
||||
Profile index (integer, 0 typically means 'off')
|
||||
|
||||
set-route
|
||||
^^^^^^^^^
|
||||
|
||||
**wpctl set-route** *ID* *INDEX*
|
||||
|
||||
Sets the route of the specified device to the given index.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Device node ID or special identifier
|
||||
*INDEX*
|
||||
Route index (integer, 0 typically means 'off')
|
||||
|
||||
clear-default
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
**wpctl clear-default** [*ID*]
|
||||
|
||||
Clears the default configured node. If no ID is specified, clears all default
|
||||
nodes.
|
||||
|
||||
Arguments:
|
||||
*ID* (optional)
|
||||
Settings ID to clear (0-2 for Audio/Sink, Audio/Source, Video/Source).
|
||||
If omitted, clears all defaults.
|
||||
|
||||
settings
|
||||
^^^^^^^^
|
||||
|
||||
**wpctl settings** [*KEY*] [*VAL*] [**-d**\|\ **--delete**] [**-s**\|\ **--save**] [**-r**\|\ **--reset**]
|
||||
|
||||
Shows, changes, or removes WirePlumber settings.
|
||||
|
||||
Arguments:
|
||||
*KEY* (optional)
|
||||
Setting key name
|
||||
*VAL* (optional)
|
||||
Setting value (JSON format)
|
||||
|
||||
Options:
|
||||
**-d**, **--delete**
|
||||
Delete the saved setting value (no KEY means delete all)
|
||||
**-s**, **--save**
|
||||
Save the setting value (no KEY means save all, no VAL means current value)
|
||||
**-r**, **--reset**
|
||||
Reset the setting to its default value
|
||||
|
||||
Behavior:
|
||||
- No arguments: Show all settings
|
||||
- KEY only: Show specific setting value
|
||||
- KEY and VAL: Set specific setting value
|
||||
|
||||
set-log-level
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
**wpctl set-log-level** [*ID*] *LEVEL*
|
||||
|
||||
Sets the log level of a client.
|
||||
|
||||
Arguments:
|
||||
*ID* (optional)
|
||||
Client ID. If omitted, applies to WirePlumber. Use 0 for PipeWire server.
|
||||
*LEVEL*
|
||||
Log level (e.g., ``0``, ``1``, ``2``, ``3``, ``4``, ``5``, ``E``, ``W``, ``N``, ``I``, ``D``, ``T``).
|
||||
Use ``-`` to unset the log level.
|
||||
|
||||
SPECIAL IDENTIFIERS
|
||||
-------------------
|
||||
|
||||
Instead of numeric IDs, **wpctl** accepts these special identifiers for
|
||||
commonly used defaults:
|
||||
|
||||
**@DEFAULT_SINK@**, **@DEFAULT_AUDIO_SINK@**
|
||||
The current default audio sink (playback device)
|
||||
|
||||
**@DEFAULT_SOURCE@**, **@DEFAULT_AUDIO_SOURCE@**
|
||||
The current default audio source (capture device)
|
||||
|
||||
**@DEFAULT_VIDEO_SOURCE@**
|
||||
The current default video source (camera)
|
||||
|
||||
These identifiers are resolved at runtime to the appropriate node IDs.
|
||||
|
||||
EXIT STATUS
|
||||
-----------
|
||||
|
||||
**wpctl** returns the following exit codes:
|
||||
|
||||
0
|
||||
Success
|
||||
1
|
||||
General error (e.g., invalid arguments, connection failure)
|
||||
2
|
||||
Could not connect to PipeWire
|
||||
3
|
||||
Command-specific error (e.g., object not found)
|
||||
|
||||
EXAMPLES
|
||||
--------
|
||||
|
||||
Display system status::
|
||||
|
||||
wpctl status
|
||||
|
||||
Set default audio sink::
|
||||
|
||||
wpctl set-default 42
|
||||
|
||||
Set volume to 75% on default sink::
|
||||
|
||||
wpctl set-volume @DEFAULT_SINK@ 75%
|
||||
|
||||
Increase volume by 5% on a specific node::
|
||||
|
||||
wpctl set-volume 42 5%+
|
||||
|
||||
Mute the default source::
|
||||
|
||||
wpctl set-mute @DEFAULT_SOURCE@ 1
|
||||
|
||||
Toggle mute on default sink::
|
||||
|
||||
wpctl set-mute @DEFAULT_SINK@ toggle
|
||||
|
||||
Inspect a device with associated objects::
|
||||
|
||||
wpctl inspect --associated 30
|
||||
|
||||
Show all WirePlumber settings::
|
||||
|
||||
wpctl settings
|
||||
|
||||
Set a specific setting::
|
||||
|
||||
wpctl settings bluetooth.autoswitch true
|
||||
|
||||
Save all current settings::
|
||||
|
||||
wpctl settings --save
|
||||
|
||||
Set log level for WirePlumber to debug::
|
||||
|
||||
wpctl set-log-level D
|
||||
|
||||
Set log level for a specific client::
|
||||
|
||||
wpctl set-log-level 42 W
|
||||
|
||||
NOTES
|
||||
-----
|
||||
|
||||
Object IDs can be found using the **status** command. The hierarchical display
|
||||
shows IDs for devices, nodes, and other objects.
|
||||
|
||||
Volume values are floating-point numbers where 1.0 represents 100% volume.
|
||||
Values can exceed 1.0 to introduce volume amplification.
|
||||
|
||||
When using the **--pid** option, **wpctl** will find all audio nodes associated
|
||||
with the specified process ID and apply the operation to all of them.
|
||||
|
||||
SEE ALSO
|
||||
--------
|
||||
|
||||
**pipewire**\ (1), **pw-cli**\ (1), **pw-dump**\ (1), **wireplumber**\ (1)
|
||||
|
||||
WirePlumber Documentation: https://pipewire.pages.freedesktop.org/wireplumber/
|
||||
|
|
@ -438,7 +438,7 @@ spa_device_event_object_info (void *data, uint32_t id,
|
|||
g_autoptr (WpProperties) props = NULL;
|
||||
|
||||
type = spa_debug_type_short_name (info->type);
|
||||
props = wp_properties_new_wrap_dict (info->props);
|
||||
props = wp_properties_new_copy_dict (info->props);
|
||||
|
||||
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
|
||||
id, type, info->factory_name);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,161 @@
|
|||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
|
||||
|
||||
typedef struct _HookData HookData;
|
||||
struct _HookData
|
||||
{
|
||||
struct spa_list link;
|
||||
WpEventHook *hook;
|
||||
GPtrArray *dependencies;
|
||||
};
|
||||
|
||||
static inline HookData *
|
||||
hook_data_new (WpEventHook * hook)
|
||||
{
|
||||
HookData *hook_data = g_new0 (HookData, 1);
|
||||
spa_list_init (&hook_data->link);
|
||||
hook_data->hook = g_object_ref (hook);
|
||||
hook_data->dependencies = g_ptr_array_new ();
|
||||
return hook_data;
|
||||
}
|
||||
|
||||
static void
|
||||
hook_data_free (HookData *self)
|
||||
{
|
||||
g_clear_object (&self->hook);
|
||||
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
static inline void
|
||||
record_dependency (struct spa_list *list, const gchar *target,
|
||||
const gchar *dependency)
|
||||
{
|
||||
HookData *hook_data;
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
||||
{
|
||||
HookData *hook_data;
|
||||
if (!spa_list_is_empty (list)) {
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
sort_hooks (GPtrArray *hooks)
|
||||
{
|
||||
struct spa_list collected, result, remaining;
|
||||
HookData *sorted_hook_data = NULL;
|
||||
|
||||
spa_list_init (&collected);
|
||||
spa_list_init (&result);
|
||||
spa_list_init (&remaining);
|
||||
|
||||
for (guint i = 0; i < hooks->len; i++) {
|
||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
||||
HookData *hook_data = hook_data_new (hook);
|
||||
|
||||
/* record "after" dependencies directly */
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
||||
strv++;
|
||||
}
|
||||
|
||||
spa_list_append (&collected, &hook_data->link);
|
||||
}
|
||||
|
||||
if (!spa_list_is_empty (&collected)) {
|
||||
HookData *hook_data;
|
||||
|
||||
/* convert "before" dependencies into "after" dependencies */
|
||||
spa_list_for_each (hook_data, &collected, link) {
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
/* record hook_data->hook as a dependency of the *strv hook */
|
||||
record_dependency (&collected, *strv,
|
||||
wp_event_hook_get_name (hook_data->hook));
|
||||
strv++;
|
||||
}
|
||||
}
|
||||
|
||||
/* sort */
|
||||
while (!spa_list_is_empty (&collected)) {
|
||||
gboolean made_progress = FALSE;
|
||||
|
||||
/* examine each hook to see if its dependencies are satisfied in the
|
||||
result list; if yes, then append it to the result too */
|
||||
spa_list_consume (hook_data, &collected, link) {
|
||||
guint deps_satisfied = 0;
|
||||
|
||||
spa_list_remove (&hook_data->link);
|
||||
|
||||
for (guint i = 0; i < hook_data->dependencies->len; i++) {
|
||||
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
|
||||
/* if the dependency is already in the sorted result list or if
|
||||
it doesn't exist at all, we consider it satisfied */
|
||||
if (hook_exists_in (dep, &result) ||
|
||||
!(hook_exists_in (dep, &collected) ||
|
||||
hook_exists_in (dep, &remaining))) {
|
||||
deps_satisfied++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deps_satisfied == hook_data->dependencies->len) {
|
||||
spa_list_append (&result, &hook_data->link);
|
||||
made_progress = TRUE;
|
||||
} else {
|
||||
spa_list_append (&remaining, &hook_data->link);
|
||||
}
|
||||
}
|
||||
|
||||
if (made_progress) {
|
||||
/* run again with the remaining hooks */
|
||||
spa_list_insert_list (&collected, &remaining);
|
||||
spa_list_init (&remaining);
|
||||
}
|
||||
else if (!spa_list_is_empty (&remaining)) {
|
||||
/* if we did not make any progress towards growing the result list,
|
||||
it means the dependencies cannot be satisfied because of circles */
|
||||
spa_list_consume (hook_data, &result, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
spa_list_consume (hook_data, &remaining, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* clear hooks and add the sorted ones */
|
||||
g_ptr_array_set_size (hooks, 0);
|
||||
spa_list_consume (sorted_hook_data, &result, link) {
|
||||
spa_list_remove (&sorted_hook_data->link);
|
||||
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
|
||||
hook_data_free (sorted_hook_data);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
typedef struct _EventData EventData;
|
||||
struct _EventData
|
||||
{
|
||||
|
|
@ -49,7 +204,8 @@ struct _WpEventDispatcher
|
|||
GObject parent;
|
||||
|
||||
GWeakRef core;
|
||||
GPtrArray *hooks; /* registered hooks */
|
||||
GHashTable *defined_hooks; /* registered hooks for defined events */
|
||||
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
|
||||
GSource *source; /* the event loop source */
|
||||
GList *events; /* the events stack */
|
||||
struct spa_system *system;
|
||||
|
|
@ -160,7 +316,9 @@ static void
|
|||
wp_event_dispatcher_init (WpEventDispatcher * self)
|
||||
{
|
||||
g_weak_ref_init (&self->core, NULL);
|
||||
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
||||
(GDestroyNotify)g_ptr_array_unref);
|
||||
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
|
||||
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
|
||||
((WpEventSource *) self->source)->dispatcher = self;
|
||||
|
|
@ -184,7 +342,8 @@ wp_event_dispatcher_finalize (GObject * object)
|
|||
|
||||
close (self->eventfd);
|
||||
|
||||
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
|
||||
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
|
||||
g_weak_ref_clear (&self->core);
|
||||
|
||||
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
|
||||
|
|
@ -284,6 +443,10 @@ void
|
|||
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
||||
WpEventHook * hook)
|
||||
{
|
||||
g_autoptr (GPtrArray) event_types = NULL;
|
||||
gboolean is_defined = FALSE;
|
||||
const gchar *hook_name;
|
||||
|
||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||
|
||||
|
|
@ -292,7 +455,74 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
|||
g_return_if_fail (already_registered_dispatcher == NULL);
|
||||
|
||||
wp_event_hook_set_dispatcher (hook, self);
|
||||
g_ptr_array_add (self->hooks, g_object_ref (hook));
|
||||
|
||||
/* Register the event hook in the defined hooks table if it is defined */
|
||||
hook_name = wp_event_hook_get_name (hook);
|
||||
event_types = wp_event_hook_get_matching_event_types (hook);
|
||||
if (event_types) {
|
||||
for (guint i = 0; i < event_types->len; i++) {
|
||||
const gchar *event_type = g_ptr_array_index (event_types, i);
|
||||
GPtrArray *hooks;
|
||||
|
||||
wp_debug_object (self, "Registering hook %s for defined event type %s",
|
||||
hook_name, event_type);
|
||||
|
||||
/* Check if the event type was registered in the hash table */
|
||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
||||
if (hooks) {
|
||||
g_ptr_array_add (hooks, g_object_ref (hook));
|
||||
if (!sort_hooks (hooks))
|
||||
goto sort_error;
|
||||
} else {
|
||||
GPtrArray *new_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
/* Add undefined hooks */
|
||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
||||
WpEventHook *uh = g_ptr_array_index (self->undefined_hooks, i);
|
||||
g_ptr_array_add (new_hooks, g_object_ref (uh));
|
||||
}
|
||||
/* Add current hook */
|
||||
g_ptr_array_add (new_hooks, g_object_ref (hook));
|
||||
g_hash_table_insert (self->defined_hooks, g_strdup (event_type),
|
||||
new_hooks);
|
||||
if (!sort_hooks (new_hooks))
|
||||
goto sort_error;
|
||||
}
|
||||
|
||||
is_defined = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise just register it as undefined hook */
|
||||
if (!is_defined) {
|
||||
GHashTableIter iter;
|
||||
gpointer value;
|
||||
|
||||
wp_debug_object (self, "Registering hook %s for undefined event types",
|
||||
hook_name);
|
||||
|
||||
/* Add it to the defined hooks table */
|
||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
||||
GPtrArray *defined_hooks = value;
|
||||
g_ptr_array_add (defined_hooks, g_object_ref (hook));
|
||||
if (!sort_hooks (defined_hooks))
|
||||
goto sort_error;
|
||||
}
|
||||
|
||||
/* Add it to the undefined hooks */
|
||||
g_ptr_array_add (self->undefined_hooks, g_object_ref (hook));
|
||||
if (!sort_hooks (self->undefined_hooks))
|
||||
goto sort_error;
|
||||
}
|
||||
|
||||
wp_info_object (self, "Registered hook %s successfully", hook_name);
|
||||
return;
|
||||
|
||||
sort_error:
|
||||
/* Unregister hook */
|
||||
wp_event_dispatcher_unregister_hook (self, hook);
|
||||
wp_warning_object (self,
|
||||
"Could not register hook %s because of circular dependencies", hook_name);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
@ -306,6 +536,9 @@ void
|
|||
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||
WpEventHook * hook)
|
||||
{
|
||||
GHashTableIter iter;
|
||||
gpointer value;
|
||||
|
||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||
|
||||
|
|
@ -314,11 +547,29 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
|||
g_return_if_fail (already_registered_dispatcher == self);
|
||||
|
||||
wp_event_hook_set_dispatcher (hook, NULL);
|
||||
g_ptr_array_remove_fast (self->hooks, hook);
|
||||
|
||||
/* Remove hook from defined table and undefined list */
|
||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
||||
GPtrArray *defined_hooks = value;
|
||||
g_ptr_array_remove (defined_hooks, hook);
|
||||
}
|
||||
g_ptr_array_remove (self->undefined_hooks, hook);
|
||||
}
|
||||
|
||||
static void
|
||||
add_unique (GPtrArray *array, WpEventHook * hook)
|
||||
{
|
||||
for (guint i = 0; i < array->len; i++)
|
||||
if (g_ptr_array_index (array, i) == hook)
|
||||
return;
|
||||
g_ptr_array_add (array, g_object_ref (hook));
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to iterate over all the registered hooks
|
||||
* \deprecated Use \ref wp_event_dispatcher_new_hooks_for_event_type_iterator
|
||||
* instead.
|
||||
* \ingroup wpeventdispatcher
|
||||
*
|
||||
* \param self the event dispatcher
|
||||
|
|
@ -327,7 +578,56 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
|||
WpIterator *
|
||||
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
||||
{
|
||||
GPtrArray *items =
|
||||
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
|
||||
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
GHashTableIter iter;
|
||||
gpointer value;
|
||||
|
||||
/* Add all defined hooks */
|
||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
||||
GPtrArray *hooks = value;
|
||||
for (guint i = 0; i < hooks->len; i++) {
|
||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
||||
add_unique (items, hook);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add all undefined hooks */
|
||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
||||
WpEventHook *hook = g_ptr_array_index (self->undefined_hooks, i);
|
||||
add_unique (items, hook);
|
||||
}
|
||||
|
||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to iterate over the registered hooks for a
|
||||
* particular event type.
|
||||
* \ingroup wpeventdispatcher
|
||||
*
|
||||
* \param self the event dispatcher
|
||||
* \param event_type the event type
|
||||
* \return (transfer full): a new iterator
|
||||
* \since 0.5.13
|
||||
*/
|
||||
WpIterator *
|
||||
wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
||||
WpEventDispatcher * self, const gchar *event_type)
|
||||
{
|
||||
GPtrArray *items;
|
||||
GPtrArray *hooks;
|
||||
|
||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
||||
if (hooks) {
|
||||
wp_debug_object (self, "Using %d defined hooks for event type %s",
|
||||
hooks->len, event_type);
|
||||
} else {
|
||||
hooks = self->undefined_hooks;
|
||||
wp_debug_object (self, "Using %d undefined hooks for event type %s",
|
||||
hooks->len, event_type);
|
||||
}
|
||||
|
||||
items = g_ptr_array_copy (hooks, (GCopyFunc) g_object_ref, NULL);
|
||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
|||
WpEventHook * hook);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
|
||||
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
||||
G_GNUC_DEPRECATED_FOR (wp_event_dispatcher_new_hooks_for_event_type_iterator);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
||||
WpEventDispatcher * self, const gchar *event_type);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,24 @@ wp_event_hook_run (WpEventHook * self,
|
|||
callback_data);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets all the matching event types for this hook if any.
|
||||
*
|
||||
* \ingroup wpeventhook
|
||||
* \param self the event hook
|
||||
* \returns (element-type gchar*) (transfer full) (nullable): the matching
|
||||
* event types for this hook if any.
|
||||
* \since 0.5.13
|
||||
*/
|
||||
GPtrArray *
|
||||
wp_event_hook_get_matching_event_types (WpEventHook * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
|
||||
g_return_val_if_fail (
|
||||
WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
|
||||
return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finishes the async operation that was started by wp_event_hook_run()
|
||||
*
|
||||
|
|
@ -321,36 +339,63 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
|
|||
wp_interest_event_hook_get_instance_private (self);
|
||||
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
|
||||
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
||||
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
|
||||
guint i;
|
||||
WpObjectInterest *interest = NULL;
|
||||
WpInterestMatch match;
|
||||
|
||||
const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
|
||||
WP_INTEREST_MATCH_PW_PROPERTIES |
|
||||
WP_INTEREST_MATCH_G_PROPERTIES);
|
||||
|
||||
for (i = 0; i < priv->interests->len; i++) {
|
||||
interest = g_ptr_array_index (priv->interests, i);
|
||||
match = wp_object_interest_matches_full (interest,
|
||||
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
|
||||
gtype, subject, properties, properties);
|
||||
|
||||
/* the interest may have a GType that matches the GType of the subject
|
||||
or it may have WP_TYPE_EVENT as its GType, in which case it will
|
||||
match any type of subject */
|
||||
if (match == WP_INTEREST_MATCH_ALL)
|
||||
return TRUE;
|
||||
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
|
||||
match = wp_object_interest_matches_full (interest, 0,
|
||||
WP_TYPE_EVENT, NULL, NULL, NULL);
|
||||
if (match & WP_INTEREST_MATCH_GTYPE)
|
||||
if (wp_object_interest_matches_full (interest,
|
||||
WP_INTEREST_MATCH_FLAGS_NONE,
|
||||
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
add_unique (GPtrArray *array, const gchar * lookup)
|
||||
{
|
||||
for (guint i = 0; i < array->len; i++)
|
||||
if (g_str_equal (g_ptr_array_index (array, i), lookup))
|
||||
return;
|
||||
g_ptr_array_add (array, g_strdup (lookup));
|
||||
}
|
||||
|
||||
static GPtrArray *
|
||||
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
|
||||
{
|
||||
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
|
||||
WpInterestEventHookPrivate *priv =
|
||||
wp_interest_event_hook_get_instance_private (self);
|
||||
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 res;
|
||||
}
|
||||
|
||||
static void
|
||||
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
|
||||
{
|
||||
|
|
@ -359,6 +404,8 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
|
|||
|
||||
object_class->finalize = wp_interest_event_hook_finalize;
|
||||
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,8 +39,10 @@ struct _WpEventHookClass
|
|||
|
||||
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
|
||||
|
||||
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
|
||||
|
||||
/*< private >*/
|
||||
WP_PADDING(5)
|
||||
WP_PADDING(4)
|
||||
};
|
||||
|
||||
WP_API
|
||||
|
|
@ -67,6 +69,9 @@ void wp_event_hook_run (WpEventHook * self,
|
|||
WpEvent * event, GCancellable * cancellable,
|
||||
GAsyncReadyCallback callback, gpointer callback_data);
|
||||
|
||||
WP_API
|
||||
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
|
||||
GError ** error);
|
||||
|
|
|
|||
262
lib/wp/event.c
262
lib/wp/event.c
|
|
@ -17,37 +17,11 @@
|
|||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
|
||||
|
||||
typedef struct _HookData HookData;
|
||||
struct _HookData
|
||||
{
|
||||
struct spa_list link;
|
||||
WpEventHook *hook;
|
||||
GPtrArray *dependencies;
|
||||
};
|
||||
|
||||
static inline HookData *
|
||||
hook_data_new (WpEventHook * hook)
|
||||
{
|
||||
HookData *hook_data = g_new0 (HookData, 1);
|
||||
spa_list_init (&hook_data->link);
|
||||
hook_data->hook = g_object_ref (hook);
|
||||
hook_data->dependencies = g_ptr_array_new ();
|
||||
return hook_data;
|
||||
}
|
||||
|
||||
static void
|
||||
hook_data_free (HookData *self)
|
||||
{
|
||||
g_clear_object (&self->hook);
|
||||
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
struct _WpEvent
|
||||
{
|
||||
grefcount ref;
|
||||
GData *datalist;
|
||||
struct spa_list hooks;
|
||||
GPtrArray *hooks;
|
||||
|
||||
/* immutable fields */
|
||||
gint priority;
|
||||
|
|
@ -96,7 +70,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
|
|||
WpEvent * self = g_new0 (WpEvent, 1);
|
||||
g_ref_count_init (&self->ref);
|
||||
g_datalist_init (&self->datalist);
|
||||
spa_list_init (&self->hooks);
|
||||
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
|
||||
self->priority = priority;
|
||||
self->properties = properties ?
|
||||
|
|
@ -155,11 +129,7 @@ wp_event_get_name(WpEvent *self)
|
|||
static void
|
||||
wp_event_free (WpEvent * self)
|
||||
{
|
||||
HookData *hook_data;
|
||||
spa_list_consume (hook_data, &self->hooks, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
||||
g_datalist_clear (&self->datalist);
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
g_clear_object (&self->source);
|
||||
|
|
@ -316,33 +286,6 @@ wp_event_get_data (WpEvent * self, const gchar * key)
|
|||
return g_datalist_get_data (&self->datalist, key);
|
||||
}
|
||||
|
||||
static inline void
|
||||
record_dependency (struct spa_list *list, const gchar *target,
|
||||
const gchar *dependency)
|
||||
{
|
||||
HookData *hook_data;
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
||||
{
|
||||
HookData *hook_data;
|
||||
if (!spa_list_is_empty (list)) {
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Collects all the hooks registered in the \a dispatcher that run for
|
||||
* this \a event
|
||||
|
|
@ -355,199 +298,37 @@ hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
|||
gboolean
|
||||
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
||||
{
|
||||
struct spa_list collected, result, remaining;
|
||||
g_autoptr (WpIterator) all_hooks = NULL;
|
||||
g_auto (GValue) value = G_VALUE_INIT;
|
||||
const gchar *event_type = NULL;
|
||||
|
||||
g_return_val_if_fail (event != NULL, FALSE);
|
||||
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
|
||||
|
||||
/* hooks already collected */
|
||||
if (!spa_list_is_empty (&event->hooks))
|
||||
return TRUE;
|
||||
/* Clear all current hooks */
|
||||
g_ptr_array_set_size (event->hooks, 0);
|
||||
|
||||
spa_list_init (&collected);
|
||||
spa_list_init (&result);
|
||||
spa_list_init (&remaining);
|
||||
/* Get the event type */
|
||||
event_type = wp_properties_get (event->properties, "event.type");
|
||||
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
|
||||
event->name, event_type);
|
||||
|
||||
/* collect hooks that run for this event */
|
||||
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
|
||||
/* Collect hooks that run for this event */
|
||||
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
|
||||
event_type);
|
||||
while (wp_iterator_next (all_hooks, &value)) {
|
||||
WpEventHook *hook = g_value_get_object (&value);
|
||||
|
||||
if (wp_event_hook_runs_for_event (hook, event)) {
|
||||
HookData *hook_data = hook_data_new (hook);
|
||||
|
||||
/* record "after" dependencies directly */
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
||||
strv++;
|
||||
}
|
||||
|
||||
spa_list_append (&collected, &hook_data->link);
|
||||
|
||||
g_ptr_array_add (event->hooks, g_object_ref (hook));
|
||||
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
||||
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
|
||||
}
|
||||
|
||||
g_value_unset (&value);
|
||||
}
|
||||
|
||||
if (!spa_list_is_empty (&collected)) {
|
||||
HookData *hook_data;
|
||||
|
||||
/* convert "before" dependencies into "after" dependencies */
|
||||
spa_list_for_each (hook_data, &collected, link) {
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
/* record hook_data->hook as a dependency of the *strv hook */
|
||||
record_dependency (&collected, *strv,
|
||||
wp_event_hook_get_name (hook_data->hook));
|
||||
strv++;
|
||||
}
|
||||
}
|
||||
|
||||
/* sort */
|
||||
while (!spa_list_is_empty (&collected)) {
|
||||
gboolean made_progress = FALSE;
|
||||
|
||||
/* examine each hook to see if its dependencies are satisfied in the
|
||||
result list; if yes, then append it to the result too */
|
||||
spa_list_consume (hook_data, &collected, link) {
|
||||
guint deps_satisfied = 0;
|
||||
|
||||
spa_list_remove (&hook_data->link);
|
||||
|
||||
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);
|
||||
return event->hooks->len > 0;
|
||||
}
|
||||
|
||||
struct event_hooks_iterator_data
|
||||
{
|
||||
WpEvent *event;
|
||||
HookData *cur;
|
||||
};
|
||||
|
||||
static void
|
||||
event_hooks_iterator_reset (WpIterator *it)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
struct spa_list *list = &it_data->event->hooks;
|
||||
|
||||
if (!spa_list_is_empty (list))
|
||||
it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
event_hooks_iterator_next (WpIterator *it, GValue *item)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
struct spa_list *list = &it_data->event->hooks;
|
||||
|
||||
if (!spa_list_is_empty (list) &&
|
||||
!spa_list_is_end (it_data->cur, list, link)) {
|
||||
g_value_init (item, WP_TYPE_EVENT_HOOK);
|
||||
g_value_set_object (item, it_data->cur->hook);
|
||||
it_data->cur = spa_list_next (it_data->cur, link);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
||||
gpointer data)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
struct spa_list *list = &it_data->event->hooks;
|
||||
HookData *hook_data;
|
||||
|
||||
if (!spa_list_is_empty (list)) {
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
g_value_init (&item, WP_TYPE_EVENT_HOOK);
|
||||
g_value_set_object (&item, hook_data->hook);
|
||||
if (!func (&item, ret, data))
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
event_hooks_iterator_finalize (WpIterator *it)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
wp_event_unref (it_data->event);
|
||||
}
|
||||
|
||||
static const WpIteratorMethods event_hooks_iterator_methods = {
|
||||
.version = WP_ITERATOR_METHODS_VERSION,
|
||||
.reset = event_hooks_iterator_reset,
|
||||
.next = event_hooks_iterator_next,
|
||||
.fold = event_hooks_iterator_fold,
|
||||
.finalize = event_hooks_iterator_finalize,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator that iterates over all the hooks that were
|
||||
* collected by wp_event_collect_hooks()
|
||||
|
|
@ -558,15 +339,8 @@ static const WpIteratorMethods event_hooks_iterator_methods = {
|
|||
WpIterator *
|
||||
wp_event_new_hooks_iterator (WpEvent * event)
|
||||
{
|
||||
WpIterator *it = NULL;
|
||||
struct event_hooks_iterator_data *it_data;
|
||||
GPtrArray *hooks;
|
||||
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL);
|
||||
return wp_iterator_new_ptr_array (hooks, WP_TYPE_EVENT_HOOK);
|
||||
|
||||
g_return_val_if_fail (event != NULL, NULL);
|
||||
|
||||
it = wp_iterator_new (&event_hooks_iterator_methods,
|
||||
sizeof (struct event_hooks_iterator_data));
|
||||
it_data = wp_iterator_get_user_data (it);
|
||||
it_data->event = wp_event_ref (event);
|
||||
event_hooks_iterator_reset (it);
|
||||
return it;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -770,6 +770,8 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
|||
if (!pw_global_props && WP_IS_SESSION_ITEM (object)) {
|
||||
WpSessionItem *si = (WpSessionItem *) object;
|
||||
pw_global_props = props = wp_session_item_get_properties (si);
|
||||
if (!pw_props)
|
||||
pw_props = props;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -879,3 +881,51 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finds all the defined constraint values for a subject in \a self.
|
||||
*
|
||||
* A defined constraint value is the value of a constraint with the 'equal' or
|
||||
* 'in-list' verb, because the full value must be defined with those verbs. This
|
||||
* can be useful for cases where we want to enumerate interests that are
|
||||
* interested in specific subjects.
|
||||
*
|
||||
* \ingroup wpobjectinterest
|
||||
* \param self the object interest
|
||||
* \param type the constraint type
|
||||
* \param subject the subject that the constraint applies to
|
||||
* \returns (element-type GVariant) (transfer full) (nullable): the defined
|
||||
* constraint values for this object interest.
|
||||
* \since 0.5.13
|
||||
*/
|
||||
GPtrArray *
|
||||
wp_object_interest_find_defined_constraint_values (WpObjectInterest * self,
|
||||
WpConstraintType type, const gchar * subject)
|
||||
{
|
||||
GPtrArray *res = g_ptr_array_new_with_free_func (
|
||||
(GDestroyNotify)g_variant_unref);
|
||||
struct constraint *c;
|
||||
|
||||
pw_array_for_each (c, &self->constraints) {
|
||||
if ((c->type == type || WP_CONSTRAINT_TYPE_NONE == type) &&
|
||||
g_str_equal (c->subject, subject)) {
|
||||
switch (c->verb) {
|
||||
case WP_CONSTRAINT_VERB_EQUALS:
|
||||
g_ptr_array_add (res, g_variant_ref (c->value));
|
||||
break;
|
||||
case WP_CONSTRAINT_VERB_IN_LIST: {
|
||||
GVariantIter iter;
|
||||
GVariant *child;
|
||||
g_variant_iter_init (&iter, c->value);
|
||||
while ((child = g_variant_iter_next_value (&iter)))
|
||||
g_ptr_array_add (res, child);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
|
|||
WpInterestMatchFlags flags, GType object_type, gpointer object,
|
||||
WpProperties * pw_props, WpProperties * pw_global_props);
|
||||
|
||||
WP_API
|
||||
GPtrArray * wp_object_interest_find_defined_constraint_values (
|
||||
WpObjectInterest * self, WpConstraintType type, const gchar * subject);
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
|
||||
|
||||
G_END_DECLS
|
||||
|
|
|
|||
|
|
@ -783,7 +783,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
|
|||
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
|
||||
|
||||
g_clear_pointer (&d->properties, wp_properties_unref);
|
||||
d->properties = wp_properties_new_wrap_dict (props);
|
||||
d->properties = wp_properties_new_copy_dict (props);
|
||||
|
||||
g_object_notify (G_OBJECT (instance), "properties");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <spa/utils/cleanup.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "proc-utils.h"
|
||||
|
|
@ -145,6 +147,21 @@ wp_proc_info_get_cgroup (WpProcInfo * self)
|
|||
return self->cgroup;
|
||||
}
|
||||
|
||||
static FILE *
|
||||
fdopenat (int dirfd, const char *path, int flags, const char *mode, mode_t perm)
|
||||
{
|
||||
int fd = openat (dirfd, path, flags, perm);
|
||||
if (fd >= 0) {
|
||||
FILE *f = fdopen (fd, mode);
|
||||
if (f)
|
||||
return f;
|
||||
close (fd);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* \brief Gets the process information of a given PID
|
||||
* \ingroup wpprocutils
|
||||
|
|
@ -155,51 +172,46 @@ WpProcInfo *
|
|||
wp_proc_utils_get_proc_info (pid_t pid)
|
||||
{
|
||||
WpProcInfo *ret = wp_proc_info_new (pid);
|
||||
g_autofree gchar *status = NULL;
|
||||
g_autoptr (GError) error = NULL;
|
||||
gsize length = 0;
|
||||
char path [64];
|
||||
spa_autoclose int base_fd = -1;
|
||||
FILE *file;
|
||||
g_autofree gchar *line = NULL;
|
||||
size_t size = 0;
|
||||
|
||||
snprintf (path, sizeof(path), "/proc/%d", pid);
|
||||
base_fd = open (path,
|
||||
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY, 0);
|
||||
if (base_fd < 0) {
|
||||
wp_info ("Could not open process info directory %s, skipping", path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Get parent PID */
|
||||
{
|
||||
g_autofree gchar *path = g_strdup_printf ("/proc/%d/status", pid);
|
||||
if (g_file_get_contents (path, &status, &length, &error)) {
|
||||
const gchar *loc = strstr (status, "\nPPid:");
|
||||
if (loc) {
|
||||
const gint res = sscanf (loc, "\nPPid:%d\n", &ret->parent);
|
||||
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);
|
||||
}
|
||||
file = fdopenat (base_fd, "status",
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
||||
if (file) {
|
||||
while (getline (&line, &size, file) > 1)
|
||||
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
|
||||
break;
|
||||
fclose (file);
|
||||
}
|
||||
|
||||
/* Get cgroup */
|
||||
{
|
||||
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cgroup", pid);
|
||||
if (g_file_get_contents (path, &ret->cgroup, &length, &error)) {
|
||||
if (length > 0)
|
||||
ret->cgroup [length - 1] = '\0'; /* Remove EOF character */
|
||||
} else {
|
||||
wp_warning ("failed to get cgroup for PID %d: %s", pid, error->message);
|
||||
}
|
||||
file = fdopenat (base_fd, "cgroup",
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
||||
if (file) {
|
||||
if (getline (&line, &size, file) > 1)
|
||||
ret->cgroup = g_strstrip (g_strdup (line));
|
||||
fclose (file);
|
||||
}
|
||||
|
||||
/* Get args */
|
||||
{
|
||||
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cmdline", pid);
|
||||
FILE *file = fopen (path, "rb");
|
||||
if (file) {
|
||||
g_autofree gchar *lineptr = NULL;
|
||||
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);
|
||||
}
|
||||
file = fdopenat (base_fd, "cmdline",
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
||||
if (file) {
|
||||
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
|
||||
ret->args[ret->n_args++] = g_strdup (line);
|
||||
fclose (file);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
|
|
|||
20
meson.build
20
meson.build
|
|
@ -1,5 +1,5 @@
|
|||
project('wireplumber', ['c'],
|
||||
version : '0.5.11',
|
||||
version : '0.5.13',
|
||||
license : 'MIT',
|
||||
meson_version : '>= 0.59.0',
|
||||
default_options : [
|
||||
|
|
@ -158,7 +158,25 @@ common_args = [
|
|||
'-DG_LOG_USE_STRUCTURED',
|
||||
'-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')
|
||||
summary({'SPA_AUDIO_MAX_CHANNELS': spa_max_channels})
|
||||
|
||||
i18n_conf = files()
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,16 @@ shared_library(
|
|||
dependencies : [wp_dep, giounix_dep],
|
||||
)
|
||||
|
||||
shared_library(
|
||||
'wireplumber-module-notifications-api',
|
||||
[
|
||||
'module-notifications-api.c',
|
||||
],
|
||||
install : true,
|
||||
install_dir : wireplumber_module_dir,
|
||||
dependencies : [wp_dep, giounix_dep],
|
||||
)
|
||||
|
||||
shared_library(
|
||||
'wireplumber-module-si-audio-adapter',
|
||||
[
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@ static int
|
|||
core_get_properties (lua_State *L)
|
||||
{
|
||||
WpCore * core = get_wp_core (L);
|
||||
g_autoptr (WpProperties) p = wp_core_get_properties (core);
|
||||
wplua_properties_to_table (L, p);
|
||||
WpProperties *p = wp_core_get_properties (core);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ static int
|
|||
core_get_info (lua_State *L)
|
||||
{
|
||||
WpCore * core = get_wp_core (L);
|
||||
g_autoptr (WpProperties) p = wp_core_get_remote_properties (core);
|
||||
WpProperties *p = wp_core_get_remote_properties (core);
|
||||
|
||||
lua_newtable (L);
|
||||
lua_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_pushstring (L, wp_core_get_remote_version (core));
|
||||
lua_setfield (L, -2, "version");
|
||||
wplua_properties_to_table (L, p);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
|
||||
lua_setfield (L, -2, "properties");
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -297,8 +297,13 @@ static int
|
|||
core_update_properties (lua_State *L)
|
||||
{
|
||||
WpCore *core = get_wp_core(L);
|
||||
luaL_checktype (L, 1, LUA_TTABLE);
|
||||
wp_core_update_properties (core, wplua_table_to_properties (L, 1));
|
||||
WpProperties *props = NULL;
|
||||
if (lua_istable (L, 1))
|
||||
props = wplua_table_to_properties (L, 1);
|
||||
else
|
||||
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
|
||||
|
||||
wp_core_update_properties (core, props);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -599,6 +604,28 @@ push_wpiterator (lua_State *L, WpIterator *it)
|
|||
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 */
|
||||
|
||||
static int
|
||||
|
|
@ -837,7 +864,11 @@ object_interest_matches (lua_State *L)
|
|||
matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
|
||||
}
|
||||
else if (lua_istable (L, 2)) {
|
||||
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
if (lua_istable (L, 2))
|
||||
props = wplua_table_to_properties (L, 2);
|
||||
else
|
||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
||||
matches = wp_object_interest_matches (interest, props);
|
||||
} else
|
||||
luaL_argerror (L, 2, "expected GObject or table");
|
||||
|
|
@ -997,10 +1028,11 @@ impl_metadata_new (lua_State *L)
|
|||
const char *name = luaL_checkstring (L, 1);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
if (lua_istable (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),
|
||||
name, properties);
|
||||
|
|
@ -1017,10 +1049,11 @@ device_new (lua_State *L)
|
|||
const char *factory = luaL_checkstring (L, 1);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
if (lua_istable (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),
|
||||
factory, properties);
|
||||
|
|
@ -1037,10 +1070,11 @@ spa_device_new (lua_State *L)
|
|||
const char *factory = luaL_checkstring (L, 1);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
if (lua_istable (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),
|
||||
factory, properties);
|
||||
|
|
@ -1105,10 +1139,11 @@ node_new (lua_State *L)
|
|||
const char *factory = luaL_checkstring (L, 1);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
if (lua_istable (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),
|
||||
factory, properties);
|
||||
|
|
@ -1214,10 +1249,11 @@ impl_node_new (lua_State *L)
|
|||
const char *factory = luaL_checkstring (L, 1);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
if (lua_istable (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),
|
||||
factory, properties);
|
||||
|
|
@ -1250,10 +1286,11 @@ link_new (lua_State *L)
|
|||
const char *factory = luaL_checkstring (L, 1);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
if (lua_istable (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);
|
||||
if (l)
|
||||
|
|
@ -1329,9 +1366,12 @@ static int
|
|||
client_update_properties (lua_State *L)
|
||||
{
|
||||
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
|
||||
WpProperties *properties = NULL;
|
||||
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
WpProperties *properties = wplua_table_to_properties (L, 2);
|
||||
if (lua_istable (L, 2))
|
||||
properties = wplua_table_to_properties (L, 2);
|
||||
else
|
||||
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
||||
|
||||
wp_client_update_properties (client, properties);
|
||||
return 0;
|
||||
|
|
@ -1391,46 +1431,12 @@ static int
|
|||
session_item_configure (lua_State *L)
|
||||
{
|
||||
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
|
||||
WpProperties *props = wp_properties_new_empty ();
|
||||
WpProperties *props;
|
||||
|
||||
/* validate arguments */
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
|
||||
/* 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);
|
||||
}
|
||||
if (lua_istable (L, 2))
|
||||
props = wplua_table_to_properties (L, 2);
|
||||
else
|
||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
||||
|
||||
lua_pushboolean (L, wp_session_item_configure (si, props));
|
||||
return 1;
|
||||
|
|
@ -1452,12 +1458,23 @@ session_item_remove (lua_State *L)
|
|||
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[] = {
|
||||
{ "get_associated_proxy", session_item_get_associated_proxy },
|
||||
{ "reset", session_item_reset },
|
||||
{ "configure", session_item_configure },
|
||||
{ "register", session_item_register },
|
||||
{ "remove", session_item_remove },
|
||||
{ "get_property", session_item_get_property },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
|
|
@ -1527,19 +1544,24 @@ on_enum_params_done (WpPipewireObject * pwobj, GAsyncResult * res,
|
|||
GClosure * closure)
|
||||
{
|
||||
g_autoptr (GError) error = NULL;
|
||||
GValue val = G_VALUE_INIT;
|
||||
int n_vals = 0;
|
||||
GValue vals[2] = { G_VALUE_INIT, G_VALUE_INIT };
|
||||
int n_vals = 1;
|
||||
WpIterator *it;
|
||||
|
||||
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) {
|
||||
g_value_init (&val, G_TYPE_STRING);
|
||||
g_value_set_string (&val, error->message);
|
||||
n_vals = 1;
|
||||
g_value_init (&vals[1], G_TYPE_STRING);
|
||||
g_value_set_string (&vals[1], error->message);
|
||||
n_vals = 2;
|
||||
}
|
||||
|
||||
g_clear_pointer (&it, wp_iterator_unref);
|
||||
g_closure_invoke (closure, NULL, n_vals, &val, NULL);
|
||||
g_value_unset (&val);
|
||||
g_closure_invoke (closure, NULL, n_vals, vals, NULL);
|
||||
|
||||
g_value_unset (&vals[0]);
|
||||
g_value_unset (&vals[1]);
|
||||
g_closure_invalidate (closure);
|
||||
g_closure_unref (closure);
|
||||
}
|
||||
|
|
@ -1575,11 +1597,22 @@ pipewire_object_set_param (lua_State *L)
|
|||
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[] = {
|
||||
{ "enum_params", pipewire_object_enum_params },
|
||||
{ "iterate_params", pipewire_object_iterate_params },
|
||||
{ "set_param" , pipewire_object_set_param },
|
||||
{ "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
|
||||
{ "get_property", pipewire_object_get_property },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
|
|
@ -1606,9 +1639,14 @@ static int
|
|||
state_save (lua_State *L)
|
||||
{
|
||||
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
|
||||
g_autoptr (WpProperties) props = 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);
|
||||
lua_pushboolean (L, saved);
|
||||
lua_pushstring (L, error ? error->message : "");
|
||||
|
|
@ -1619,8 +1657,13 @@ static int
|
|||
state_save_after_timeout (lua_State *L)
|
||||
{
|
||||
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
|
||||
if (lua_istable (L, 2))
|
||||
props = wplua_table_to_properties (L, 2);
|
||||
else
|
||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
||||
|
||||
wp_state_save_after_timeout (state, get_wp_core (L), props);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1629,8 +1672,8 @@ static int
|
|||
state_load (lua_State *L)
|
||||
{
|
||||
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
|
||||
g_autoptr (WpProperties) props = wp_state_load (state);
|
||||
wplua_properties_to_table (L, props);
|
||||
WpProperties *props = wp_state_load (state);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -1655,10 +1698,11 @@ impl_module_new (lua_State *L)
|
|||
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
|
||||
args = luaL_checkstring (L, 2);
|
||||
|
||||
if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
|
||||
luaL_checktype (L, 3, LUA_TTABLE);
|
||||
if (lua_istable (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),
|
||||
name, args, properties);
|
||||
|
|
@ -1681,9 +1725,10 @@ conf_new (lua_State *L)
|
|||
WpProperties *p = NULL;
|
||||
WpConf *conf = NULL;
|
||||
|
||||
if (lua_istable (L, 2)) {
|
||||
if (lua_istable (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);
|
||||
if (conf) {
|
||||
|
|
@ -1721,7 +1766,7 @@ conf_get_section_as_properties (lua_State *L)
|
|||
const char *section = NULL;
|
||||
g_autoptr (WpConf) conf = NULL;
|
||||
g_autoptr (WpSpaJson) s = NULL;
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
WpProperties *props = NULL;
|
||||
int argi = 1;
|
||||
|
||||
/* check if called as method on object */
|
||||
|
|
@ -1736,6 +1781,8 @@ conf_get_section_as_properties (lua_State *L)
|
|||
|
||||
if (lua_istable (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
|
||||
props = wp_properties_new_empty ();
|
||||
|
||||
|
|
@ -1744,7 +1791,7 @@ conf_get_section_as_properties (lua_State *L)
|
|||
if (s && wp_spa_json_is_object (s))
|
||||
wp_properties_update_from_json (props, s);
|
||||
}
|
||||
wplua_properties_to_table (L, props);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -1901,10 +1948,12 @@ json_utils_match_rules (lua_State *L)
|
|||
gboolean res;
|
||||
|
||||
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
luaL_checktype (L, 3, LUA_TFUNCTION);
|
||||
|
||||
properties = wplua_table_to_properties (L, 2);
|
||||
if (lua_istable (L, 2))
|
||||
properties = wplua_table_to_properties (L, 2);
|
||||
else
|
||||
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
||||
|
||||
res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
|
||||
L, &error);
|
||||
|
|
@ -1920,17 +1969,21 @@ json_utils_match_rules (lua_State *L)
|
|||
static int
|
||||
json_utils_match_rules_update_properties (lua_State *L)
|
||||
{
|
||||
g_autoptr (WpProperties) properties = NULL;
|
||||
WpProperties *properties = NULL;
|
||||
WpSpaJson *json;
|
||||
int count;
|
||||
|
||||
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
|
||||
luaL_checktype (L, 2, LUA_TTABLE);
|
||||
properties = wplua_table_to_properties (L, 2);
|
||||
|
||||
if (lua_istable (L, 2))
|
||||
properties = wplua_table_to_properties (L, 2);
|
||||
else
|
||||
properties = wp_properties_ref (wplua_checkboxed (L, 2,
|
||||
WP_TYPE_PROPERTIES));
|
||||
|
||||
count = wp_json_utils_match_rules_update_properties (json, properties);
|
||||
|
||||
wplua_properties_to_table (L, properties);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, properties);
|
||||
lua_pushinteger (L, count);
|
||||
return 2;
|
||||
}
|
||||
|
|
@ -2012,6 +2065,108 @@ static const luaL_Reg proc_utils_funcs[] = {
|
|||
{ 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 */
|
||||
|
||||
static int
|
||||
|
|
@ -2305,8 +2460,8 @@ static int
|
|||
event_get_properties (lua_State *L)
|
||||
{
|
||||
WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
|
||||
g_autoptr (WpProperties) props = wp_event_get_properties (event);
|
||||
wplua_properties_to_table (L, props);
|
||||
WpProperties *props = wp_event_get_properties (event);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -2428,10 +2583,11 @@ event_dispatcher_push_event (lua_State *L)
|
|||
lua_pop (L, 1);
|
||||
|
||||
lua_pushliteral (L, "properties");
|
||||
if (lua_gettable (L, 1) != LUA_TNIL) {
|
||||
luaL_checktype (L, -1, LUA_TTABLE);
|
||||
if (lua_istable (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_pushliteral (L, "source");
|
||||
|
|
@ -2984,6 +3140,10 @@ wp_lua_scripting_api_init (lua_State *L)
|
|||
conf_new, conf_methods);
|
||||
wplua_register_type_methods (L, WP_TYPE_PROC_INFO,
|
||||
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);
|
||||
|
||||
if (!wplua_load_uri (L, URI_API, &error) ||
|
||||
!wplua_pcall (L, 0, 0, &error)) {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ SANDBOX_EXPORT = {
|
|||
Conf = WpConf,
|
||||
JsonUtils = JsonUtils,
|
||||
ProcUtils = ProcUtils,
|
||||
Properties = WpProperties_new,
|
||||
SimpleEventHook = WpSimpleEventHook_new,
|
||||
AsyncEventHook = WpAsyncEventHook_new,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,40 +300,54 @@ spa_json_object_new (lua_State *L)
|
|||
{
|
||||
g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object ();
|
||||
|
||||
luaL_checktype (L, 1, LUA_TTABLE);
|
||||
if (lua_istable (L, 1)) {
|
||||
luaL_checktype (L, 1, LUA_TTABLE);
|
||||
|
||||
lua_pushnil (L);
|
||||
while (lua_next (L, -2)) {
|
||||
/* We only add table values with string keys */
|
||||
if (lua_type (L, -2) == LUA_TSTRING) {
|
||||
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
|
||||
lua_pushnil (L);
|
||||
while (lua_next (L, -2)) {
|
||||
/* We only add table values with string keys */
|
||||
if (lua_type (L, -2) == LUA_TSTRING) {
|
||||
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
|
||||
|
||||
switch (lua_type (L, -1)) {
|
||||
case LUA_TBOOLEAN:
|
||||
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
|
||||
break;
|
||||
case LUA_TNUMBER:
|
||||
if (lua_isinteger (L, -1))
|
||||
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
|
||||
else
|
||||
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
|
||||
break;
|
||||
case LUA_TSTRING:
|
||||
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
|
||||
break;
|
||||
case LUA_TUSERDATA: {
|
||||
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
|
||||
wp_spa_json_builder_add_json (builder, json);
|
||||
break;
|
||||
switch (lua_type (L, -1)) {
|
||||
case LUA_TBOOLEAN:
|
||||
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
|
||||
break;
|
||||
case LUA_TNUMBER:
|
||||
if (lua_isinteger (L, -1))
|
||||
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
|
||||
else
|
||||
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
|
||||
break;
|
||||
case LUA_TSTRING:
|
||||
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
|
||||
break;
|
||||
case LUA_TUSERDATA: {
|
||||
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
|
||||
wp_spa_json_builder_add_json (builder, json);
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ _wplua_gboxed___index (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_checkstring (L, 2);
|
||||
const gchar *key = luaL_tolstring (L, 2, NULL);
|
||||
GType type = G_VALUE_TYPE (obj_v);
|
||||
GType boxed_type = type;
|
||||
lua_CFunction func = NULL;
|
||||
GHashTable *vtables;
|
||||
|
||||
|
|
@ -53,6 +54,104 @@ _wplua_gboxed___index (lua_State *L)
|
|||
lua_pushcfunction (L, func);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +168,8 @@ _wplua_init_gboxed (lua_State *L)
|
|||
{ "__gc", _wplua_gvalue_userdata___gc },
|
||||
{ "__eq", _wplua_gboxed___eq },
|
||||
{ "__index", _wplua_gboxed___index },
|
||||
{ "__newindex", _wplua_gboxed___newindex },
|
||||
{ "__pairs", _wplua_gboxed___pairs },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ WpProperties *
|
|||
wplua_table_to_properties (lua_State *L, int idx)
|
||||
{
|
||||
WpProperties *p = wp_properties_new_empty ();
|
||||
const gchar *key, *value;
|
||||
int table = lua_absindex (L, idx);
|
||||
|
||||
if (lua_type (L, table) != LUA_TTABLE) {
|
||||
|
|
@ -24,11 +23,34 @@ wplua_table_to_properties (lua_State *L, int idx)
|
|||
|
||||
lua_pushnil(L);
|
||||
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 */
|
||||
key = luaL_tolstring (L, -2, NULL);
|
||||
value = luaL_tolstring (L, -2, NULL);
|
||||
luaL_checkany (L, -2);
|
||||
switch (lua_type (L, -2)) {
|
||||
case LUA_TNIL:
|
||||
lua_pop (L, 2);
|
||||
break;
|
||||
case LUA_TUSERDATA: {
|
||||
if (wplua_gvalue_userdata_type(L, -2) != G_TYPE_INVALID) {
|
||||
GValue *v = lua_touserdata (L, -2);
|
||||
gpointer p = g_value_peek_pointer (v);
|
||||
value = g_strdup_printf ("%p", p);
|
||||
lua_pop (L, 2);
|
||||
} else {
|
||||
value = g_strdup (luaL_tolstring (L, -2, NULL));
|
||||
lua_pop (L, 3);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
value = g_strdup (luaL_tolstring (L, -2, NULL));
|
||||
lua_pop (L, 3);
|
||||
break;
|
||||
}
|
||||
|
||||
wp_properties_set (p, key, value);
|
||||
lua_pop (L, 3);
|
||||
}
|
||||
|
||||
/* sort, because the lua table has a random order and it's too messy to read */
|
||||
|
|
@ -313,10 +335,7 @@ wplua_gvalue_to_lua (lua_State *L, const GValue *v)
|
|||
lua_pushlightuserdata (L, g_value_get_pointer (v));
|
||||
break;
|
||||
case G_TYPE_BOXED:
|
||||
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));
|
||||
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
|
||||
break;
|
||||
case G_TYPE_OBJECT:
|
||||
case G_TYPE_INTERFACE: {
|
||||
|
|
|
|||
|
|
@ -103,14 +103,15 @@ static void
|
|||
bind_call (GObject * obj, GAsyncResult * res, gpointer data)
|
||||
{
|
||||
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
|
||||
GError *err = NULL;
|
||||
g_autoptr (GError) err = NULL;
|
||||
GDBusProxy *call;
|
||||
GVariant *prop;
|
||||
g_autoptr (GVariant) prop = NULL;
|
||||
gint init_state;
|
||||
|
||||
call = g_dbus_proxy_new_finish (res, &err);
|
||||
if (call == NULL) {
|
||||
wp_warning_object (wpmm, "Failed to get call");
|
||||
g_prefix_error (&err, "Failed to get call: ");
|
||||
wp_warning_object (wpmm, "%s", err->message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -122,8 +123,6 @@ bind_call (GObject * obj, GAsyncResult * res, gpointer data)
|
|||
|
||||
if (is_active_state (init_state))
|
||||
active_calls_inc (wpmm);
|
||||
|
||||
g_variant_unref (prop);
|
||||
}
|
||||
|
||||
wpmm->calls = g_list_prepend (wpmm->calls, call);
|
||||
|
|
@ -165,7 +164,7 @@ on_voice_signal (GDBusProxy * iface,
|
|||
g_object_get (wpmm->dbus, "connection", &conn, NULL);
|
||||
|
||||
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_FLAGS_NONE,
|
||||
NULL,
|
||||
|
|
@ -175,9 +174,8 @@ on_voice_signal (GDBusProxy * iface,
|
|||
NULL,
|
||||
bind_call,
|
||||
wpmm);
|
||||
g_free (path);
|
||||
} 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.
|
||||
deleted = g_list_find_custom (wpmm->calls, path, match_call_path);
|
||||
|
|
@ -185,8 +183,6 @@ on_voice_signal (GDBusProxy * iface,
|
|||
g_object_unref (deleted->data);
|
||||
wpmm->calls = g_list_delete_link (wpmm->calls, deleted);
|
||||
}
|
||||
|
||||
g_free (path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,22 +192,23 @@ list_calls_done (GObject * obj,
|
|||
gpointer data)
|
||||
{
|
||||
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
|
||||
GVariant *params;
|
||||
GVariantIter *calls;
|
||||
g_autoptr (GVariant) params = NULL;
|
||||
g_autoptr (GVariantIter) calls = NULL;
|
||||
gchar *path;
|
||||
GError *err = NULL;
|
||||
g_autoptr (GError) err = NULL;
|
||||
g_autoptr (GDBusConnection) conn = NULL;
|
||||
|
||||
params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err);
|
||||
if (params == NULL) {
|
||||
g_prefix_error (&err, "Failed to list active calls on startup: ");
|
||||
wp_warning_object (wpmm, "%s", err->message);
|
||||
g_clear_object (&err);
|
||||
return;
|
||||
}
|
||||
|
||||
g_object_get (wpmm->dbus, "connection", &conn, NULL);
|
||||
|
||||
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_FLAGS_NONE,
|
||||
NULL,
|
||||
|
|
@ -222,9 +219,6 @@ list_calls_done (GObject * obj,
|
|||
bind_call,
|
||||
wpmm);
|
||||
}
|
||||
|
||||
g_variant_iter_free (calls);
|
||||
g_variant_unref (params);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -353,7 +347,7 @@ static void
|
|||
wp_modem_manager_enable (WpPlugin * self, WpTransition * transition)
|
||||
{
|
||||
WpModemManager *wpmm = WP_MODEM_MANAGER (self);
|
||||
WpCore *core;
|
||||
g_autoptr (WpCore) core = NULL;
|
||||
GError *err;
|
||||
g_autoptr (GDBusConnection) conn = NULL;
|
||||
|
||||
|
|
|
|||
|
|
@ -98,8 +98,10 @@ static void item_free (gpointer data)
|
|||
{
|
||||
Item *item = data;
|
||||
|
||||
free(item->desktop_entry);
|
||||
free(item);
|
||||
g_clear_pointer (&item->desktop_entry, g_free);
|
||||
g_clear_pointer (&item->flatpak_app_id, g_free);
|
||||
g_clear_pointer (&item->flatpak_instance_id, g_free);
|
||||
g_free (item);
|
||||
}
|
||||
|
||||
static Players *players_new (GDBusConnection *conn)
|
||||
|
|
@ -128,7 +130,7 @@ static void players_unref (Players *players)
|
|||
return;
|
||||
|
||||
g_mutex_clear (&players->lock);
|
||||
g_clear_object (&players->items);
|
||||
g_clear_pointer (&players->items, g_hash_table_unref);
|
||||
g_clear_object (&players->conn);
|
||||
g_clear_object (&players->cancellable);
|
||||
g_free (players);
|
||||
|
|
@ -236,7 +238,7 @@ static void item_desktop_entry_cb (GObject *source_object, GAsyncResult* res, gp
|
|||
}
|
||||
|
||||
g_variant_get (result, "(v)", &value);
|
||||
if (!g_str_equal(g_variant_get_type_string (value), "s")) {
|
||||
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
|
||||
wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name);
|
||||
return;
|
||||
}
|
||||
|
|
@ -426,6 +428,8 @@ wp_mpris_plugin_operation_finalize (GObject *object)
|
|||
WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object);
|
||||
|
||||
g_clear_object (&self->conn);
|
||||
|
||||
G_OBJECT_CLASS (wp_mpris_plugin_operation_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -529,9 +533,7 @@ wp_mpris_plugin_disable (WpPlugin * plugin)
|
|||
static gpointer
|
||||
wp_mpris_plugin_get_players (WpMprisPlugin *self)
|
||||
{
|
||||
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
|
||||
|
||||
g_variant_builder_init (&b, G_VARIANT_TYPE ("av"));
|
||||
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("av"));
|
||||
|
||||
if (self->players) {
|
||||
g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock);
|
||||
|
|
|
|||
152
modules/module-notifications-api.c
Normal file
152
modules/module-notifications-api.c
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2021 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <wp/wp.h>
|
||||
#include "dbus-connection-state.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("m-notification")
|
||||
|
||||
#define DBUS_INTERFACE_NAME "org.freedesktop.Notifications"
|
||||
#define DBUS_OBJECT_PATH "/org/freedesktop/Notifications"
|
||||
|
||||
enum
|
||||
{
|
||||
ACTION_GET_DBUS,
|
||||
ACTION_SEND,
|
||||
LAST_SIGNAL
|
||||
};
|
||||
|
||||
static guint signals[LAST_SIGNAL] = { 0 };
|
||||
|
||||
struct _WpNotificationsPlugin
|
||||
{
|
||||
WpPlugin parent;
|
||||
|
||||
WpPlugin *dbus;
|
||||
};
|
||||
|
||||
G_DECLARE_FINAL_TYPE (WpNotificationsPlugin,
|
||||
wp_notifications_plugin, WP, NOTIFICATIONS_PLUGIN,
|
||||
WpPlugin)
|
||||
G_DEFINE_TYPE (WpNotificationsPlugin, wp_notifications_plugin,
|
||||
WP_TYPE_PLUGIN)
|
||||
|
||||
static gpointer
|
||||
wp_notifications_plugin_get_dbus (WpNotificationsPlugin *self)
|
||||
{
|
||||
return self->dbus ? g_object_ref (self->dbus) : NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
wp_notifications_plugin_send (WpNotificationsPlugin *self,
|
||||
const gchar *summary, const gchar *body_message)
|
||||
{
|
||||
g_autoptr (GDBusConnection) conn = NULL;
|
||||
g_autoptr (GError) error = NULL;
|
||||
g_autoptr (GVariant) res = NULL;
|
||||
GVariantBuilder hints;
|
||||
|
||||
g_object_get (self->dbus, "connection", &conn, NULL);
|
||||
g_return_if_fail (conn);
|
||||
|
||||
/* Set urgency */
|
||||
g_variant_builder_init (&hints, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add (&hints, "{sv}", "urgency", g_variant_new_byte (0));
|
||||
|
||||
/* Notify */
|
||||
res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
|
||||
DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Notify",
|
||||
g_variant_new("(susssasa{sv}i)", "wireplumber", 0, "", summary,
|
||||
body_message, NULL, &hints, -1), NULL, G_DBUS_CALL_FLAGS_NONE, -1,
|
||||
NULL, &error);
|
||||
if (error) {
|
||||
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
|
||||
g_dbus_error_strip_remote_error (error);
|
||||
|
||||
wp_warning_object (self, "Notify: %s (%s)", error->message, remote_error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
wp_notifications_plugin_init (WpNotificationsPlugin * self)
|
||||
{
|
||||
}
|
||||
|
||||
static void
|
||||
wp_notifications_plugin_enable (WpPlugin * plugin,
|
||||
WpTransition * transition)
|
||||
{
|
||||
WpNotificationsPlugin *self = WP_NOTIFICATIONS_PLUGIN (plugin);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
|
||||
self->dbus = wp_plugin_find (core, "dbus-connection");
|
||||
if (!self->dbus) {
|
||||
wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
|
||||
WP_LIBRARY_ERROR_INVARIANT,
|
||||
"dbus-connection module must be loaded before notifications"));
|
||||
return;
|
||||
}
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_notifications_plugin_disable (WpPlugin * plugin)
|
||||
{
|
||||
WpNotificationsPlugin *self = WP_NOTIFICATIONS_PLUGIN (plugin);
|
||||
|
||||
g_clear_object (&self->dbus);
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_notifications_plugin_class_init (WpNotificationsPluginClass * klass)
|
||||
{
|
||||
WpPluginClass *plugin_class = (WpPluginClass *) klass;
|
||||
|
||||
plugin_class->enable = wp_notifications_plugin_enable;
|
||||
plugin_class->disable = wp_notifications_plugin_disable;
|
||||
|
||||
/**
|
||||
* WpNotificationsPlugin::get-dbus:
|
||||
*
|
||||
* Returns: (transfer full): the dbus object
|
||||
*/
|
||||
signals[ACTION_GET_DBUS] = g_signal_new_class_handler (
|
||||
"get-dbus", G_TYPE_FROM_CLASS (klass),
|
||||
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
||||
(GCallback) wp_notifications_plugin_get_dbus,
|
||||
NULL, NULL, NULL,
|
||||
G_TYPE_OBJECT, 0);
|
||||
|
||||
/**
|
||||
* WpNotificationsPlugin::send:
|
||||
*
|
||||
* @brief
|
||||
* @em summary: the summary
|
||||
* @em body_message: The body message
|
||||
*/
|
||||
signals[ACTION_SEND] = g_signal_new_class_handler (
|
||||
"send", G_TYPE_FROM_CLASS (klass),
|
||||
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
||||
(GCallback) wp_notifications_plugin_send,
|
||||
NULL, NULL, NULL, G_TYPE_VARIANT,
|
||||
2, G_TYPE_STRING, G_TYPE_STRING);
|
||||
}
|
||||
|
||||
WP_PLUGIN_EXPORT GObject *
|
||||
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
|
||||
{
|
||||
return G_OBJECT (g_object_new (
|
||||
wp_notifications_plugin_get_type(),
|
||||
"name", "notifications-api",
|
||||
"core", core,
|
||||
NULL));
|
||||
}
|
||||
|
|
@ -204,6 +204,8 @@ wp_portal_permissionstore_plugin_disable (WpPlugin * plugin)
|
|||
WP_PORTAL_PERMISSIONSTORE_PLUGIN (plugin);
|
||||
|
||||
clear_signal (self);
|
||||
if (self->dbus)
|
||||
g_signal_handlers_disconnect_by_data (self->dbus, self);
|
||||
g_clear_object (&self->dbus);
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED);
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ struct _WpSiAudioAdapter
|
|||
WpSessionItem parent;
|
||||
|
||||
/* configuration */
|
||||
gboolean reconfigured;
|
||||
WpNode *node;
|
||||
WpPort *port; /* only used for passthrough or convert mode */
|
||||
gboolean no_format;
|
||||
gboolean mono;
|
||||
gboolean control_port;
|
||||
gboolean monitor;
|
||||
gboolean disable_dsp;
|
||||
|
|
@ -104,6 +106,7 @@ si_audio_adapter_reset (WpSessionItem * item)
|
|||
g_clear_object (&self->node);
|
||||
g_clear_object (&self->port);
|
||||
self->no_format = FALSE;
|
||||
self->mono = FALSE;
|
||||
self->control_port = FALSE;
|
||||
self->monitor = FALSE;
|
||||
self->disable_dsp = FALSE;
|
||||
|
|
@ -134,10 +137,11 @@ si_audio_adapter_get_default_clock_rate (WpSiAudioAdapter * self)
|
|||
static gboolean
|
||||
is_unpositioned (struct spa_audio_info_raw *info)
|
||||
{
|
||||
uint32_t i;
|
||||
uint32_t i, n_pos;
|
||||
if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
|
||||
return TRUE;
|
||||
for (i = 0; i < info->channels; i++)
|
||||
n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
|
||||
for (i = 0; i < n_pos; i++)
|
||||
if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
|
||||
info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
|
||||
return TRUE;
|
||||
|
|
@ -145,7 +149,8 @@ is_unpositioned (struct spa_audio_info_raw *info)
|
|||
}
|
||||
|
||||
static gboolean
|
||||
si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node)
|
||||
si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node,
|
||||
gboolean mono)
|
||||
{
|
||||
g_autoptr (WpIterator) formats = NULL;
|
||||
g_auto (GValue) value = G_VALUE_INIT;
|
||||
|
|
@ -193,9 +198,14 @@ si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node)
|
|||
continue;
|
||||
|
||||
if (position == NULL ||
|
||||
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS))
|
||||
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_N_ELEMENTS(raw_format.position)))
|
||||
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
|
||||
|
||||
if (mono) {
|
||||
raw_format.channels = 1;
|
||||
raw_format.position [0] = SPA_AUDIO_CHANNEL_MONO;
|
||||
}
|
||||
|
||||
if (self->raw_format.channels < raw_format.channels) {
|
||||
self->raw_format = raw_format;
|
||||
if (is_unpositioned(&raw_format))
|
||||
|
|
@ -247,6 +257,8 @@ si_audio_adapter_configure (WpSessionItem * item, WpProperties *p)
|
|||
WpNode *node = NULL;
|
||||
const gchar *str;
|
||||
|
||||
self->reconfigured = self->node != NULL;
|
||||
|
||||
/* reset previous config */
|
||||
si_audio_adapter_reset (item);
|
||||
|
||||
|
|
@ -264,7 +276,12 @@ si_audio_adapter_configure (WpSessionItem * item, WpProperties *p)
|
|||
|
||||
str = wp_properties_get (si_props, "item.features.no-format");
|
||||
self->no_format = str && pw_properties_parse_bool (str);
|
||||
if (!self->no_format && !si_audio_adapter_find_format (self, node)) {
|
||||
|
||||
str = wp_properties_get (si_props, "item.features.mono");
|
||||
self->mono = str && pw_properties_parse_bool (str);
|
||||
|
||||
if (!self->no_format && !si_audio_adapter_find_format (self, node,
|
||||
self->mono)) {
|
||||
wp_notice_object (item, "no usable format found for node %d",
|
||||
wp_proxy_get_bound_id (WP_PROXY (node)));
|
||||
return FALSE;
|
||||
|
|
@ -333,7 +350,8 @@ format_audio_raw_build (const struct spa_audio_info_raw *info)
|
|||
if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
|
||||
/* Build the position array spa pod */
|
||||
g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
|
||||
for (guint i = 0; i < info->channels; i++)
|
||||
guint n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
|
||||
for (guint i = 0; i < n_pos; i++)
|
||||
wp_spa_pod_builder_add_id (position_builder, info->position[i]);
|
||||
|
||||
/* Add the position property */
|
||||
|
|
@ -611,8 +629,10 @@ si_audio_adapter_enable_active (WpSessionItem *si, WpTransition *transition)
|
|||
|
||||
/* If device node, enum available formats and set one of them */
|
||||
if (!self->no_format && (self->is_device || self->dont_remix ||
|
||||
!self->is_autoconnect || self->disable_dsp || self->is_unpositioned))
|
||||
!self->is_autoconnect || self->disable_dsp || self->is_unpositioned ||
|
||||
self->reconfigured)) {
|
||||
si_audio_adapter_configure_node (self, transition);
|
||||
}
|
||||
|
||||
/* Otherwise just finish activating */
|
||||
else
|
||||
|
|
|
|||
|
|
@ -195,12 +195,16 @@ on_link_activated (WpObject * proxy, GAsyncResult * res,
|
|||
{
|
||||
WpSiStandardLink *self = wp_transition_get_source_object (transition);
|
||||
guint len = self->node_links ? self->node_links->len : 0;
|
||||
g_autoptr (GError) error = NULL;
|
||||
|
||||
/* Count the number of failed and active links */
|
||||
if (wp_object_activate_finish (proxy, res, NULL))
|
||||
if (wp_object_activate_finish (proxy, res, &error)) {
|
||||
self->n_active_links++;
|
||||
else
|
||||
} else {
|
||||
self->n_failed_links++;
|
||||
wp_info_object (self, "Failed to activate link %p: %s", proxy,
|
||||
error->message);
|
||||
}
|
||||
|
||||
/* Wait for all links to finish activation */
|
||||
if (self->n_failed_links + self->n_active_links != len)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ typedef enum {
|
|||
typedef enum {
|
||||
RESCAN_CONTEXT_LINKING,
|
||||
RESCAN_CONTEXT_DEFAULT_NODES,
|
||||
RESCAN_CONTEXT_MEDIA_ROLE_VOLUME,
|
||||
N_RESCAN_CONTEXTS,
|
||||
} RescanContext;
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ rescan_context_get_type (void)
|
|||
static const GEnumValue values[] = {
|
||||
{ RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" },
|
||||
{ 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 }
|
||||
};
|
||||
if (g_once_init_enter (>ype_id)) {
|
||||
|
|
@ -161,6 +163,8 @@ get_default_event_priority (const gchar *event_type)
|
|||
return -490;
|
||||
else if (!g_strcmp0 (event_type, "rescan-for-linking"))
|
||||
return -500;
|
||||
else if (!g_strcmp0 (event_type, "rescan-for-media-role-volume"))
|
||||
return -510;
|
||||
else if (!g_strcmp0 (event_type, "node-state-changed"))
|
||||
return 50;
|
||||
else if (!g_strcmp0 (event_type, "metadata-changed"))
|
||||
|
|
|
|||
40
po/conf.pot
40
po/conf.pot
|
|
@ -63,6 +63,26 @@ msgstr ""
|
|||
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"
|
||||
|
|
@ -103,6 +123,16 @@ msgstr ""
|
|||
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"
|
||||
|
|
@ -133,6 +163,16 @@ msgstr ""
|
|||
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"
|
||||
|
|
|
|||
68
po/sl.po
68
po/sl.po
|
|
@ -2,24 +2,22 @@
|
|||
# Copyright (C) 2024 WirePlumber's COPYRIGHT HOLDER
|
||||
# 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 ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WirePlumber master\n"
|
||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
||||
"issues\n"
|
||||
"POT-Creation-Date: 2025-08-21 03:57+0000\n"
|
||||
"PO-Revision-Date: 2025-08-21 15:45+0200\n"
|
||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
|
||||
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
|
||||
"PO-Revision-Date: 2025-12-15 23:31+0100\n"
|
||||
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
|
||||
"Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n"
|
||||
"Language: sl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\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"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n"
|
||||
"X-Generator: Poedit 3.8\n"
|
||||
|
||||
#. WirePlumber
|
||||
#.
|
||||
|
|
@ -49,7 +47,7 @@ msgstr "Razdeli %s"
|
|||
#. also sanitize nick, replace ':' with ' '
|
||||
#. ensure the node has a description
|
||||
#. also sanitize description, replace ':' with ' '
|
||||
#. add api.alsa.card.* properties for rule matching purposes
|
||||
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
|
||||
#. add cpu.vm.name for rule matching purposes
|
||||
#. apply properties from rules defined in JSON .conf file
|
||||
#. handle split HW node
|
||||
|
|
@ -75,6 +73,7 @@ msgstr "Modem"
|
|||
#. 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
|
||||
|
|
@ -201,6 +200,34 @@ msgstr "Privzeta glasnost za zvočne vire"
|
|||
msgid "Default source volume"
|
||||
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.conf
|
||||
msgid "Streams may be moved by adding PipeWire metadata at runtime"
|
||||
|
|
@ -247,6 +274,19 @@ msgstr ""
|
|||
msgid "Ducking level"
|
||||
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.conf
|
||||
msgid "The camera discovery timeout in milliseconds"
|
||||
|
|
@ -277,6 +317,16 @@ msgstr "Omogoči vrata nadzornih zvočnikov na zvočnih vozliščih"
|
|||
msgid "Monitor ports"
|
||||
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.conf
|
||||
msgid "Do not convert audio to F32 format"
|
||||
|
|
|
|||
400
po/tr.po
400
po/tr.po
|
|
@ -1,26 +1,25 @@
|
|||
# Turkish translation for PipeWire.
|
||||
# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PipeWire package.
|
||||
# Necdet Yücel <necdetyucel@gmail.com>, 2014.
|
||||
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
|
||||
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
|
||||
# Oğuz Ersen <oguzersen@protonmail.com>, 2021.
|
||||
# Turkish translation for WirePlumber.
|
||||
# Copyright (C) 2025 WirePlumber's COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the WirePlumber package.
|
||||
#
|
||||
# Sabri Ünal <yakushabb@gmail.com>, 2025.
|
||||
# Emin Tufan Çetin <etcetin@gmail.com>, 2025
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PipeWire master\n"
|
||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
|
||||
"issues/new\n"
|
||||
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
|
||||
"PO-Revision-Date: 2021-12-06 21:31+0300\n"
|
||||
"Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n"
|
||||
"Language-Team: Turkish <tr>\n"
|
||||
"Project-Id-Version: WirePlumber master\n"
|
||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
||||
"issues\n"
|
||||
"POT-Creation-Date: 2025-11-09 04:07+0000\n"
|
||||
"PO-Revision-Date: 2025-11-09 08:00+0300\n"
|
||||
"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
|
||||
"Language-Team: Turkish <takim@gnome.org.tr>\n"
|
||||
"Language: tr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.4.2\n"
|
||||
"X-Generator: Poedit 3.8\n"
|
||||
|
||||
#. WirePlumber
|
||||
#.
|
||||
|
|
@ -28,13 +27,19 @@ msgstr ""
|
|||
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
#.
|
||||
#. SPDX-License-Identifier: MIT
|
||||
#. Receive script arguments from config.lua
|
||||
#. ensure config.properties is not nil
|
||||
#. preprocess rules and create Interest objects
|
||||
#. applies properties from config.rules when asked to
|
||||
#. 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 "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 default pause-on-idle setting
|
||||
#. try to negotiate the max ammount of channels
|
||||
#. try to negotiate the max amount of channels
|
||||
#. set priority
|
||||
#. ensure the node has a media class
|
||||
#. ensure the node has a name
|
||||
|
|
@ -45,15 +50,360 @@ msgstr ""
|
|||
#. ensure the node has a description
|
||||
#. also sanitize description, replace ':' with ' '
|
||||
#. add api.alsa.card.* properties for rule matching purposes
|
||||
#. apply properties from config.rules
|
||||
#. add cpu.vm.name for rule matching purposes
|
||||
#. apply properties from rules defined in JSON .conf file
|
||||
#. handle split HW node
|
||||
#. create split PCM node
|
||||
#. create the node
|
||||
#. ensure the device has an appropriate name
|
||||
#. deduplicate devices with the same name
|
||||
#. ensure the device has a description
|
||||
#: src/scripts/monitors/alsa.lua:222
|
||||
msgid "Built-in Audio"
|
||||
msgstr "Dahili Ses"
|
||||
#: src/scripts/monitors/alsa.lua:438
|
||||
msgid "Loopback"
|
||||
msgstr "Geri Döngü"
|
||||
|
||||
#: src/scripts/monitors/alsa.lua:224
|
||||
#: src/scripts/monitors/alsa.lua:440
|
||||
msgid "Built-in Audio"
|
||||
msgstr "Yerleşik Ses"
|
||||
|
||||
#: src/scripts/monitors/alsa.lua:442
|
||||
msgid "Modem"
|
||||
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"
|
||||
|
|
|
|||
300
po/zh_CN.po
300
po/zh_CN.po
|
|
@ -6,15 +6,15 @@
|
|||
# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
|
||||
# Frank Hill <hxf.prc@gmail.com>, 2015.
|
||||
# Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015.
|
||||
# lumingzh <lumingzh@qq.com>, 2024.
|
||||
# lumingzh <lumingzh@qq.com>, 2024-2025.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: pipewire.master-tx\n"
|
||||
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
|
||||
"issues\n"
|
||||
"POT-Creation-Date: 2024-01-08 15:36+0000\n"
|
||||
"PO-Revision-Date: 2024-10-08 08:59+0800\n"
|
||||
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
|
||||
"PO-Revision-Date: 2025-12-16 10:10+0800\n"
|
||||
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
|
||||
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
|
||||
"Language: zh_CN\n"
|
||||
|
|
@ -22,7 +22,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2016-03-22 13:23+0000\n"
|
||||
"X-Generator: Gtranslator 47.0\n"
|
||||
"X-Generator: Gtranslator 49.0\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#. WirePlumber
|
||||
|
|
@ -32,9 +32,18 @@ msgstr ""
|
|||
#.
|
||||
#. SPDX-License-Identifier: MIT
|
||||
#. 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 "分离 %s"
|
||||
|
||||
#. Connect ObjectConfig events to the right node
|
||||
#. set the device id and spa factory name; REQUIRED, do not change
|
||||
#. set the default pause-on-idle setting
|
||||
#. try to negotiate the max ammount of channels
|
||||
#. try to negotiate the max amount of channels
|
||||
#. set priority
|
||||
#. ensure the node has a media class
|
||||
#. ensure the node has a name
|
||||
|
|
@ -44,22 +53,24 @@ msgstr ""
|
|||
#. also sanitize nick, replace ':' with ' '
|
||||
#. ensure the node has a description
|
||||
#. also sanitize description, replace ':' with ' '
|
||||
#. add api.alsa.card.* properties for rule matching purposes
|
||||
#. add vm.type for rule matching purposes
|
||||
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
|
||||
#. add cpu.vm.name for rule matching purposes
|
||||
#. apply properties from rules defined in JSON .conf file
|
||||
#. handle split HW node
|
||||
#. create split PCM node
|
||||
#. create the node
|
||||
#. ensure the device has an appropriate name
|
||||
#. deduplicate devices with the same name
|
||||
#. ensure the device has a description
|
||||
#: src/scripts/monitors/alsa.lua:211
|
||||
#: src/scripts/monitors/alsa.lua:438
|
||||
msgid "Loopback"
|
||||
msgstr "回环"
|
||||
|
||||
#: src/scripts/monitors/alsa.lua:213
|
||||
#: src/scripts/monitors/alsa.lua:440
|
||||
msgid "Built-in Audio"
|
||||
msgstr "内置音频"
|
||||
|
||||
#: src/scripts/monitors/alsa.lua:215
|
||||
#: src/scripts/monitors/alsa.lua:442
|
||||
msgid "Modem"
|
||||
msgstr "调制解调器"
|
||||
|
||||
|
|
@ -68,6 +79,7 @@ msgstr "调制解调器"
|
|||
#. 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
|
||||
|
|
@ -97,13 +109,13 @@ msgstr "调制解调器"
|
|||
#. set the node name
|
||||
#. sanitize name
|
||||
#. deduplicate nodes with the same name
|
||||
#. apply properties from bluetooth.conf
|
||||
#. 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:113
|
||||
#: src/scripts/monitors/bluez-midi.lua:114
|
||||
#, lua-format
|
||||
msgid "BLE MIDI %d"
|
||||
msgstr "BLE MIDI %d"
|
||||
|
|
@ -129,3 +141,267 @@ 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 "自动检测 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 "恢复目标"
|
||||
|
|
|
|||
|
|
@ -335,6 +335,14 @@ wireplumber.components = [
|
|||
provides = api.file-monitor
|
||||
}
|
||||
|
||||
## API to send D-Bus notifications
|
||||
{
|
||||
name = libwireplumber-module-notifications-api, type = module
|
||||
provides = api.notifications
|
||||
requires = [ support.dbus ]
|
||||
}
|
||||
|
||||
|
||||
## Provide the "default" pw_metadata
|
||||
{
|
||||
name = metadata.lua, type = script/lua
|
||||
|
|
@ -554,12 +562,18 @@ wireplumber.components = [
|
|||
name = device/apply-routes.lua, type = script/lua
|
||||
provides = hooks.device.routes.apply
|
||||
}
|
||||
{
|
||||
name = device/automute-alsa-routes.lua, type = script/lua
|
||||
provides = hooks.device.routes.automute-alsa
|
||||
wants = [ api.notifications ]
|
||||
}
|
||||
{
|
||||
type = virtual, provides = policy.device.routes
|
||||
requires = [ hooks.device.routes.select,
|
||||
hooks.device.routes.apply ]
|
||||
wants = [ hooks.device.routes.find-best,
|
||||
hooks.device.routes.state ]
|
||||
hooks.device.routes.state,
|
||||
hooks.device.routes.automute-alsa ]
|
||||
}
|
||||
|
||||
## Default nodes selection hooks
|
||||
|
|
@ -613,12 +627,17 @@ wireplumber.components = [
|
|||
name = node/filter-forward-format.lua, type = script/lua
|
||||
provides = hooks.filter.forward-format
|
||||
}
|
||||
{
|
||||
name = node/filter-graph.lua, type = script/lua
|
||||
provides = hooks.filter.graph
|
||||
}
|
||||
{
|
||||
type = virtual, provides = policy.node
|
||||
requires = [ hooks.node.create-session-item ]
|
||||
wants = [ hooks.node.suspend
|
||||
hooks.stream.state
|
||||
hooks.filter.forward-format ]
|
||||
hooks.filter.forward-format
|
||||
hooks.filter.graph ]
|
||||
}
|
||||
{
|
||||
name = node/software-dsp.lua, type = script/lua
|
||||
|
|
@ -703,10 +722,21 @@ wireplumber.components = [
|
|||
provides = hooks.linking.role-based.rescan
|
||||
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
|
||||
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
|
||||
|
|
@ -847,6 +877,19 @@ wireplumber.settings.schema = {
|
|||
min = 0.0
|
||||
max = 1.0
|
||||
}
|
||||
device.routes.mute-on-alsa-playback-removed = {
|
||||
name = "Auto-mute on wired audio disconnect"
|
||||
description = "Automatically mute all audio devices when active wired headphones/speakers are disconnected to prevent unintended sound output"
|
||||
type = "bool"
|
||||
default = false
|
||||
}
|
||||
device.routes.mute-on-bluetooth-playback-removed = {
|
||||
name = "Auto-mute on Bluetooth audio disconnect"
|
||||
description = "Automatically mute all audio devices when active Bluetooth headphones/speakers are disconnected to prevent unintended sound output"
|
||||
type = "bool"
|
||||
default = false
|
||||
}
|
||||
|
||||
|
||||
## Linking
|
||||
linking.role-based.duck-level = {
|
||||
|
|
@ -885,6 +928,12 @@ wireplumber.settings.schema = {
|
|||
min = 0
|
||||
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.features.audio.no-dsp = {
|
||||
|
|
@ -905,6 +954,12 @@ wireplumber.settings.schema = {
|
|||
type = "bool"
|
||||
default = false
|
||||
}
|
||||
node.features.audio.mono = {
|
||||
name = "Mono"
|
||||
description = "Configure all audio device sink nodes in MONO"
|
||||
type = "bool"
|
||||
default = false
|
||||
}
|
||||
node.stream.restore-props = {
|
||||
name = "Restore properties"
|
||||
description = "Remember and restore properties of streams"
|
||||
|
|
|
|||
43
src/config/wireplumber.conf.d.examples/filter-graph.conf
Normal file
43
src/config/wireplumber.conf.d.examples/filter-graph.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
node.filter-graph.rules = [
|
||||
## The list of filter graph rules
|
||||
|
||||
## This rule example creates two filter graphs for each audio source node
|
||||
# {
|
||||
# matches = [
|
||||
# {
|
||||
# ## This matches all audio source nodes
|
||||
# media.class = "Audio/Source"
|
||||
# }
|
||||
# ]
|
||||
# actions = {
|
||||
# create-filter-graph = [
|
||||
# ## Multiple filter graphs can be defined here.
|
||||
# ## The syntax is the same as the pipewire filter-chain conf files.
|
||||
#
|
||||
# ## This is an example of a bultin passthrough filter
|
||||
# {
|
||||
# nodes = [
|
||||
# {
|
||||
# type = builtin
|
||||
# label = copy
|
||||
# name = passthrough
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# ## This is an example of a LADSPA rnnoise filter
|
||||
# {
|
||||
# nodes = [
|
||||
# {
|
||||
# type = ladspa
|
||||
# name = rnnoise
|
||||
# plugin = librnnoise_ladspa
|
||||
# label = noise_suppressor_stereo
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
]
|
||||
|
||||
|
|
@ -150,6 +150,7 @@ wireplumber.components = [
|
|||
policy.role-based.priority = 100
|
||||
policy.role-based.action.same-priority = "mix"
|
||||
policy.role-based.action.lower-priority = "cork"
|
||||
policy.role-based.preferred-target = "Speaker"
|
||||
}
|
||||
}
|
||||
provides = loopback.sink.role.alert
|
||||
|
|
|
|||
|
|
@ -19,5 +19,25 @@ monitor.alsa.rules = [
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ SimpleEventHook {
|
|||
|
||||
local om = source:call ("get-object-manager", "metadata")
|
||||
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||
if metadata == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if selected_node then
|
||||
local key = "default." .. def_node_type
|
||||
|
|
|
|||
|
|
@ -46,10 +46,18 @@ Hooks
|
|||
- File
|
||||
- Description
|
||||
|
||||
* - device/find-calling-profile
|
||||
- find-voice-call-profile.lua
|
||||
- selects a profile based on the state of calls
|
||||
|
||||
* - device/find-stored-profile
|
||||
- state-profile.lua
|
||||
- selects the profile that has been stored in the state file (user's explicit selection)
|
||||
|
||||
* - device/find-preferred-profile
|
||||
- find-preferred-profile.lua
|
||||
- selects the profile based on device configured priorities
|
||||
|
||||
* - device/find-best-profile
|
||||
- find-best-profile.lua
|
||||
- finds the best profile for a device based on profile priorities and availability
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ log = Log.open_topic ("s-device")
|
|||
|
||||
AsyncEventHook {
|
||||
name = "device/apply-profile",
|
||||
after = { "device/find-stored-profile", "device/find-preferred-profile", "device/find-best-profile" },
|
||||
after = { "device/find-calling-profile", "device/find-stored-profile", "device/find-preferred-profile", "device/find-best-profile" },
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-profile" },
|
||||
|
|
|
|||
220
src/scripts/device/automute-alsa-routes.lua
Normal file
220
src/scripts/device/automute-alsa-routes.lua
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2025 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-automute-alsa-routes")
|
||||
hooks_registered = false
|
||||
|
||||
function setRoute (device, route, mute)
|
||||
local param = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Route", "Route",
|
||||
index = route.index,
|
||||
device = route.device,
|
||||
props = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Props", "Route",
|
||||
mute = mute
|
||||
},
|
||||
save = false,
|
||||
}
|
||||
|
||||
log:info (device, "Setting mute to " .. tostring(mute) ..
|
||||
" on route " .. route.name)
|
||||
device:set_param("Route", param)
|
||||
end
|
||||
|
||||
function findLowestPriorityAvailableOutputRoute (device)
|
||||
local lowest_prio_r = nil
|
||||
for p in device:iterate_params("Route") do
|
||||
local route = cutils.parseParam (p, "Route")
|
||||
if route and route.direction == "Output" and route.available ~= "no" then
|
||||
if lowest_prio_r == nil or lowest_prio_r.priority > route.priority then
|
||||
lowest_prio_r = route
|
||||
end
|
||||
end
|
||||
end
|
||||
return lowest_prio_r
|
||||
end
|
||||
|
||||
function evaluateNode (node, source)
|
||||
if nodes_info [node.id] == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get node info
|
||||
local node_state = nodes_info [node.id].state
|
||||
local node_api = nodes_info [node.id].api
|
||||
local node_dev_id = nodes_info [node.id].dev_id
|
||||
local node_cpd = nodes_info [node.id].cpd
|
||||
|
||||
-- Don't do anything if node was not running
|
||||
if node_state ~= "running" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Emite event if setting is enabled for this API
|
||||
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")
|
||||
if (mute_alsa and node_api == "alsa") or
|
||||
(mute_bluez and node_api == "bluez5") then
|
||||
local e = source:call ("create-event", "mute-alsa-devices", nil, nil)
|
||||
e:set_data ("device-id", node_dev_id)
|
||||
e:set_data ("card-profile-device", node_cpd)
|
||||
EventDispatcher.push_event (e)
|
||||
end
|
||||
end
|
||||
|
||||
mute_alsa_devices_hook = SimpleEventHook {
|
||||
name = "device/mute-alsa-devices",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "mute-alsa-devices" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local dev_id = tonumber (event:get_data ("device-id"))
|
||||
local cpd = tonumber (event:get_data ("card-profile-device"))
|
||||
local device_om = source:call ("get-object-manager", "device")
|
||||
local send_notification = false
|
||||
|
||||
-- We mute all available output ALSA routes but the one associated with
|
||||
-- the running node.
|
||||
--
|
||||
-- We also don't mute any routes if the running node is associated with
|
||||
-- the lowest priority route as this is most likely to be the Speakers.
|
||||
--
|
||||
-- For instance, we want to mute all routes except the Headphones one
|
||||
-- when unplugging a headset while playing audio, but we don't
|
||||
-- want to mute the Headphones route when a headset is plugged in while
|
||||
-- playing audio on the Speakers.
|
||||
for device in device_om:iterate() do
|
||||
local dev_bound_id = device["bound-id"]
|
||||
if device.properties["device.api"] == "alsa" then
|
||||
local lpr = findLowestPriorityAvailableOutputRoute (device)
|
||||
if lpr == nil or lpr.device ~= cpd or dev_bound_id ~= dev_id then
|
||||
for p in device:iterate_params("Route") do
|
||||
local route = cutils.parseParam (p, "Route")
|
||||
if route and
|
||||
route.direction == "Output" and
|
||||
route.available ~= "no" and
|
||||
(route.device ~= cpd or dev_bound_id ~= dev_id) then
|
||||
setRoute (device, route, true)
|
||||
send_notification = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Send notification if devices were muted
|
||||
notifications = notifications or Plugin.find("notifications-api")
|
||||
if notifications ~= nil and send_notification then
|
||||
notifications:call ("send", I18n.gettext("Audio was auto-muted"),
|
||||
I18n.gettext("Active playback device was disconnected"))
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
update_nodes_info_hook = SimpleEventHook {
|
||||
name = "device/update-nodes-info",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "matches", "Audio/Sink" },
|
||||
Constraint { "device.api", "+" },
|
||||
Constraint { "device.id", "+" },
|
||||
Constraint { "card.profile.device", "+", type = "pw" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local node_id = node["bound-id"]
|
||||
local device_api = node.properties ["device.api"]
|
||||
local device_id = node.properties ["device.id"]
|
||||
local cpd = node.properties ["card.profile.device"]
|
||||
local new_state = event:get_properties ()["event.subject.new-state"]
|
||||
|
||||
-- Update node info
|
||||
if nodes_info [node.id] == nil then
|
||||
nodes_info [node.id] = {}
|
||||
end
|
||||
nodes_info [node.id].api = device_api
|
||||
nodes_info [node.id].state = new_state
|
||||
nodes_info [node.id].dev_id = device_id
|
||||
nodes_info [node.id].cpd = cpd
|
||||
end
|
||||
}
|
||||
|
||||
evaluate_mute_on_device_route_changed_hook = SimpleEventHook {
|
||||
name = "device/evaluate-mute-on-device-route-changed",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "EnumRoute" }
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local device = event:get_subject ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
|
||||
-- Evaluate all nodes for this device when the EnumRoute param changed
|
||||
for node in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Audio/Sink", type = "pw-global" },
|
||||
Constraint { "device.id", "=", device["bound-id"], type = "pw-global" },
|
||||
} do
|
||||
evaluateNode (node, source)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
evaluate_mute_on_node_removed_hook = SimpleEventHook {
|
||||
name = "device/evaluate-mute-on-node-removed",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "matches", "Audio/Sink" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local node = event:get_subject ()
|
||||
|
||||
-- Evaluate removed node
|
||||
evaluateNode (node, source)
|
||||
|
||||
-- Clear removed node info
|
||||
nodes_info [node.id] = nil
|
||||
end
|
||||
}
|
||||
|
||||
function toggleState ()
|
||||
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")
|
||||
if (mute_alsa or mute_bluez) and not hooks_registered then
|
||||
nodes_info = {}
|
||||
mute_alsa_devices_hook:register ()
|
||||
update_nodes_info_hook:register ()
|
||||
evaluate_mute_on_device_route_changed_hook:register ()
|
||||
evaluate_mute_on_node_removed_hook:register ()
|
||||
hooks_registered = true
|
||||
elseif not mute_alsa and not mute_bluez and hooks_registered then
|
||||
mute_alsa_devices_hook:remove ()
|
||||
update_nodes_info_hook:remove ()
|
||||
evaluate_mute_on_device_route_changed_hook:remove ()
|
||||
evaluate_mute_on_node_removed_hook:remove ()
|
||||
hooks_registered = false
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("device.routes.mute-on-alsa-playback-removed", function ()
|
||||
toggleState ()
|
||||
end)
|
||||
Settings.subscribe ("device.routes.mute-on-bluetooth-playback-removed", function ()
|
||||
toggleState ()
|
||||
end)
|
||||
toggleState ()
|
||||
|
|
@ -28,43 +28,25 @@
|
|||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
persistent_storage_hooks_registered = false
|
||||
autoswitch_hooks_registered = false
|
||||
|
||||
state = nil
|
||||
headset_profiles = nil
|
||||
local PROFILE_RESTORE_TIMEOUT_MSEC = 2000
|
||||
local PROFILE_SWITCH_TIMEOUT_MSEC = 500
|
||||
|
||||
local profile_restore_timeout_msec = 2000
|
||||
local profile_switch_timeout_msec = 500
|
||||
|
||||
local INVALID = -1
|
||||
local state = nil
|
||||
local headset_profiles = {}
|
||||
local non_headset_profiles = {}
|
||||
local capture_stream_links = {}
|
||||
local restore_timeout_source = {}
|
||||
local switch_timeout_source = {}
|
||||
|
||||
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)
|
||||
function saveHeadsetProfile (device, profile_name, persistent)
|
||||
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||
headset_profiles [key] = profile_name
|
||||
state:save_after_timeout (headset_profiles)
|
||||
if state ~= nil and persistent then
|
||||
state:save_after_timeout (headset_profiles)
|
||||
end
|
||||
end
|
||||
|
||||
function getSavedHeadsetProfile (device)
|
||||
|
|
@ -72,85 +54,72 @@ function getSavedHeadsetProfile (device)
|
|||
return headset_profiles [key]
|
||||
end
|
||||
|
||||
function saveLastProfile (device, profile_name)
|
||||
last_profiles [device.properties ["device.name"]] = profile_name
|
||||
function saveNonHeadsetProfile (device, profile_name)
|
||||
non_headset_profiles [device.properties ["device.name"]] = profile_name
|
||||
end
|
||||
|
||||
function getSavedLastProfile (device)
|
||||
return last_profiles [device.properties ["device.name"]]
|
||||
end
|
||||
|
||||
function isSwitchedToHeadsetProfile (device)
|
||||
return getSavedLastProfile (device) ~= nil
|
||||
function getSavedNonHeadsetProfile (device)
|
||||
return non_headset_profiles [device.properties ["device.name"]]
|
||||
end
|
||||
|
||||
function findProfile (device, index, name)
|
||||
for p in device:iterate_params ("EnumProfile") do
|
||||
local profile = cutils.parseParam (p, "EnumProfile")
|
||||
if not profile then
|
||||
goto skip_enum_profile
|
||||
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
|
||||
|
||||
return INVALID, INVALID, nil
|
||||
end
|
||||
|
||||
function getCurrentProfile (device)
|
||||
for p in device:iterate_params ("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
if profile then
|
||||
return profile.name
|
||||
if profile ~= nil then
|
||||
if (index ~= nil and profile.index == index) or
|
||||
(name ~= nil and profile.name == name) then
|
||||
return profile
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function highestPrioProfileWithInputRoute (device)
|
||||
local profile_priority = INVALID
|
||||
local profile_index = INVALID
|
||||
local profile_name = nil
|
||||
function getCurrentProfile (device)
|
||||
for p in device:iterate_params ("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
if profile then
|
||||
return profile
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function highestPrioProfileWithInputRoute (device)
|
||||
local found_profile = 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
|
||||
if route ~= nil and route.profiles ~= nil and route.direction == "Input" 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
|
||||
local p = findProfile (device, v)
|
||||
if p ~= nil then
|
||||
if found_profile == nil or found_profile.priority < p.priority then
|
||||
found_profile = p
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
::skip_enum_route::
|
||||
end
|
||||
return found_profile
|
||||
end
|
||||
|
||||
return profile_priority, profile_index, profile_name
|
||||
function highestPrioProfileWithoutInputRoute (device)
|
||||
local found_profile = nil
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
|
||||
for _, v in pairs (route.profiles) do
|
||||
local p = findProfile (device, v)
|
||||
if p ~= nil then
|
||||
if found_profile == nil or found_profile.priority < p.priority then
|
||||
found_profile = p
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return found_profile
|
||||
end
|
||||
|
||||
function hasProfileInputRoute (device, profile_index)
|
||||
|
|
@ -177,49 +146,40 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
|
|||
return
|
||||
end
|
||||
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||
if hasProfileInputRoute (device, index) then
|
||||
log:info ("Current profile has input route, not switching")
|
||||
-- Do not switch if the current profile is already a headset profile
|
||||
local cur_profile = getCurrentProfile (device)
|
||||
if cur_profile ~= nil and
|
||||
hasProfileInputRoute (device, cur_profile.index) then
|
||||
log:info (device,
|
||||
"Current profile is already a headset profile, no need to switch")
|
||||
return
|
||||
end
|
||||
|
||||
if isSwitchedToHeadsetProfile (device) then
|
||||
log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||
return
|
||||
end
|
||||
|
||||
local saved_headset_profile = getSavedHeadsetProfile (device)
|
||||
|
||||
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)
|
||||
-- Get saved headset profile if any, otherwise find the highest priority one
|
||||
local profile = nil
|
||||
local profile_name = getSavedHeadsetProfile (device)
|
||||
if profile_name ~= nil then
|
||||
profile = findProfile (device, nil, profile_name)
|
||||
if profile ~= nil and not hasProfileInputRoute (device, profile.index) then
|
||||
saveHeadsetProfile (device, nil, false)
|
||||
end
|
||||
end
|
||||
if index == INVALID then
|
||||
priority, index, name = highestPrioProfileWithInputRoute (device)
|
||||
if profile == nil then
|
||||
profile = highestPrioProfileWithInputRoute (device)
|
||||
end
|
||||
|
||||
if index ~= INVALID then
|
||||
-- Switch if headset profile was found
|
||||
if profile ~= nil then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = index
|
||||
index = profile.index,
|
||||
save = false
|
||||
}
|
||||
|
||||
-- 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)
|
||||
log:info (device, "Switching profile from: " .. cur_profile.name
|
||||
.. " to: " .. profile.name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
log:warning ("Got invalid index when switching profile")
|
||||
log:warning ("Could not find valid headset profile, not switching")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -233,45 +193,40 @@ function restoreProfile (dev_id, device_om)
|
|||
return
|
||||
end
|
||||
|
||||
if not isSwitchedToHeadsetProfile (device) then
|
||||
log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
|
||||
-- Do not restore if the current profile is already a non-headset profile
|
||||
local cur_profile = getCurrentProfile (device)
|
||||
if cur_profile ~= nil and
|
||||
not hasProfileInputRoute (device, cur_profile.index) then
|
||||
log:info (device,
|
||||
"Current profile is already a non-headset profile, no need to restore")
|
||||
return
|
||||
end
|
||||
|
||||
local profile_name = getSavedLastProfile (device)
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local priority, index, name
|
||||
|
||||
if cur_profile_name then
|
||||
priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||
|
||||
if index ~= INVALID and hasProfileInputRoute (device, index) then
|
||||
log:info ("Setting saved headset profile to: " .. cur_profile_name)
|
||||
saveHeadsetProfile (device, cur_profile_name)
|
||||
-- Get saved non-headset profile if any, otherwise find the highest priority one
|
||||
local profile = nil
|
||||
local profile_name = getSavedNonHeadsetProfile (device)
|
||||
if profile_name ~= nil then
|
||||
profile = findProfile (device, nil, profile_name)
|
||||
if profile ~= nil and hasProfileInputRoute (device, profile.index) then
|
||||
saveNonHeadsetProfile (device, nil)
|
||||
end
|
||||
end
|
||||
if profile == nil then
|
||||
profile = highestPrioProfileWithoutInputRoute (device)
|
||||
end
|
||||
|
||||
if profile_name then
|
||||
priority, index, name = findProfile (device, nil, profile_name)
|
||||
|
||||
if index ~= INVALID then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = index
|
||||
}
|
||||
|
||||
-- clear last profile as we will restore it now
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
-- 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
|
||||
-- Restore if non-headset profile was found
|
||||
if profile ~= nil then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = profile.index,
|
||||
save = false
|
||||
}
|
||||
log:info (device, "Restoring profile from: " .. cur_profile.name
|
||||
.. " to: " .. profile.name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
log:warning ("Could not find valid non-headset profile, not switching")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -280,95 +235,89 @@ function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
|
|||
if restore_timeout_source[dev_id] ~= nil then
|
||||
restore_timeout_source[dev_id]:destroy ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
|
||||
end
|
||||
if switch_timeout_source[dev_id] ~= nil then
|
||||
switch_timeout_source[dev_id]:destroy ()
|
||||
switch_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
|
||||
end
|
||||
|
||||
-- create new switch callback
|
||||
switch_timeout_source[dev_id] = Core.timeout_add (profile_switch_timeout_msec, function ()
|
||||
log:info ("Triggering profile switch on device " .. tostring (dev_id))
|
||||
switch_timeout_source[dev_id] = Core.timeout_add (PROFILE_SWITCH_TIMEOUT_MSEC, function ()
|
||||
switch_timeout_source[dev_id] = nil
|
||||
switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||
end)
|
||||
end
|
||||
|
||||
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
|
||||
if switch_timeout_source[dev_id] ~= nil then
|
||||
switch_timeout_source[dev_id]:destroy ()
|
||||
switch_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
|
||||
end
|
||||
if restore_timeout_source[dev_id] ~= nil then
|
||||
restore_timeout_source[dev_id]:destroy ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
|
||||
end
|
||||
|
||||
-- create new restore callback
|
||||
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||
log:info ("Triggering profile restore on device " .. tostring (dev_id))
|
||||
restore_timeout_source[dev_id] = Core.timeout_add (PROFILE_RESTORE_TIMEOUT_MSEC, function ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
restoreProfile (dev_id, device_om)
|
||||
end)
|
||||
end
|
||||
|
||||
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||
-- source filter
|
||||
function checkStreamStatus (stream, node_om, visited_link_groups)
|
||||
-- check if the stream is linked to a bluetooth loopback source
|
||||
local stream_id = tonumber(stream["bound-id"])
|
||||
local peer_id = lutils.getNodePeerId (stream_id)
|
||||
if peer_id ~= nil then
|
||||
local bt_node = node_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
}
|
||||
if bt_node ~= nil then
|
||||
local dev_id = bt_node.properties["device.id"]
|
||||
if dev_id ~= nil then
|
||||
-- If a stream we previously saw stops running, we consider it
|
||||
-- inactive, because some applications (Teams) just cork input
|
||||
-- streams, but don't close them.
|
||||
if previous_streams [stream.id] == dev_id and
|
||||
stream.state ~= "running" then
|
||||
return nil
|
||||
end
|
||||
function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups)
|
||||
local stream_id = stream["bound-id"]
|
||||
|
||||
return dev_id
|
||||
-- Make sure the node is linked
|
||||
local link = link_om:lookup {
|
||||
Constraint { "link.input.node", "=", stream_id, type = "pw-global"}
|
||||
}
|
||||
if link == nil then
|
||||
return nil
|
||||
end
|
||||
local peer_id = link.properties["link.output.node"]
|
||||
|
||||
-- If the peer node is the BT loopback source node, return its Id.
|
||||
-- Otherwise recursively advance in the graph if it is linked to a filter.
|
||||
local bt_node = node_om:lookup {
|
||||
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
}
|
||||
if bt_node ~= nil then
|
||||
return bt_node
|
||||
else
|
||||
local filter_main_node = node_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "node.link-group", "+", type = "pw" }
|
||||
}
|
||||
if filter_main_node ~= nil then
|
||||
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||
if visited_link_groups == nil then
|
||||
visited_link_groups = {}
|
||||
end
|
||||
else
|
||||
-- Check if it is linked to a filter main node, and recursively advance if so
|
||||
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
|
||||
-- Now check all stream nodes for this filter
|
||||
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||
if visited_link_groups == nil then
|
||||
visited_link_groups = {}
|
||||
end
|
||||
if visited_link_groups [filter_link_group] then
|
||||
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
|
||||
if visited_link_groups [filter_link_group] then
|
||||
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 filter_stream_id = filter_stream_node["bound-id"]
|
||||
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_id, node_om, link_om, visited_link_groups)
|
||||
if bt_node ~= nil then
|
||||
return bt_node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -377,60 +326,65 @@ function checkStreamStatus (stream, node_om, visited_link_groups)
|
|||
return nil
|
||||
end
|
||||
|
||||
function handleStream (stream, node_om, device_om)
|
||||
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)
|
||||
function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
|
||||
local bt_node_id = bt_node["bound-id"]
|
||||
for stream in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "node.link-group", "-", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||
} do
|
||||
handleStream (stream, node_om, device_om)
|
||||
local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om)
|
||||
if linked_bt_node ~= nil then
|
||||
local linked_bt_node_id = linked_bt_node ["bound-id"]
|
||||
if tonumber (linked_bt_node_id) == tonumber (bt_node_id) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node-removed@autoswitch-bluetooth-profile",
|
||||
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
|
||||
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
Constraint { "event.type", "=", "evaluate-bluetooth-profiles" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local stream = 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")
|
||||
local link_om = source:call ("get-object-manager", "link")
|
||||
|
||||
local dev_id = active_streams[stream.id]
|
||||
active_streams[stream.id] = nil
|
||||
previous_streams[stream.id] = nil
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id, device_om)
|
||||
-- Evaluate all bluetooth loopback source nodes, and switch to headset
|
||||
-- profile only if the node is running and linked to a stream that is not a
|
||||
-- monitor, otherwise just restore the profile.
|
||||
--
|
||||
-- If the bluetooth node is linked to a stream that is a monitor, its state
|
||||
-- will be 'running', so we cannot just rely on the state to know if we
|
||||
-- have to switch or not, we also need to check if the node is linked to
|
||||
-- a stream that is not a monitor.
|
||||
for bt_node in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Audio/Source" },
|
||||
Constraint { "device.id", "+" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
} do
|
||||
local bt_node_state = bt_node["state"]
|
||||
local bt_dev_id = bt_node.properties ["device.id"]
|
||||
|
||||
if bt_node_state == "running" and
|
||||
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
|
||||
triggerSwitchDeviceToHeadsetProfile (bt_dev_id, device_om)
|
||||
else
|
||||
triggerRestoreProfile (bt_dev_id, device_om)
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
}
|
||||
|
||||
SimpleEventHook {
|
||||
local link_added_hook = SimpleEventHook {
|
||||
name = "link-added@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
|
|
@ -438,46 +392,149 @@ SimpleEventHook {
|
|||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local link = 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")
|
||||
local link_props = link.properties
|
||||
local link = event:get_subject ()
|
||||
local in_stream_id = link.properties["link.input.node"]
|
||||
|
||||
for stream in node_om:iterate {
|
||||
-- Only evaluate bluetooth profiles if a capture stream was linked
|
||||
local stream = node_om:lookup {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "node.link-group", "-", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||
} do
|
||||
local in_id = tonumber(link_props["link.input.node"])
|
||||
local stream_id = tonumber(stream["bound-id"])
|
||||
if in_id == stream_id then
|
||||
handleStream (stream, node_om, device_om)
|
||||
end
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
Constraint { "bound-id", "=", in_stream_id, type = "gobject" },
|
||||
}
|
||||
if stream ~= nil then
|
||||
capture_stream_links [link.id] = true
|
||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
}
|
||||
|
||||
SimpleEventHook {
|
||||
name = "bluez-device-added@autoswitch-bluetooth-profile",
|
||||
local link_removed_hook = SimpleEventHook {
|
||||
name = "link-removed@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-added" },
|
||||
Constraint { "event.type", "=", "link-removed" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local link = event:get_subject ()
|
||||
|
||||
-- Only evaluate bluetooth profiles if a capture stream was unlinked
|
||||
if capture_stream_links [link.id] then
|
||||
capture_stream_links [link.id] = nil
|
||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local state_changed_hook = SimpleEventHook {
|
||||
name = "bluez-loopback-state-changed@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "matches", "Audio/Source" },
|
||||
Constraint { "device.id", "+" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
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 = "device/store-user-selected-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Profile" },
|
||||
Constraint { "device.api", "=", "bluez5" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local source = event:get_source ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
local device_om = source:call ("get-object-manager", "device")
|
||||
|
||||
-- Devices are unswitched initially
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
-- Handle all streams when BT device is added
|
||||
handleAllStreams (node_om, device_om)
|
||||
-- Always save the current profile when it changes
|
||||
local cur_profile = getCurrentProfile (device)
|
||||
if cur_profile ~= nil then
|
||||
if hasProfileInputRoute (device, cur_profile.index) then
|
||||
log:info (device, "Saving headset profile " .. cur_profile.name)
|
||||
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
|
||||
else
|
||||
log:info (device, "Saving non-headset profile " .. cur_profile.name)
|
||||
saveNonHeadsetProfile (device, cur_profile.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
}: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 = {}
|
||||
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
|
||||
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 event_properties = event:get_properties ()
|
||||
local active_ids = event_properties ["profile.active-device-ids"]
|
||||
local selected_routes = event:get_data ("selected-routes") or {}
|
||||
local selected_routes = event:get_data ("selected-routes") or Properties()
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ SimpleEventHook {
|
|||
|
||||
if selected_profile then
|
||||
log:info (device, string.format (
|
||||
"Found best profile '%s' (%d) for device %s",
|
||||
"Found calling profile '%s' (%d) for device %s",
|
||||
selected_profile.name, selected_profile.index, dev_name))
|
||||
event:set_data ("selected-profile", selected_profile)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ find_stored_profile_hook = SimpleEventHook {
|
|||
end
|
||||
|
||||
local device = event:get_subject ()
|
||||
local dev_name = device.properties["device.name"]
|
||||
local device_props = device.properties
|
||||
local dev_name = device_props["device.name"]
|
||||
local dont_restore_off_profile = cutils.parseBool (
|
||||
device_props["session.dont-restore-off-profile"])
|
||||
if not dev_name then
|
||||
log:critical (device, "invalid device.name")
|
||||
return
|
||||
|
|
@ -45,7 +48,8 @@ find_stored_profile_hook = SimpleEventHook {
|
|||
if profile_name then
|
||||
for p in device:iterate_params ("EnumProfile") do
|
||||
local profile = cutils.parseParam (p, "EnumProfile")
|
||||
if profile.name == profile_name and profile.available ~= "no" then
|
||||
if profile.name == profile_name and profile.available ~= "no" and
|
||||
(not dont_restore_off_profile or profile.index ~= 0) then
|
||||
selected_profile = profile
|
||||
break
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ find_stored_routes_hook = SimpleEventHook {
|
|||
local event_properties = event:get_properties ()
|
||||
local profile_name = event_properties ["profile.name"]
|
||||
local active_ids = event_properties ["profile.active-device-ids"]
|
||||
local selected_routes = event:get_data ("selected-routes") or {}
|
||||
local selected_routes = event:get_data ("selected-routes") or Properties()
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
|
@ -108,13 +108,13 @@ apply_route_props_hook = SimpleEventHook {
|
|||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local selected_routes = event:get_data ("selected-routes") or {}
|
||||
local selected_routes = event:get_data ("selected-routes") or Properties()
|
||||
local new_selected_routes = {}
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
assert (dev_info)
|
||||
|
||||
if next (selected_routes) == nil then
|
||||
if selected_routes:get_count () == 0 then
|
||||
log:info (device, "No routes selected to set on " .. dev_info.name)
|
||||
return
|
||||
end
|
||||
|
|
@ -159,122 +159,125 @@ store_or_restore_routes_hook = AsyncEventHook {
|
|||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "evaluate",
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local source = event:get_source ()
|
||||
local device = event:get_subject ()
|
||||
|
||||
-- Make sure the routes are always updated before evaluating them.
|
||||
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762
|
||||
local device = event:get_subject ()
|
||||
device:enum_params ("EnumRoute", function (_, e)
|
||||
device:enum_params ("EnumRoute", function (enum_route_it, e)
|
||||
local selected_routes = {}
|
||||
local push_select_routes = false
|
||||
|
||||
-- check for error
|
||||
if e then
|
||||
transition:return_error ("failed to enum routes: "
|
||||
.. tostring (e));
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
-- Make sure the device is still valid
|
||||
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
|
||||
transition:advance ()
|
||||
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
|
||||
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
if not dev_info then
|
||||
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
|
||||
return
|
||||
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
|
||||
local dev_info = devinfo:get_device_info (device)
|
||||
if not dev_info then
|
||||
transition:advance ()
|
||||
return
|
||||
end
|
||||
|
||||
-- update properties
|
||||
route_info.prev_active = route_info.active
|
||||
route_info.active = false
|
||||
route_info.save = false
|
||||
local new_route_infos = {}
|
||||
|
||||
-- store
|
||||
new_route_infos [route.index] = route_info
|
||||
-- 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
|
||||
|
||||
::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 route_infos with new prev_active, active and save changes
|
||||
dev_info.route_infos = new_route_infos
|
||||
new_route_infos = nil
|
||||
-- update properties
|
||||
route_info.prev_active = route_info.active
|
||||
route_info.active = false
|
||||
route_info.save = false
|
||||
|
||||
-- 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
|
||||
-- store
|
||||
new_route_infos [route.index] = route_info
|
||||
|
||||
::skip_enum_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_infos with new prev_active, active and save changes
|
||||
dev_info.route_infos = new_route_infos
|
||||
new_route_infos = nil
|
||||
|
||||
-- update route_info state
|
||||
route_info.active = true
|
||||
route_info.save = route.save
|
||||
-- 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
|
||||
|
||||
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
|
||||
-- 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
|
||||
|
||||
selected_routes [tostring (route.device)] =
|
||||
Json.Object { index = route_info.index }:to_string ()
|
||||
push_select_routes = true
|
||||
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
|
||||
|
||||
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))
|
||||
selected_routes [tostring (route.device)] =
|
||||
Json.Object { index = route_info.index }:to_string ()
|
||||
push_select_routes = true
|
||||
|
||||
saveRouteProps (dev_info, route)
|
||||
elseif route.available ~= "no" and route.save and route.props then
|
||||
-- just save route properties
|
||||
log:info (device,
|
||||
string.format ("storing route(%s) props of device(%s)",
|
||||
route.name, dev_info.name))
|
||||
|
||||
saveRouteProps (dev_info, route)
|
||||
end
|
||||
|
||||
::skip_route::
|
||||
end
|
||||
|
||||
::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
|
||||
|
||||
-- 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
|
||||
|
||||
-- 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 ()
|
||||
transition:advance ()
|
||||
end)
|
||||
end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function module.get_device_info (self, device)
|
|||
if not dev_info then
|
||||
local device_name = device_properties ["device.name"]
|
||||
if not device_name then
|
||||
Log.critical (device, "invalid device.name")
|
||||
Log.warning (device, "invalid device.name")
|
||||
return nil
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Hooks
|
|||
|
||||
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
|
||||
to schedule a "rescan-for-linking" event, which is the lowest priority event and
|
||||
to schedule a "rescan-for-linking" event, which is the lowest priority linking event and
|
||||
its purpose is to scan through all the linkable session items and link them
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ SimpleEventHook {
|
|||
name = "linking/find-default-target",
|
||||
after = { "linking/find-defined-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",
|
||||
interests = {
|
||||
EventInterest {
|
||||
|
|
|
|||
81
src/scripts/linking/find-media-role-sink-target.lua
Normal file
81
src/scripts/linking/find-media-role-sink-target.lua
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2025 Phosh.mobi e.V.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Pick up a preferred target node for the output stream of role-based loopbacks
|
||||
|
||||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
|
||||
log = Log.open_topic ("s-linking")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "linking/find-media-role-sink-target",
|
||||
after = { "linking/find-defined-target",
|
||||
"linking/find-media-role-target" },
|
||||
before = "linking/prepare-link",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local _, om, si, si_props, _, target =
|
||||
lutils:unwrap_select_target_event (event)
|
||||
|
||||
local node_name = si_props["node.name"]
|
||||
local target_direction = cutils.getTargetDirection (si_props)
|
||||
local media_class = si_props["media.class"]
|
||||
local link_group = si_props["node.link-group"]
|
||||
local is_virtual = si_props["node.virtual"]
|
||||
|
||||
log:info (si, string.format ("Lookup for '%s' (%s) / '%s' / '%s'",
|
||||
node_name, tostring (si_props ["node.id"]), media_class, link_group))
|
||||
|
||||
--- bypass the hook if the target is already set or there's no link group
|
||||
if target or media_class ~= "Stream/Output/Audio" or not is_virtual or link_group == nil then
|
||||
return
|
||||
end
|
||||
|
||||
--- We link the output node but the relevant properties are on the input node
|
||||
--- of the link group
|
||||
local input_node = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "media.class", "=", "Audio/Sink" },
|
||||
Constraint { "node.link-group", "=", link_group },
|
||||
}
|
||||
|
||||
if input_node == nil then
|
||||
log:warning (si, string.format("No input node for %s found", link_group))
|
||||
return
|
||||
end
|
||||
|
||||
local target_name = input_node.properties["policy.role-based.preferred-target"]
|
||||
--- no preferred target
|
||||
if target_name == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local si_target = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "node.name", "=", target_name },
|
||||
}
|
||||
if si_target == nil then
|
||||
si_target = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "node.nick", "=", target_name },
|
||||
}
|
||||
end
|
||||
if si_target then
|
||||
log:info (si,
|
||||
string.format ("... role based sink target picked: %s (%s)",
|
||||
tostring (si_target.properties ["node.name"]),
|
||||
tostring (si_target.properties ["node.id"])))
|
||||
event:set_data ("target", si_target)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
|
@ -26,7 +26,7 @@ function checkFilter (si, om, handle_nonstreams)
|
|||
|
||||
-- always return true if this is not a filter
|
||||
local node = si:get_associated_proxy ("node")
|
||||
local link_group = node.properties["node.link-group"]
|
||||
local link_group = node:get_property ("node.link-group")
|
||||
if link_group == nil then
|
||||
return true
|
||||
end
|
||||
|
|
@ -43,36 +43,34 @@ function checkFilter (si, om, handle_nonstreams)
|
|||
end
|
||||
|
||||
function checkLinkable (si, om, handle_nonstreams)
|
||||
local si_props = si.properties
|
||||
|
||||
-- For the rest of them, only handle stream session items
|
||||
if not si_props or (si_props ["item.node.type"] ~= "stream"
|
||||
and not handle_nonstreams) then
|
||||
return false, si_props
|
||||
if si:get_property ("item.node.type") ~= "stream" and
|
||||
not handle_nonstreams then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check filters
|
||||
if not checkFilter (si, om, handle_nonstreams) then
|
||||
return false, si_props
|
||||
return false
|
||||
end
|
||||
|
||||
return true, si_props
|
||||
return true
|
||||
end
|
||||
|
||||
function unhandleLinkable (si, om)
|
||||
local si_id = si.id
|
||||
local valid, si_props = checkLinkable (si, om, true)
|
||||
if not valid then
|
||||
if not checkLinkable (si, om, true) then
|
||||
return
|
||||
end
|
||||
|
||||
local si_id = si.id
|
||||
log:info (si, string.format ("unhandling item %d", si_id))
|
||||
|
||||
-- iterate over all the links in the graph and
|
||||
-- remove any links associated with this item
|
||||
for silink in om:iterate { type = "SiLink" } do
|
||||
local out_id = tonumber (silink.properties ["out.item.id"])
|
||||
local in_id = tonumber (silink.properties ["in.item.id"])
|
||||
local silink_props = silink.properties
|
||||
local out_id = silink_props:get_int ("out.item.id")
|
||||
local in_id = silink_props:get_int ("in.item.id")
|
||||
|
||||
if out_id == si_id or in_id == si_id then
|
||||
local in_flags = lutils:get_flags (in_id)
|
||||
|
|
@ -84,7 +82,7 @@ function unhandleLinkable (si, om)
|
|||
out_flags.peer_id = nil
|
||||
end
|
||||
|
||||
if cutils.parseBool (silink.properties["is.role.policy.link"]) then
|
||||
if silink_props:get_boolean ("is.role.policy.link") then
|
||||
lutils.clearPriorityMediaRoleLink(silink)
|
||||
end
|
||||
|
||||
|
|
@ -113,17 +111,67 @@ SimpleEventHook {
|
|||
end
|
||||
}: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)
|
||||
local om = source:call ("get-object-manager", "session-item")
|
||||
|
||||
for si in om:iterate { type = "SiLinkable" } do
|
||||
local valid, si_props = checkLinkable (si, om)
|
||||
if not valid then
|
||||
if not checkLinkable (si, om) then
|
||||
goto skip_linkable
|
||||
end
|
||||
|
||||
-- Get properties
|
||||
local si_props = si.properties
|
||||
|
||||
-- check if we need to link this node at all
|
||||
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
|
||||
local autoconnect = si_props:get_boolean ("node.autoconnect")
|
||||
if not autoconnect then
|
||||
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
|
||||
goto skip_linkable
|
||||
|
|
@ -155,7 +203,7 @@ SimpleEventHook {
|
|||
Constraint { "node.link-group", "+" },
|
||||
} do
|
||||
local node = si:get_associated_proxy ("node")
|
||||
local link_group = node.properties["node.link-group"]
|
||||
local link_group = node:get_property ("node.link-group")
|
||||
local direction = cutils.getTargetDirection (si.properties)
|
||||
if futils.is_filter_smart (direction, link_group) and
|
||||
futils.is_filter_disabled (direction, link_group) then
|
||||
|
|
@ -235,7 +283,6 @@ SimpleEventHook {
|
|||
},
|
||||
execute = function (event)
|
||||
local si = event:get_subject ()
|
||||
local si_props = si.properties
|
||||
local source = event:get_source ()
|
||||
|
||||
-- clear timeout source, if any
|
||||
|
|
|
|||
|
|
@ -297,9 +297,9 @@ function createNode(parent, id, obj_type, factory, properties)
|
|||
properties["node.description"] = desc:gsub("(:)", " ")
|
||||
end
|
||||
|
||||
-- add api.alsa.card.* properties for rule matching purposes
|
||||
-- add api.alsa.card.* and alsa.* properties for rule matching purposes
|
||||
for k, v in pairs(dev_props) do
|
||||
if k:find("^api%.alsa%.card%..*") then
|
||||
if k:find("^api%.alsa%.card%..*") or k:find("^alsa%..*") then
|
||||
properties[k] = v
|
||||
end
|
||||
end
|
||||
|
|
@ -494,6 +494,11 @@ function prepareDevice(parent, id, obj_type, factory, properties)
|
|||
factory = "api.alsa.acp.device"
|
||||
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
|
||||
if rd_plugin and properties["api.alsa.card"] then
|
||||
local rd_name = "Audio" .. properties["api.alsa.card"]
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ end
|
|||
|
||||
function createMonitor()
|
||||
local monitor_props = {}
|
||||
for k, v in pairs(config.properties or {}) do
|
||||
for k, v in pairs(config.properties or Properties()) do
|
||||
monitor_props[k] = v
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
COMBINE_OFFSET = 64
|
||||
LOOPBACK_SOURCE_ID = 128
|
||||
LOOPBACK_SINK_ID = 129
|
||||
DEVICE_SOURCE_ID = 0
|
||||
DEVICE_SINK_ID = 1
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-monitors")
|
||||
|
|
@ -23,6 +25,8 @@ config.properties["api.bluez5.connection-info"] = true
|
|||
-- Properties used for previously creating a SCO source node. key: SPA device id
|
||||
sco_source_node_properties = {}
|
||||
|
||||
-- Properties used for previously creating a SCO or A2DP sink node. key: SPA device id
|
||||
sco_a2dp_sink_node_properties = {}
|
||||
|
||||
devices_om = ObjectManager {
|
||||
Interest {
|
||||
|
|
@ -271,6 +275,16 @@ function createNode(parent, id, type, factory, properties)
|
|||
name_prefix = name_prefix .. "_internal"
|
||||
end
|
||||
|
||||
-- hide the sink node because we use the loopback sink instead
|
||||
if parent:get_managed_object (LOOPBACK_SINK_ID) ~= nil and
|
||||
(factory == "api.bluez5.sco.sink" or
|
||||
factory == "api.bluez5.a2dp.sink") then
|
||||
properties["bluez5.sink-loopback-target"] = true
|
||||
properties["api.bluez5.internal"] = true
|
||||
-- add 'internal' to name prefix to not be confused with loopback node
|
||||
name_prefix = name_prefix .. "_internal"
|
||||
end
|
||||
|
||||
-- set the node name
|
||||
local name = name_prefix .. "." ..
|
||||
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
|
||||
|
|
@ -304,9 +318,12 @@ function createNode(parent, id, type, factory, properties)
|
|||
parent:set_managed_pending(id)
|
||||
else
|
||||
log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id))
|
||||
properties["bluez5.loopback"] = false
|
||||
if factory == "api.bluez5.sco.source" then
|
||||
properties["bluez5.loopback"] = false
|
||||
sco_source_node_properties[parent_spa_id] = properties
|
||||
elseif factory == "api.bluez5.sco.sink" or factory == "api.bluez5.a2dp.sink" then
|
||||
properties["bluez5.sink-loopback"] = false
|
||||
sco_a2dp_sink_node_properties[parent_spa_id] = properties
|
||||
end
|
||||
local node = LocalNode("adapter", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
|
|
@ -318,14 +335,20 @@ function removeNode(parent, id)
|
|||
local dev_props = parent.properties
|
||||
local parent_spa_id = tonumber(dev_props["spa.object.id"])
|
||||
local src_properties = sco_source_node_properties[parent_spa_id]
|
||||
local sink_properties = sco_a2dp_sink_node_properties[parent_spa_id]
|
||||
|
||||
log:debug("Remove node: " .. tostring (id))
|
||||
|
||||
if src_properties ~= nil and id == tonumber(src_properties["spa.object.id"]) then
|
||||
log:debug("Clear old SCO properties")
|
||||
log:debug("Clear old SCO source properties")
|
||||
sco_source_node_properties[parent_spa_id] = nil
|
||||
end
|
||||
|
||||
if sink_properties ~= nil and id == tonumber(sink_properties["spa.object.id"]) then
|
||||
log:debug("Clear old SCO-A2DP sink properties")
|
||||
sco_a2dp_sink_node_properties[parent_spa_id] = nil
|
||||
end
|
||||
|
||||
-- Clear also the device set module, if any
|
||||
parent:store_managed_object(id + COMBINE_OFFSET, nil)
|
||||
end
|
||||
|
|
@ -400,6 +423,7 @@ end
|
|||
|
||||
function removeDevice(parent, id)
|
||||
sco_source_node_properties[id] = nil
|
||||
sco_a2dp_sink_node_properties[id] = nil
|
||||
end
|
||||
|
||||
function createMonitor()
|
||||
|
|
@ -442,7 +466,6 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
|
|||
["device.id"] = dev_id,
|
||||
["card.profile.device"] = DEVICE_SOURCE_ID,
|
||||
["device.routes"] = "1",
|
||||
["priority.driver"] = 2010,
|
||||
["priority.session"] = 2010,
|
||||
["bluez5.loopback"] = true,
|
||||
["filter.smart"] = true,
|
||||
|
|
@ -456,6 +479,41 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
|
|||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||
end
|
||||
|
||||
function CreateDeviceLoopbackSink (dev_name, dec_desc, dev_id)
|
||||
local args = Json.Object {
|
||||
["capture.props"] = Json.Object {
|
||||
["node.name"] = string.format ("bluez_output.%s", dev_name),
|
||||
["node.description"] = string.format ("%s", dec_desc),
|
||||
["node.virtual"] = false,
|
||||
["audio.position"] = "[FL, FR]",
|
||||
["media.class"] = "Audio/Sink",
|
||||
["device.id"] = dev_id,
|
||||
["card.profile.device"] = DEVICE_SINK_ID,
|
||||
["device.routes"] = "1",
|
||||
["priority.session"] = 2010,
|
||||
["bluez5.sink-loopback"] = true,
|
||||
["filter.smart"] = true,
|
||||
["filter.smart.target"] = Json.Object {
|
||||
["bluez5.sink-loopback-target"] = true,
|
||||
["bluez5.sink-loopback"] = false,
|
||||
["device.id"] = dev_id
|
||||
}
|
||||
},
|
||||
["playback.props"] = Json.Object {
|
||||
["node.name"] = string.format ("bluez_playback_internal.%s", dev_name),
|
||||
["media.class"] = "Stream/Output/Audio/Internal",
|
||||
["node.description"] =
|
||||
string.format ("Bluetooth internal playback stream for %s", dec_desc),
|
||||
["bluez5.sink-loopback"] = true,
|
||||
["node.passive"] = true,
|
||||
["node.dont-fallback"] = true,
|
||||
["node.linger"] = true,
|
||||
["state.restore-props"] = false,
|
||||
}
|
||||
}
|
||||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||
end
|
||||
|
||||
function checkProfiles (dev)
|
||||
local device_id = dev["bound-id"]
|
||||
local props = dev.properties
|
||||
|
|
@ -488,19 +546,19 @@ function checkProfiles (dev)
|
|||
return
|
||||
end
|
||||
|
||||
-- Create the loopback device if never created before
|
||||
local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||
if loopback == nil then
|
||||
-- Create the source loopback device if never created before
|
||||
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||
if source_loopback == nil then
|
||||
local dev_name = props["api.bluez5.address"] or props["device.name"]
|
||||
local dec_desc = props["device.description"] or props["device.name"]
|
||||
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
|
||||
|
||||
log:info("create SCO loopback node: " .. dev_name)
|
||||
log:info("create SCO source loopback node: " .. dev_name)
|
||||
|
||||
-- sanitize description, replace ':' with ' '
|
||||
dec_desc = dec_desc:gsub("(:)", " ")
|
||||
loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback)
|
||||
source_loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
|
||||
|
||||
-- recreate any sco source node
|
||||
local properties = sco_source_node_properties[device_spa_id]
|
||||
|
|
@ -521,6 +579,39 @@ function checkProfiles (dev)
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
local sink_loopback = spa_device:get_managed_object (LOOPBACK_SINK_ID)
|
||||
if sink_loopback == nil then
|
||||
local dev_name = props["api.bluez5.address"] or props["device.name"]
|
||||
local dec_desc = props["device.description"] or props["device.name"]
|
||||
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
|
||||
|
||||
log:info("create SCO-A2DP sink loopback node: " .. dev_name)
|
||||
|
||||
-- sanitize description, replace ':' with ' '
|
||||
dec_desc = dec_desc:gsub("(:)", " ")
|
||||
sink_loopback = CreateDeviceLoopbackSink (dev_name, dec_desc, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SINK_ID, sink_loopback)
|
||||
|
||||
-- recreate any sco-a2dp sink node
|
||||
local properties = sco_a2dp_sink_node_properties[device_spa_id]
|
||||
if properties ~= nil then
|
||||
local node_id = tonumber(properties["spa.object.id"])
|
||||
local node = spa_device:get_managed_object (node_id)
|
||||
if node ~= nil then
|
||||
log:info("Recreate node: " .. properties["node.name"] .. ": " ..
|
||||
properties["factory.name"] .. " " .. tostring (node_id))
|
||||
|
||||
spa_device:store_managed_object(node_id, nil)
|
||||
|
||||
properties["bluez5.sink-loopback-target"] = true
|
||||
properties["api.bluez5.internal"] = true
|
||||
node = LocalNode("adapter", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
spa_device:store_managed_object(node_id, node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function onDeviceParamsChanged (dev, param_name)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ SimpleEventHook {
|
|||
-- Create group loopback module if it does not exist
|
||||
local m = group_loopback_modules [direction][group]
|
||||
if m == nil then
|
||||
Log.warning ("Creating " .. direction .. " loopback for audio group " .. group ..
|
||||
Log.info ("Creating " .. direction .. " loopback for audio group " .. group ..
|
||||
(target_object and (" with target object " .. tostring (target_object)) or ""))
|
||||
m = CreateStreamLoopback (stream_props, group, target_object, direction)
|
||||
group_loopback_modules [direction][group] = m
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ items = {}
|
|||
function configProperties (node)
|
||||
local properties = node.properties
|
||||
local media_class = properties ["media.class"] or ""
|
||||
local factory_name = properties ["factory.name"] or ""
|
||||
|
||||
-- ensure a media.type is set
|
||||
if not properties ["media.type"] then
|
||||
|
|
@ -39,6 +40,9 @@ function configProperties (node)
|
|||
Settings.get_boolean ("node.features.audio.monitor-ports")
|
||||
properties ["item.features.control-port"] =
|
||||
Settings.get_boolean ("node.features.audio.control-port")
|
||||
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")
|
||||
properties ["node.id"] = node ["bound-id"]
|
||||
|
||||
-- set the default media.role, if configured
|
||||
|
|
@ -152,3 +156,50 @@ SimpleEventHook {
|
|||
|
||||
end
|
||||
}:register ()
|
||||
|
||||
function reconfigureAudioAdapters ()
|
||||
local ids = {}
|
||||
|
||||
-- Get the Id of all session items that are audio adapters
|
||||
for id, item in pairs(items) do
|
||||
local si_props = item.properties
|
||||
if si_props ["item.factory.name"] == "si-audio-adapter" then
|
||||
table.insert (ids, id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Re-configure all audio adapters
|
||||
for _, id in pairs (ids) do
|
||||
local item = items[id]
|
||||
local node = item:get_associated_proxy ("node")
|
||||
|
||||
log:info (item, "Started re-configuring audio adapter")
|
||||
|
||||
-- Remove the session item so that it is unlinked
|
||||
items[id] = nil
|
||||
item:remove()
|
||||
|
||||
-- Configure the session item
|
||||
if not item:configure (configProperties (node)) then
|
||||
log:warning (item, "Could not re-configure audio adapter")
|
||||
goto skip_item
|
||||
end
|
||||
|
||||
-- Activate the session item so that it is linked again
|
||||
items[id] = item
|
||||
item:activate (Features.ALL, function (si, e)
|
||||
if e then
|
||||
log:warning (si, "Could not re-activate audio adapter")
|
||||
else
|
||||
log:info (si, "Successfully re-activated audio adapter")
|
||||
si:register ()
|
||||
end
|
||||
end)
|
||||
|
||||
::skip_item::
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("node.features.audio.*", function ()
|
||||
reconfigureAudioAdapters ()
|
||||
end)
|
||||
|
|
|
|||
53
src/scripts/node/filter-graph.lua
Normal file
53
src/scripts/node/filter-graph.lua
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2025 The WirePlumber project contributors
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
log = Log.open_topic("s-node")
|
||||
|
||||
config = {}
|
||||
config.rules = Conf.get_section_as_json ("node.filter-graph.rules", Json.Array{})
|
||||
|
||||
function setNodeFilterGraphParams (node, graph_params)
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Props", "Props",
|
||||
params = Pod.Struct (graph_params)
|
||||
}
|
||||
node:set_params("Props", pod)
|
||||
end
|
||||
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/create-filter-graph",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "library.name", "=", "audioconvert/libspa-audioconvert", type = "pw" },
|
||||
},
|
||||
},
|
||||
execute = function(event)
|
||||
local node = event:get_subject()
|
||||
|
||||
JsonUtils.match_rules (config.rules, node.properties, function (action, value)
|
||||
|
||||
if action == "create-filter-graph" then
|
||||
local graphs = value:parse (1)
|
||||
|
||||
local graph_params = {}
|
||||
for idx, val in ipairs (graphs) do
|
||||
local index = tonumber(idx) - 1
|
||||
local key = "audioconvert.filter-graph." .. tostring (index)
|
||||
|
||||
log:info (node, "setting node filter graph param '" .. key .. "' to: " .. val)
|
||||
|
||||
table.insert(graph_params, key)
|
||||
table.insert(graph_params, val)
|
||||
end
|
||||
|
||||
setNodeFilterGraphParams (node, graph_params)
|
||||
end
|
||||
end)
|
||||
end
|
||||
}:register()
|
||||
94
src/scripts/node/find-media-role-default-volume.lua
Normal file
94
src/scripts/node/find-media-role-default-volume.lua
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2025 Phosh.mobi e.V.
|
||||
-- @author Guido Günther <agx@sigxcpu.org>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Select the media role default volume
|
||||
|
||||
log = Log.open_topic("s-node")
|
||||
|
||||
local cutils = require ("common-utils")
|
||||
|
||||
function findHighestPriorityRoleNode (node_om)
|
||||
local best_role = nil
|
||||
local best_prio = 0
|
||||
|
||||
local default_role = Settings.get ("node.stream.default-media-role")
|
||||
if default_role then
|
||||
default_role = default_role:parse()
|
||||
end
|
||||
|
||||
for ni in node_om:iterate {
|
||||
type = "node",
|
||||
Constraint { "media.class", "=", "Audio/Sink" },
|
||||
Constraint { "node.name", "#", "input.loopback.sink.role.*" },
|
||||
} do
|
||||
local ni_props = ni.properties
|
||||
local roles = ni_props["device.intended-roles"]
|
||||
local node_name = ni_props ["node.name"]
|
||||
local prio = tonumber(ni_props ["policy.role-based.priority"])
|
||||
|
||||
-- Use the node that handles the default_role as fallback
|
||||
-- when no node is in running state
|
||||
if best_role == nil and roles and default_role then
|
||||
local roles_table = Json.Raw(roles):parse()
|
||||
for i, v in ipairs (roles_table) do
|
||||
if default_role == v then
|
||||
best_role = node_name
|
||||
best_prio = prio
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ni.state == "running" then
|
||||
if prio > best_prio then
|
||||
best_role = node_name
|
||||
best_prio = prio
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log:info (string.format ("Volume control is on : '%s', prio %d", best_role, best_prio))
|
||||
local metadata = cutils.get_default_metadata_object ()
|
||||
metadata:set (0, "current.role-based.volume.control", "Spa:String:JSON",
|
||||
Json.Object { ["name"] = best_role }:to_string ())
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/rescan-for-media-role-volume",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "rescan-for-media-role-volume" }
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
findHighestPriorityRoleNode (node_om)
|
||||
end
|
||||
}:register ()
|
||||
|
||||
-- Track best volume control for media role based priorities
|
||||
SimpleEventHook {
|
||||
name = "node/find-media-role-default-volume",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "=", "Audio/Sink" },
|
||||
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "=", "Audio/Sink" },
|
||||
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
source:call ("schedule-rescan", "media-role-volume")
|
||||
end
|
||||
}:register ()
|
||||
|
|
@ -4,6 +4,11 @@ executable('wpctl',
|
|||
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_dir: get_option('datadir') / 'zsh/site-functions',
|
||||
rename: '_wpctl'
|
||||
|
|
|
|||
30
src/tools/shell-completion/wpctl.bash
Normal file
30
src/tools/shell-completion/wpctl.bash
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
_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"
|
||||
|
||||
_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"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
complete -F _wpctl wpctl
|
||||
|
|
@ -174,7 +174,6 @@ test_node (TestFixture *f, gconstpointer data)
|
|||
|
||||
props = wp_pipewire_object_get_properties (proxy);
|
||||
g_assert_nonnull (props);
|
||||
g_assert_true (wp_properties_peek_dict (props) == info->props);
|
||||
id = wp_properties_get (props, PW_KEY_OBJECT_ID);
|
||||
g_assert_nonnull (id);
|
||||
g_assert_cmpint (info->id, ==, atoi(id));
|
||||
|
|
|
|||
|
|
@ -64,3 +64,9 @@ test(
|
|||
args: ['lua-api-tests', 'event-hooks.lua'],
|
||||
env: common_env,
|
||||
)
|
||||
test(
|
||||
'test-lua-properties',
|
||||
script_tester,
|
||||
args: ['lua-api-tests', 'properties.lua'],
|
||||
env: common_env,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -177,6 +177,23 @@ assert (#val == 0)
|
|||
assert (val.key1 == nil)
|
||||
assert (json:get_data() == "{}")
|
||||
|
||||
json = Json.Object (
|
||||
Properties {
|
||||
["key0"] = nil,
|
||||
["key1"] = false,
|
||||
["key2"] = 64,
|
||||
["key3"] = 2.71,
|
||||
["key4"] = "string",
|
||||
}
|
||||
)
|
||||
assert (json:is_object())
|
||||
val = json:parse ()
|
||||
assert (val.key0 == nil)
|
||||
assert (val.key1 == "false")
|
||||
assert (val.key2 == "64")
|
||||
assert (tonumber (val.key3) > 2.70 and tonumber (val.key3) < 2.72)
|
||||
assert (val.key4 == "string")
|
||||
|
||||
-- Raw
|
||||
json = Json.Raw ("[\"foo\", \"bar\"]")
|
||||
assert (json:is_array())
|
||||
|
|
|
|||
93
tests/wplua/scripts/properties.lua
Normal file
93
tests/wplua/scripts/properties.lua
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
-- create empty properties
|
||||
props = Properties ()
|
||||
|
||||
-- set nil
|
||||
props["key-nil"] = nil
|
||||
assert (props["key-nil"] == nil)
|
||||
|
||||
-- set bool
|
||||
props["key-bool"] = false
|
||||
assert (props["key-bool"] == "false")
|
||||
assert (props:get_boolean ("key-bool") == false)
|
||||
props["key-bool"] = true
|
||||
assert (props["key-bool"] == "true")
|
||||
assert (props:get_boolean ("key-bool") == true)
|
||||
|
||||
-- set int
|
||||
props["key-int"] = 4
|
||||
assert (props["key-int"] == "4")
|
||||
assert (props:get_int ("key-int") == 4)
|
||||
|
||||
-- set float
|
||||
props["key-float"] = 3.14
|
||||
val = props:get_float ("key-float")
|
||||
assert (val > 3.13 and val < 3.15)
|
||||
|
||||
-- set string
|
||||
props["key-string"] = "value"
|
||||
assert (props["key-string"] == "value")
|
||||
assert (props:get_boolean ("key-string") == false)
|
||||
assert (props:get_int ("key-string") == nil)
|
||||
assert (props:get_float ("key-string") == nil)
|
||||
|
||||
-- copy
|
||||
copy = props:copy ()
|
||||
assert (copy["key-nil"] == nil)
|
||||
assert (copy:get_boolean ("key-bool") == true)
|
||||
assert (copy:get_int ("key-int") == 4)
|
||||
val = copy:get_float ("key-float")
|
||||
assert (val > 3.13 and val < 3.15)
|
||||
assert (copy["key-string"] == "value")
|
||||
|
||||
-- remove int property
|
||||
props["key-int"] = nil
|
||||
assert (props["key-int"] == nil)
|
||||
assert (copy:get_int ("key-int") == 4)
|
||||
|
||||
-- create properties from table
|
||||
props = Properties {
|
||||
["key0"] = nil,
|
||||
["key1"] = false,
|
||||
["key2"] = 64,
|
||||
["key3"] = 2.71,
|
||||
["key4"] = "string",
|
||||
}
|
||||
assert (props["key0"] == nil)
|
||||
assert (props:get_boolean ("key1") == false)
|
||||
assert (props:get_int ("key2") == 64)
|
||||
val = props:get_float ("key3")
|
||||
assert (val > 2.70 and val < 2.72)
|
||||
assert (props["key4"] == "string")
|
||||
|
||||
-- count
|
||||
assert (props:get_count () == 4)
|
||||
|
||||
-- parse
|
||||
parsed = props:parse ()
|
||||
assert (parsed["key0"] == nil)
|
||||
assert (parsed["key1"] == "false")
|
||||
assert (tonumber (parsed["key2"]) == 64)
|
||||
val = tonumber (parsed["key3"])
|
||||
assert (val > 2.70 and val < 2.72)
|
||||
assert (parsed["key4"] == "string")
|
||||
|
||||
-- pairs
|
||||
values = {}
|
||||
for k, v in pairs (props) do
|
||||
values [k] = v
|
||||
end
|
||||
assert (values["key0"] == nil)
|
||||
assert (values["key1"] == "false")
|
||||
assert (tonumber (values["key2"]) == 64)
|
||||
val = tonumber (values["key3"])
|
||||
assert (val > 2.70 and val < 2.72)
|
||||
assert (values["key4"] == "string")
|
||||
|
||||
-- Make sure the reference changes are also updated
|
||||
local properties = Properties ()
|
||||
properties["key"] = "value"
|
||||
assert (properties["key"] == "value")
|
||||
local properties2 = properties
|
||||
properties2["key"] = "another-value"
|
||||
assert (properties2["key"] == "another-value")
|
||||
assert (properties["key"] == "another-value")
|
||||
Loading…
Add table
Reference in a new issue