mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-07 02:58:06 +02:00
Compare commits
No commits in common. "master" and "0.4.81" have entirely different histories.
315 changed files with 8351 additions and 25700 deletions
117
.gitlab-ci.yml
117
.gitlab-ci.yml
|
|
@ -1,15 +1,6 @@
|
||||||
# Create merge request pipelines for open merge requests, branch pipelines
|
|
||||||
# otherwise. This allows MRs for new users to run CI, and prevents duplicate
|
|
||||||
# pipelines for branches with open MRs.
|
|
||||||
workflow:
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
|
||||||
when: never
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- container
|
- container
|
||||||
|
- container_coverity
|
||||||
- build
|
- build
|
||||||
- analysis
|
- analysis
|
||||||
- pages
|
- pages
|
||||||
|
|
@ -19,8 +10,8 @@ variables:
|
||||||
# change to build against a different tag or branch of pipewire
|
# change to build against a different tag or branch of pipewire
|
||||||
PIPEWIRE_HEAD: 'master'
|
PIPEWIRE_HEAD: 'master'
|
||||||
|
|
||||||
# ci-templates as of Feb 14th 2025
|
# ci-templates as of Mar 24th 2023
|
||||||
.templates_sha: &templates_sha ef5e4669b7500834a17ffe9277e15fbb6d977fff
|
.templates_sha: &templates_sha dd90ac0d7a03b574eb4f18d7358083f0c97825f3
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- project: 'freedesktop/ci-templates'
|
- project: 'freedesktop/ci-templates'
|
||||||
|
|
@ -36,8 +27,8 @@ include:
|
||||||
.fedora:
|
.fedora:
|
||||||
variables:
|
variables:
|
||||||
# Update this tag when you want to trigger a rebuild
|
# Update this tag when you want to trigger a rebuild
|
||||||
FDO_DISTRIBUTION_TAG: '2025-03-05.1'
|
FDO_DISTRIBUTION_TAG: '2023-03-24.1'
|
||||||
FDO_DISTRIBUTION_VERSION: '41'
|
FDO_DISTRIBUTION_VERSION: '37'
|
||||||
# findutils: used by the .build script below
|
# findutils: used by the .build script below
|
||||||
# dbus-devel: required by pipewire
|
# dbus-devel: required by pipewire
|
||||||
# dbus-daemon: required by GDBus unit tests
|
# dbus-daemon: required by GDBus unit tests
|
||||||
|
|
@ -89,8 +80,8 @@ include:
|
||||||
.alpine:
|
.alpine:
|
||||||
variables:
|
variables:
|
||||||
# Update this tag when you want to trigger a rebuild
|
# Update this tag when you want to trigger a rebuild
|
||||||
FDO_DISTRIBUTION_TAG: '2025-03-05.1'
|
FDO_DISTRIBUTION_TAG: '2023-03-24.1'
|
||||||
FDO_DISTRIBUTION_VERSION: '3.21'
|
FDO_DISTRIBUTION_VERSION: '3.15'
|
||||||
FDO_DISTRIBUTION_PACKAGES: >-
|
FDO_DISTRIBUTION_PACKAGES: >-
|
||||||
dbus
|
dbus
|
||||||
dbus-dev
|
dbus-dev
|
||||||
|
|
@ -121,23 +112,14 @@ include:
|
||||||
tar xf /tmp/cov-analysis-linux64.tgz ;
|
tar xf /tmp/cov-analysis-linux64.tgz ;
|
||||||
mv cov-analysis-linux64-* coverity ;
|
mv cov-analysis-linux64-* coverity ;
|
||||||
rm /tmp/cov-analysis-linux64.tgz
|
rm /tmp/cov-analysis-linux64.tgz
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $COVERITY
|
||||||
|
|
||||||
.rules_on_success_except_coverity:
|
.not_coverity:
|
||||||
rules:
|
except:
|
||||||
- if: $COVERITY
|
variables:
|
||||||
when: never
|
- $COVERITY
|
||||||
- when: on_success
|
|
||||||
|
|
||||||
.rules_only_on_coverity:
|
|
||||||
rules:
|
|
||||||
- if: $COVERITY
|
|
||||||
|
|
||||||
.rules_only_on_mr_and_branch:
|
|
||||||
rules:
|
|
||||||
- if: $COVERITY
|
|
||||||
when: never
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH != "master"
|
|
||||||
|
|
||||||
.build:
|
.build:
|
||||||
before_script:
|
before_script:
|
||||||
|
|
@ -154,22 +136,13 @@ include:
|
||||||
# Fedora also ships that, but without the test plugins that we need...
|
# Fedora also ships that, but without the test plugins that we need...
|
||||||
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
|
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
|
||||||
https://gitlab.freedesktop.org/pipewire/pipewire.git
|
https://gitlab.freedesktop.org/pipewire/pipewire.git
|
||||||
# Set build options based on PipeWire version
|
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
|
||||||
- |
|
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled
|
||||||
case "$PIPEWIRE_HEAD" in
|
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
|
||||||
1.0|1.2|1.4)
|
-Dvulkan=disabled -Dgstreamer=disabled -Dsystemd=disabled
|
||||||
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
|
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
|
||||||
;;
|
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
|
||||||
*)
|
-Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
||||||
export PIPEWIRE_BUILD_OPTIONS="-Dlibsystemd=disabled"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" $PIPEWIRE_BUILD_OPTIONS
|
|
||||||
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled
|
|
||||||
-Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled
|
|
||||||
-Dgstreamer=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled
|
|
||||||
-Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled
|
|
||||||
-Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
|
||||||
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
|
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
|
||||||
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
|
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
|
||||||
# misc environment only for wireplumber
|
# misc environment only for wireplumber
|
||||||
|
|
@ -191,7 +164,6 @@ include:
|
||||||
|
|
||||||
container_fedora:
|
container_fedora:
|
||||||
extends:
|
extends:
|
||||||
- .rules_on_success_except_coverity
|
|
||||||
- .fedora
|
- .fedora
|
||||||
- .fdo.container-build@fedora
|
- .fdo.container-build@fedora
|
||||||
stage: container
|
stage: container
|
||||||
|
|
@ -200,7 +172,6 @@ container_fedora:
|
||||||
|
|
||||||
container_ubuntu:
|
container_ubuntu:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_mr_and_branch
|
|
||||||
- .ubuntu
|
- .ubuntu
|
||||||
- .fdo.container-build@ubuntu
|
- .fdo.container-build@ubuntu
|
||||||
stage: container
|
stage: container
|
||||||
|
|
@ -209,7 +180,6 @@ container_ubuntu:
|
||||||
|
|
||||||
container_alpine:
|
container_alpine:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_mr_and_branch
|
|
||||||
- .alpine
|
- .alpine
|
||||||
- .fdo.container-build@alpine
|
- .fdo.container-build@alpine
|
||||||
stage: container
|
stage: container
|
||||||
|
|
@ -218,18 +188,17 @@ container_alpine:
|
||||||
|
|
||||||
container_coverity:
|
container_coverity:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_coverity
|
|
||||||
- .fedora
|
- .fedora
|
||||||
- .coverity
|
- .coverity
|
||||||
- .fdo.container-build@fedora
|
- .fdo.container-build@fedora
|
||||||
stage: container
|
stage: container_coverity
|
||||||
variables:
|
variables:
|
||||||
GIT_STRATEGY: none
|
GIT_STRATEGY: none
|
||||||
|
|
||||||
build_on_fedora_with_docs:
|
build_on_fedora_with_docs:
|
||||||
extends:
|
extends:
|
||||||
- .rules_on_success_except_coverity
|
|
||||||
- .fedora
|
- .fedora
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@fedora
|
- .fdo.distribution-image@fedora
|
||||||
- .build
|
- .build
|
||||||
stage: build
|
stage: build
|
||||||
|
|
@ -238,21 +207,18 @@ build_on_fedora_with_docs:
|
||||||
|
|
||||||
build_on_fedora_no_docs:
|
build_on_fedora_no_docs:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_mr_and_branch
|
|
||||||
- .fedora
|
- .fedora
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@fedora
|
- .fdo.distribution-image@fedora
|
||||||
- .build
|
- .build
|
||||||
stage: build
|
stage: build
|
||||||
variables:
|
variables:
|
||||||
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
|
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
|
||||||
parallel:
|
|
||||||
matrix:
|
|
||||||
- PIPEWIRE_HEAD: ['master', '1.4', '1.2', '1.0']
|
|
||||||
|
|
||||||
build_on_ubuntu_with_gir:
|
build_on_ubuntu_with_gir:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_mr_and_branch
|
|
||||||
- .ubuntu
|
- .ubuntu
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@ubuntu
|
- .fdo.distribution-image@ubuntu
|
||||||
- .build
|
- .build
|
||||||
stage: build
|
stage: build
|
||||||
|
|
@ -261,8 +227,8 @@ build_on_ubuntu_with_gir:
|
||||||
|
|
||||||
build_on_ubuntu_no_gir:
|
build_on_ubuntu_no_gir:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_mr_and_branch
|
|
||||||
- .ubuntu
|
- .ubuntu
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@ubuntu
|
- .fdo.distribution-image@ubuntu
|
||||||
- .build
|
- .build
|
||||||
stage: build
|
stage: build
|
||||||
|
|
@ -271,8 +237,8 @@ build_on_ubuntu_no_gir:
|
||||||
|
|
||||||
build_on_alpine:
|
build_on_alpine:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_mr_and_branch
|
|
||||||
- .alpine
|
- .alpine
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@alpine
|
- .fdo.distribution-image@alpine
|
||||||
- .build
|
- .build
|
||||||
stage: build
|
stage: build
|
||||||
|
|
@ -281,7 +247,6 @@ build_on_alpine:
|
||||||
|
|
||||||
build_with_coverity:
|
build_with_coverity:
|
||||||
extends:
|
extends:
|
||||||
- .rules_only_on_coverity
|
|
||||||
- .fedora
|
- .fedora
|
||||||
- .coverity
|
- .coverity
|
||||||
- .fdo.suffixed-image@fedora
|
- .fdo.suffixed-image@fedora
|
||||||
|
|
@ -311,23 +276,16 @@ build_with_coverity:
|
||||||
shellcheck:
|
shellcheck:
|
||||||
extends:
|
extends:
|
||||||
- .fedora
|
- .fedora
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@fedora
|
- .fdo.distribution-image@fedora
|
||||||
stage: analysis
|
stage: analysis
|
||||||
script:
|
script:
|
||||||
- shellcheck $(git grep -l "#\!/.*bin/.*sh")
|
- shellcheck $(git grep -l "#\!/.*bin/.*sh")
|
||||||
rules:
|
|
||||||
- if: $COVERITY
|
|
||||||
when: never
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
changes:
|
|
||||||
- "**/*.sh"
|
|
||||||
- if: $CI_COMMIT_BRANCH != "master"
|
|
||||||
changes:
|
|
||||||
- "**/*.sh"
|
|
||||||
|
|
||||||
linguas_check:
|
linguas_check:
|
||||||
extends:
|
extends:
|
||||||
- .fedora
|
- .fedora
|
||||||
|
- .not_coverity
|
||||||
- .fdo.distribution-image@fedora
|
- .fdo.distribution-image@fedora
|
||||||
stage: analysis
|
stage: analysis
|
||||||
script:
|
script:
|
||||||
|
|
@ -336,17 +294,10 @@ linguas_check:
|
||||||
- ls *.po | sed s/.po//g | sort > LINGUAS.new
|
- ls *.po | sed s/.po//g | sort > LINGUAS.new
|
||||||
- diff -u LINGUAS.sorted LINGUAS.new
|
- diff -u LINGUAS.sorted LINGUAS.new
|
||||||
- rm -f LINGUAS.*
|
- rm -f LINGUAS.*
|
||||||
rules:
|
|
||||||
- if: $COVERITY
|
|
||||||
when: never
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
changes:
|
|
||||||
- po/*
|
|
||||||
- if: $CI_COMMIT_BRANCH != "master"
|
|
||||||
changes:
|
|
||||||
- po/*
|
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
|
extends:
|
||||||
|
- .not_coverity
|
||||||
stage: pages
|
stage: pages
|
||||||
dependencies:
|
dependencies:
|
||||||
- build_on_fedora_with_docs
|
- build_on_fedora_with_docs
|
||||||
|
|
@ -356,7 +307,5 @@ pages:
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
rules:
|
only:
|
||||||
- if: $COVERITY
|
- master
|
||||||
when: never
|
|
||||||
- if: $CI_COMMIT_BRANCH == "master"
|
|
||||||
|
|
|
||||||
37
AGENTS.md
37
AGENTS.md
|
|
@ -1,37 +0,0 @@
|
||||||
## Building and Testing
|
|
||||||
|
|
||||||
- To compile the project: `meson compile -C build` (compiles everything, no target needed)
|
|
||||||
- To run tests: `meson test -C build`
|
|
||||||
- The build artifacts always live in a directory called `build` or `builddir`.
|
|
||||||
If `build` doesn't exist, use `-C builddir` in the meson commands.
|
|
||||||
|
|
||||||
## Git Workflow
|
|
||||||
|
|
||||||
- Main branch: `master`
|
|
||||||
- Always create feature branches for new work
|
|
||||||
- Use descriptive commit messages following project conventions
|
|
||||||
- Reference GitLab MR/issue numbers in commits where applicable
|
|
||||||
- Never commit build artifacts or temporary files
|
|
||||||
- Use `glab` CLI tool for GitLab interactions (MRs, issues, etc.)
|
|
||||||
|
|
||||||
## Making a release
|
|
||||||
|
|
||||||
- Each release always consists of an entry in NEWS.rst, at the top of the file, which describes
|
|
||||||
the changes between the previous release and the current one. In addition, each release is given
|
|
||||||
a unique version number, which is present:
|
|
||||||
1. on the section header of that NEWS.rst entry
|
|
||||||
2. in the project() command in meson.build
|
|
||||||
3. on the commit message of the commit that introduces the above 2 changes
|
|
||||||
4. on the git tag that marks the above commit
|
|
||||||
- In order to make a release:
|
|
||||||
- Begin by analyzing the git history and the merged MRs from GitLab between the previous release
|
|
||||||
and today. GitLab MRs that are relevant always have the new release's version number set as a
|
|
||||||
"milestone"
|
|
||||||
- Create a new entry in NEWS.rst describing the changes, in a similar style and format as the
|
|
||||||
previous entries. Consolidate the changes to larger work items and also reference the relevant
|
|
||||||
gitlab MR that corresponds to each change and/or the gitlab issues that were addressed by each
|
|
||||||
change.
|
|
||||||
- Make sure to move the "Past releases" section header up, so that the only 2 top-level sections
|
|
||||||
are the new release section and the "Past releases" section.
|
|
||||||
- Edit meson.build to change the project version to the new release number
|
|
||||||
- Do not commit anything to git. Let the user review the changes and commit manually.
|
|
||||||
654
NEWS.rst
654
NEWS.rst
|
|
@ -1,654 +1,5 @@
|
||||||
WirePlumber 0.5.14
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Additions & Enhancements:
|
|
||||||
|
|
||||||
- Added per-device default volume configuration via the
|
|
||||||
``device.routes.default-{source,sink}-volume`` property, allowing device-specific volume
|
|
||||||
defaults (e.g. a comfortable default for internal speakers or no attenuation for HDMI) (!772)
|
|
||||||
|
|
||||||
- Added Lua 5.5 support; the bundled Lua subproject wrap has also been updated to 5.5.0
|
|
||||||
(!775, !788)
|
|
||||||
|
|
||||||
- Enhanced libcamera monitor to load camera nodes locally within the WirePlumber
|
|
||||||
process instead of the PipeWire daemon, eliminating race conditions that could occur
|
|
||||||
during initial enumeration and hotplug events (!790)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth loopback nodes to always be created when a device supports both
|
|
||||||
A2DP and HSP/HFP profiles, simplifying the logic and making the BT profile autoswitch
|
|
||||||
setting take effect immediately without requiring device reconnection (!782)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth loopback nodes to use ``target.object`` property instead of smart
|
|
||||||
filters, fixing issues that prevented users from setting them as default nodes and
|
|
||||||
also allowing smart filters to be used with them (#898; !792)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth profile autoswitch logic with further robustness improvements,
|
|
||||||
including better headset profile detection using profile name patterns and resolving
|
|
||||||
race conditions by running profile switching after ``device/apply-profile`` in a
|
|
||||||
dedicated event hook (#926, #923; !776, !777, !808)
|
|
||||||
|
|
||||||
- Enhanced wpctl ``set-default`` command to accept virtual nodes (e.g.
|
|
||||||
``Audio/Source/Virtual``) in addition to regular device nodes (#896; !787)
|
|
||||||
|
|
||||||
- Improved stream linking to make the full graph rescan optional when linkable items
|
|
||||||
change, saving CPU on low-end systems and reducing audio startup latency when
|
|
||||||
connecting multiple streams in quick succession (!800)
|
|
||||||
|
|
||||||
- Allowed installation of systemd service units without libsystemd being present,
|
|
||||||
useful for distributions like Alpine Linux that allow systemd service subpackages
|
|
||||||
(!793)
|
|
||||||
|
|
||||||
- Allowed the ``mincore`` syscall in the WirePlumber systemd sandbox, required for
|
|
||||||
Mesa/EGL (e.g. for the libcamera GPUISP pipeline)
|
|
||||||
|
|
||||||
- Allowed passing ``WIREPLUMBER_CONFIG_DIR`` via the ``wp-uninstalled`` script,
|
|
||||||
useful for passing additional configuration paths in an uninstalled environment (!801)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Removed Bluetooth sink loopback node, which was causing issues with KDE and GNOME (!794)
|
|
||||||
|
|
||||||
- Fixed default audio source selection to never automatically use ``Audio/Sink`` nodes
|
|
||||||
as the default source unless explicitly selected by the user (#886; !781)
|
|
||||||
|
|
||||||
- Fixed crash in ``state-stream`` when the Format parameter has a Choice for the
|
|
||||||
number of channels (#903; !795)
|
|
||||||
|
|
||||||
- Fixed BAP Bluetooth device set channel properties, where ``audio.position`` was
|
|
||||||
incorrectly serialized as a pointer address instead of the channel array (!786)
|
|
||||||
|
|
||||||
- Fixed memory leaks in ``wp_interest_event_hook_get_matching_event_types`` and in
|
|
||||||
the Lua ``LocalModule()`` implementation (!784, !810)
|
|
||||||
|
|
||||||
- Fixed HFP HF stream media class being incorrectly assigned due to
|
|
||||||
``api.bluez5.internal=true`` being set on HFP HF streams (!809)
|
|
||||||
|
|
||||||
- Fixed Lua 5.4 compatibility in ``state-stream`` script
|
|
||||||
|
|
||||||
- Updated translations: Bulgarian, Georgian, Kazakh, Swedish
|
|
||||||
|
|
||||||
Past releases
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
WirePlumber 0.5.13
|
|
||||||
..................
|
|
||||||
|
|
||||||
Additions & Enhancements:
|
|
||||||
|
|
||||||
- Added internal filter graph support for audio nodes, allowing users to
|
|
||||||
create audio preprocessing and postprocessing chains without exposing
|
|
||||||
filters to applications, useful for software DSP (!743)
|
|
||||||
|
|
||||||
- Added new Lua Properties API that significantly improves performance by
|
|
||||||
avoiding constant serialization between WpProperties and Lua tables,
|
|
||||||
resulting in approximately 40% faster node linking (!757)
|
|
||||||
|
|
||||||
- Added WpIterator Lua API for more efficient parameter enumeration (!746)
|
|
||||||
|
|
||||||
- Added bash completions for wpctl command (!762)
|
|
||||||
|
|
||||||
- Added script to find suitable volume control when using role-based policy,
|
|
||||||
allowing volume sliders to automatically adjust the volume of the currently
|
|
||||||
active role (e.g., ringing, call, media) (!711)
|
|
||||||
|
|
||||||
- Added experimental HDMI channel detection setting to use HDMI ELD
|
|
||||||
information for channel configuration (!749)
|
|
||||||
|
|
||||||
- Enhanced role-based policy to allow setting preferred target sinks for
|
|
||||||
media role loopbacks via ``policy.role-based.preferred-target`` (!754)
|
|
||||||
|
|
||||||
- Enhanced Bluetooth profile autoswitch logic to be more robust and handle
|
|
||||||
saved profiles correctly, including support for loopback sink nodes (!739)
|
|
||||||
|
|
||||||
- Enhanced ALSA monitor to include ``alsa.*`` device properties on nodes for
|
|
||||||
rule matching (!761)
|
|
||||||
|
|
||||||
- Optimized stream node linking for common cases to reduce latency when new
|
|
||||||
audio/video streams are added (!760)
|
|
||||||
|
|
||||||
- Improved event dispatcher performance by using hash table registration for
|
|
||||||
event hooks, eliminating performance degradation as more hooks are
|
|
||||||
registered (!765)
|
|
||||||
|
|
||||||
- Increased audio headroom for VMware and VirtualBox virtual machines (!756)
|
|
||||||
|
|
||||||
- Added setting to prevent restoring "Off" profiles via
|
|
||||||
``session.dont-restore-off-profile`` property (!753)
|
|
||||||
|
|
||||||
- Added support for 128 audio channels when compiled with a recent version of
|
|
||||||
PipeWire (pipewire#4995; CI checks in !768)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed memory leaks and issues in the modem manager module (!770, !764)
|
|
||||||
|
|
||||||
- Fixed MPRIS module incorrectly treating GHashTable as GObject (!759)
|
|
||||||
|
|
||||||
- Fixed warning messages when process files in ``/proc/<pid>/*`` don't exist,
|
|
||||||
particularly when processes are removed quickly (#816, !717)
|
|
||||||
|
|
||||||
- Fixed MONO audio configuration to only apply to device sink nodes, allowing
|
|
||||||
multi-channel mixing in the graph (!769)
|
|
||||||
|
|
||||||
- Fixed event dispatcher hook registration and removal to avoid spurious
|
|
||||||
errors (!747)
|
|
||||||
|
|
||||||
- Improved logging for standard-link activation failures (!744)
|
|
||||||
|
|
||||||
- Simplified event-hook interest matching for better performance (!758)
|
|
||||||
|
|
||||||
WirePlumber 0.5.12
|
|
||||||
..................
|
|
||||||
|
|
||||||
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
|
|
||||||
device profile selection hooks to improve phone call audio routing on
|
|
||||||
mobile devices (!722, !729, #819)
|
|
||||||
|
|
||||||
- Added MPRIS media player pause functionality that automatically pauses
|
|
||||||
media playback when the audio target (e.g. headphones) is removed (!699, #764)
|
|
||||||
|
|
||||||
- Added support for human-readable names and localization of settings in
|
|
||||||
``wireplumber.conf`` with ``wpctl`` displaying localized setting descriptions (!712)
|
|
||||||
|
|
||||||
- Improved default node selection logic to use both session and route
|
|
||||||
priorities when nodes have equal session priorities (!720)
|
|
||||||
|
|
||||||
- Increased USB device priority in the ALSA monitor (!719)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed multiple Lua runtime issues including type confusion bugs, stack
|
|
||||||
overflow prevention, and SPA POD array/choice builders (!723, !728)
|
|
||||||
|
|
||||||
- Fixed proxy object lifecycle management by properly clearing the
|
|
||||||
OWNED_BY_PROXY flag when proxies are destroyed to prevent dangling
|
|
||||||
pointers (!732)
|
|
||||||
|
|
||||||
- Fixed state-routes handling to prevent saving unavailable routes and
|
|
||||||
eliminate race conditions during profile switching (!730, #762)
|
|
||||||
|
|
||||||
- Fixed some memory leaks in the script tester and the settings iterator (!727, !726)
|
|
||||||
|
|
||||||
- Fixed a potential crash caused by module-loopback destroying itself when the
|
|
||||||
pipewire connection is closed (#812)
|
|
||||||
|
|
||||||
- Fixed profile saving behavior in ``wpctl set-profile`` command (#808)
|
|
||||||
|
|
||||||
- Fixed GObject introspection closure annotation
|
|
||||||
|
|
||||||
WirePlumber 0.5.10
|
|
||||||
..................
|
|
||||||
|
|
||||||
Fixed a critical crash in ``linking-utils.haveAvailableRoutes`` that was
|
|
||||||
introduced accidentally in 0.5.9 and caused loss of audio output on affected
|
|
||||||
systems (#797, #799, #800, !713)
|
|
||||||
|
|
||||||
WirePlumber 0.5.9
|
|
||||||
.................
|
|
||||||
|
|
||||||
Additions & Enhancements:
|
|
||||||
|
|
||||||
- Added a new audio node grouping functionality using an external command line
|
|
||||||
tool (!646)
|
|
||||||
|
|
||||||
- The libcamera monitor now supports devices that are not associated with
|
|
||||||
device ids (!701)
|
|
||||||
|
|
||||||
- The wireplumber user systemd service is now associated with dbus.service to
|
|
||||||
avoid strange warnings when dbus exits (!702)
|
|
||||||
|
|
||||||
- Added "SYSLOG_IDENTIFIER", "SYSLOG_FACILITY", "SYSLOG_PID" and "TID" to log
|
|
||||||
messages that are sent to the journal (!709)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed a crash of ``wpctl set-default`` on 32-bit architectures (#773)
|
|
||||||
|
|
||||||
- Fixed a crash when a configuration component had no 'provides' field (#771)
|
|
||||||
|
|
||||||
- Reduced the log level of some messages that didn't need to be as high (!695)
|
|
||||||
|
|
||||||
- Fixed another nil reference issue in the alsa.lua monitor script (!704)
|
|
||||||
|
|
||||||
- Fixed name deduplication of v4l2 and libcamera devices (!705)
|
|
||||||
|
|
||||||
- Fixed an issue with wpctl not being able to save settings sometimes (!708, #749)
|
|
||||||
|
|
||||||
WirePlumber 0.5.8
|
|
||||||
.................
|
|
||||||
|
|
||||||
Additions & Enhancements:
|
|
||||||
|
|
||||||
- Added support for handling UCM SplitPCM nodes in the ALSA monitor, which
|
|
||||||
allows native PipeWire channel remapping using loopbacks for devices that
|
|
||||||
use this feature (!685)
|
|
||||||
|
|
||||||
- Introduced new functions to mark WpSpaDevice child objects as pending.
|
|
||||||
This allows properly associating asynchronously created loopback nodes with
|
|
||||||
their parent WpSpaDevice without losing ObjectConfig events (!687, !689)
|
|
||||||
|
|
||||||
- Improved the node name deduplication logic in the ALSA monitor to prevent
|
|
||||||
node names with .2, .3, etc appended to them in some more cases (!688)
|
|
||||||
|
|
||||||
- Added a new script to populate ``session.services``. This is a step towards
|
|
||||||
implementing detection of features that PipeWire can service (!686)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed an issue that was causing duplicate Bluetooth SCO (HSP/HFP) source
|
|
||||||
nodes to be shown in UIs (#701, !683)
|
|
||||||
|
|
||||||
- In the BlueZ monitor, marked the source loopback node as non-virtual,
|
|
||||||
addressing how it appears on UIs (#729)
|
|
||||||
|
|
||||||
- Disabled stream-restore for device loopback nodes to prevent unwanted
|
|
||||||
property changes (!691)
|
|
||||||
|
|
||||||
- Fixed ``wp_lua_log_topic_copy()`` to correctly copy topic names (#757)
|
|
||||||
|
|
||||||
- Updated script tests to handle differences in object identifiers
|
|
||||||
(``object.serial`` vs ``node.id``), ensuring proper test behavior (#761)
|
|
||||||
|
|
||||||
WirePlumber 0.5.7
|
|
||||||
.................
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- Fixed an issue that would cause random profile switching when an application
|
|
||||||
was trying to capture from non-Bluetooth devices (#715, #634, !669)
|
|
||||||
|
|
||||||
- Fixed an issue that would cause strange profile selection issues [choices
|
|
||||||
not being remembered or unavailable routes being selected] (#734)
|
|
||||||
|
|
||||||
- Added a timer that delays switching Bluetooth headsets to the HSP/HFP
|
|
||||||
profile, avoiding needless rapid switching when an application is trying to
|
|
||||||
probe device capabilities instead of actually capturing audio (!664)
|
|
||||||
|
|
||||||
- Improved libcamera/v4l2 device deduplication logic to work with more complex
|
|
||||||
devices (!674, !675, #689, #708)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed two memory leaks in module-mixer-api and module-dbus-connection
|
|
||||||
(!672, !673)
|
|
||||||
|
|
||||||
- Fixed a crash that could occur in module-reserve-device (!680, #742)
|
|
||||||
|
|
||||||
- Fixed an issue that would cause the warning "[string "alsa.lua"]:182:
|
|
||||||
attempt to concatenate a nil value (local 'node_name')" to appear in the
|
|
||||||
logs when an ALSA device was busy, breaking node name deduplication (!681)
|
|
||||||
|
|
||||||
- Fixed an issue that could make find-preferred-profile.lua crash instead of
|
|
||||||
properly applying profile priority rules (#751)
|
|
||||||
|
|
||||||
WirePlumber 0.5.6
|
|
||||||
.................
|
|
||||||
|
|
||||||
Additions:
|
|
||||||
|
|
||||||
- Implemented before/after dependencies for components, to ensure correct
|
|
||||||
load order in custom configurations (#600)
|
|
||||||
|
|
||||||
- Implemented profile inheritance in the configuration file. This allows
|
|
||||||
profiles to inherit all the feature specifications of other profiles, which
|
|
||||||
is useful to avoid copying long lists of features just to make small changes
|
|
||||||
|
|
||||||
- Added multi-instance configuration profiles, tested and documented them
|
|
||||||
|
|
||||||
- Added a ``main-systemwide`` profile, which is now the default for instances
|
|
||||||
started via the system-wide systemd service and disables features that
|
|
||||||
depend on the user session (#608)
|
|
||||||
|
|
||||||
- Added a ``wp_core_connect_fd`` method, which allows making a connection to
|
|
||||||
PipeWire via an existing open socket (useful for portal-based connections)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- The Bluetooth auto-switch script now uses the common event source object
|
|
||||||
managers, which should improve its stability (!663)
|
|
||||||
|
|
||||||
- Fix an issue where switching between Bluetooth profiles would temporarily
|
|
||||||
link active audio streams to the internal speakers (!655)
|
|
||||||
|
|
||||||
WirePlumber 0.5.5
|
|
||||||
.................
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- Hotfix release to address crashes in the Bluetooth HSP/HFP autoswitch
|
|
||||||
functionality that were side-effects of some changes that were part
|
|
||||||
of the role-based linking policy (#682)
|
|
||||||
|
|
||||||
Improvements:
|
|
||||||
|
|
||||||
- wpctl will now properly show a '*' in front of sink filters when they are
|
|
||||||
selected as the default sink (!660)
|
|
||||||
|
|
||||||
WirePlumber 0.5.4
|
|
||||||
.................
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- Refactored the role-based linking policy (previously known also as
|
|
||||||
"endpoints" or "virtual items" policy) to blend in with the standard desktop
|
|
||||||
policy. It is now possible use role-based sinks alongside standard desktop
|
|
||||||
audio operations and they will only be used for streams that have a
|
|
||||||
"media.role" defined. It is also possible to force streams to have a
|
|
||||||
media.role, using a setting. Other features include: blending with smart
|
|
||||||
filters in the graph and allowing hardware DSP nodes to be also used easily
|
|
||||||
instead of requiring software loopbacks for all roles. (#610, !649)
|
|
||||||
|
|
||||||
Improvements:
|
|
||||||
|
|
||||||
- Filters that are not declared as smart will now behave again as normal
|
|
||||||
application streams, instead of being treated sometimes differently (!657)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed an issue that would cause WirePlumber to crash at startup if an
|
|
||||||
empty configuration file was present in one of the search paths (#671)
|
|
||||||
|
|
||||||
- Fixed Bluetooth profile auto-switching when a filter is permanently linked
|
|
||||||
to the Bluetooth source (!650)
|
|
||||||
|
|
||||||
- Fixed an issue in the software-dsp script that would cause DSP filters to
|
|
||||||
stay around and cause issues after their device node was destroyed (!651)
|
|
||||||
|
|
||||||
- Fixed an issue in the autoswitch-bluetooth-profile script that could cause
|
|
||||||
an infinite loop of switching between profiles (!652, #617)
|
|
||||||
|
|
||||||
- Fixed a rare issue that could cause WirePlumber to crash when dealing with
|
|
||||||
a device object that didn't have the "device.name" property set (#674)
|
|
||||||
|
|
||||||
WirePlumber 0.5.3
|
|
||||||
.................
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed a long standing issue that would cause many device nodes to have
|
|
||||||
inconsistent naming, with a '.N' suffix (where N is a number >= 2) being
|
|
||||||
appended at seemingly random times (#500)
|
|
||||||
|
|
||||||
- Fixed an issue that would cause unavailable device profiles to be selected
|
|
||||||
if they were previously stored in the state file, sometimes requiring users
|
|
||||||
to manually remove the state file to get things working again (#613)
|
|
||||||
|
|
||||||
- Fixed an occasional crash that could sometimes be triggered by hovering
|
|
||||||
the volume icon on the KDE taskbar, and possibly other similar actions
|
|
||||||
(#628, !644)
|
|
||||||
|
|
||||||
- Fixed camera device deduplication logic when the same device is available
|
|
||||||
through both V4L2 and libcamera, and the libcamera one groups multiple V4L2
|
|
||||||
devices together (#623, !636)
|
|
||||||
|
|
||||||
- Fixed applying the default volume on streams that have no volume previously
|
|
||||||
stored in the state file (#655)
|
|
||||||
|
|
||||||
- Fixed an issue that would prevent some camera nodes - in some cases -
|
|
||||||
from being destroyed when the camera device is removed (#640)
|
|
||||||
|
|
||||||
- Fixed an issue that would cause video stream nodes to be linked with audio
|
|
||||||
smart filters, if smart audio filters were configured (!647)
|
|
||||||
|
|
||||||
- Fixed an issue that would cause WP to re-activate device profiles even
|
|
||||||
though they were already active (!639)
|
|
||||||
|
|
||||||
- Configuration files in standard JSON format (starting with a '{', among
|
|
||||||
other things) are now correctly parsed (#633)
|
|
||||||
|
|
||||||
- Fixed overriding non-container values when merging JSON objects (#653)
|
|
||||||
|
|
||||||
- Functions marked with WP_PRIVATE_API are now also marked as
|
|
||||||
non-introspectable in the gobject-introspection metadata (#599)
|
|
||||||
|
|
||||||
Improvements:
|
|
||||||
|
|
||||||
- Logging on the systemd journal now includes the log topic and also the log
|
|
||||||
level and location directly on the message string when the log level is
|
|
||||||
high enough, which is useful for gathering additional context in logs
|
|
||||||
submitted by users (!640)
|
|
||||||
|
|
||||||
- Added a video-only profile in wireplumber.conf, for systems where only
|
|
||||||
camera & screensharing are to be used (#652)
|
|
||||||
|
|
||||||
- Improved seat state monitoring so that Bluetooth devices are only enabled
|
|
||||||
when the user is active on a local seat, instead of allowing remote users
|
|
||||||
as well (!641)
|
|
||||||
|
|
||||||
- Improved how main filter nodes are detected for the smart filters (!642)
|
|
||||||
|
|
||||||
- Added Lua method to merge JSON containers (!637)
|
|
||||||
|
|
||||||
WirePlumber 0.5.2
|
|
||||||
.................
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- Added support for loading configuration files other than the default
|
|
||||||
wireplumber.conf within Lua scripts (!629)
|
|
||||||
|
|
||||||
- Added support for loading single-section configuration files, without
|
|
||||||
fragments (!629)
|
|
||||||
|
|
||||||
- Updated the node.software-dsp script to be able to load filter-chain graphs
|
|
||||||
from external configuration files, which is needed for Asahi Linux audio
|
|
||||||
DSP configuration (!629)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed destroying camera nodes when the camera device is removed (#627, !631)
|
|
||||||
|
|
||||||
- Fixed an issue with Bluetooth BAP device set naming (!632)
|
|
||||||
|
|
||||||
- Fixed an issue caused by the pipewire event loop not being "entered" as
|
|
||||||
expected (!634, #638)
|
|
||||||
|
|
||||||
- A false positive warning about no modules being loaded is now suppressed
|
|
||||||
when using libpipewire >= 1.0.5 (#620)
|
|
||||||
|
|
||||||
- Default nodes can now be selected using priority.driver when
|
|
||||||
priority.session is not set (#642)
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
- The library version is now generated following pipewire's versioning scheme:
|
|
||||||
libwireplumber-0.5.so.0.5.2 becomes libwireplumber-0.5.so.0.0502.0 (!633)
|
|
||||||
|
|
||||||
WirePlumber 0.5.1
|
|
||||||
.................
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- Added a guide documenting how to migrate configuration from 0.4 to 0.5,
|
|
||||||
also available online at:
|
|
||||||
https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/migration.html
|
|
||||||
If you are packaging WirePlumber for a distribution, please consider
|
|
||||||
informing users about this.
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed an odd issue where microphones would stop being usable when a
|
|
||||||
Bluetooth headset was connected in the HSP/HFP profile (#598, !620)
|
|
||||||
|
|
||||||
- Fixed an issue where it was not possible to store the volume/mute state of
|
|
||||||
system notifications (#604)
|
|
||||||
|
|
||||||
- Fixed a rare crash that could occur when a node was destroyed while the
|
|
||||||
'select-target' event was still being processed (!621)
|
|
||||||
|
|
||||||
- Fixed deleting all the persistent settings via ``wpctl --delete`` (!622)
|
|
||||||
|
|
||||||
- Fixed using Bluetooth autoswitch with A2DP profiles that have an input route
|
|
||||||
(!624)
|
|
||||||
|
|
||||||
- Fixed sending an error to clients when linking fails due to a format
|
|
||||||
mismatch (!625)
|
|
||||||
|
|
||||||
Additions:
|
|
||||||
|
|
||||||
- Added a check that prints a verbose warning when old-style 0.4.x Lua
|
|
||||||
configuration files are found in the system. (#611)
|
|
||||||
|
|
||||||
- The "policy-dsp" script, used in Asahi Linux to provide a software DSP
|
|
||||||
for Apple Sillicon devices, has now been ported to 0.5 properly and
|
|
||||||
documented (#619, !627)
|
|
||||||
|
|
||||||
WirePlumber 0.5.0
|
|
||||||
.................
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
- Bumped the minimum required version of PipeWire to 1.0.2, because we
|
|
||||||
make use of the 'api.bluez5.internal' property of the BlueZ monitor (!613)
|
|
||||||
|
|
||||||
- Improved the naming of Bluetooth nodes when the auto-switching loopback
|
|
||||||
node is present (!614)
|
|
||||||
|
|
||||||
- Updated the documentation on "settings", the Bluetooth monitor, the Access
|
|
||||||
configuration, the file search locations and added a document on how to
|
|
||||||
modify the configuration file (#595, !616)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
- Fixed checking for available routes when selecting the default node (!609)
|
|
||||||
|
|
||||||
- Fixed an issue that was causing an infinite loop storing routes in the
|
|
||||||
state file (!610)
|
|
||||||
|
|
||||||
- Fixed the interpretation of boolean values in the alsa monitor rules (#586, !611)
|
|
||||||
|
|
||||||
- Fixes a Lua crash when we have 2 smart filters, one with a target and one
|
|
||||||
without (!612)
|
|
||||||
|
|
||||||
- Fixed an issue where the default nodes would not be updated when the
|
|
||||||
currently selected default node became unavailable (#588, !615)
|
|
||||||
|
|
||||||
- Fixed an issue that would cause the Props (volume, mute, etc) of loopbacks
|
|
||||||
and other filter nodes to not be restored at startup (#577, !617)
|
|
||||||
|
|
||||||
- Fixed how some constants were represented in the gobject-introspection file,
|
|
||||||
mostly by converting them from defines to enums (#540, #591)
|
|
||||||
|
|
||||||
- Fixed an issue using WirePlumber headers in other projects due to
|
|
||||||
redefinition of G_LOG_DOMAIN (#571)
|
|
||||||
|
|
||||||
WirePlumber 0.4.90
|
|
||||||
..................
|
|
||||||
|
|
||||||
This is the first release candidate (RC1) of WirePlumber 0.5.0.
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- The configuration system has been changed back to load files from the
|
|
||||||
WirePlumber configuration directories, such as ``/etc/wireplumber`` and
|
|
||||||
``$XDG_CONFIG_HOME/wireplumber``, unlike in the pre-releases. This was done
|
|
||||||
because issues were observed with installations that use a different prefix
|
|
||||||
for pipewire and wireplumber. If you had a ``wireplumber.conf`` file in
|
|
||||||
``/etc/pipewire`` or ``$XDG_CONFIG_HOME/pipewire``, you should move it to
|
|
||||||
``/etc/wireplumber`` or ``$XDG_CONFIG_HOME/wireplumber`` respectively (!601)
|
|
||||||
|
|
||||||
- The internal base directories lookup system now also respects the
|
|
||||||
``XDG_CONFIG_DIRS`` and ``XDG_DATA_DIRS`` environment variables, and their
|
|
||||||
default values as per the XDG spec, so it is possible to install
|
|
||||||
configuration files also in places like ``/etc/xdg/wireplumber`` and
|
|
||||||
override system-wide data paths (!601)
|
|
||||||
|
|
||||||
- ``wpctl`` now has a ``settings`` subcommand to show, change and delete
|
|
||||||
settings at runtime. This comes with changes in the ``WpSettings`` system to
|
|
||||||
validate settings using a schema that is defined in the configuration file.
|
|
||||||
The schema is also exported on a metadata object, so it is available to any
|
|
||||||
client that wants to expose WirePlumber settings (!599, !600)
|
|
||||||
|
|
||||||
- The ``WpConf`` API has changed to not be a singleton and support opening
|
|
||||||
arbitrary config files. The main config file now needs to be opened prior to
|
|
||||||
creating a ``WpCore`` and passed to the core using a property. The core uses
|
|
||||||
that without letting the underlying ``pw_context`` open and read the default
|
|
||||||
``client.conf``. The core also closes the ``WpConf`` after all components
|
|
||||||
are loaded, which means all the config loading is done early at startup.
|
|
||||||
Finally, ``WpConf`` loads all sections lazily, keeping the underlying files
|
|
||||||
memory mapped until it is closed and merging them on demand (!601, !606)
|
|
||||||
|
|
||||||
WirePlumber 0.4.82
|
|
||||||
..................
|
|
||||||
|
|
||||||
This is a second pre-release of WirePlumber 0.5.0, made available for testing
|
|
||||||
purposes. This is not API/ABI stable yet and there is still pending work to do
|
|
||||||
before the final 0.5.0 release, both in the codebase and the documentation.
|
|
||||||
|
|
||||||
Highlights:
|
|
||||||
|
|
||||||
- Bluetooth auto-switching is now implemented with a virtual source node. When
|
|
||||||
an application links to it, the actual device switches to the HSP/HFP
|
|
||||||
profile to provide the real audio stream. This is a more robust solution
|
|
||||||
that works with more applications and is more user-friendly than the
|
|
||||||
previous application whitelist approach
|
|
||||||
|
|
||||||
- Added support for dynamic log level changes via the PipeWire ``settings``
|
|
||||||
metadata. Also added support for log level patterns in the configuration
|
|
||||||
file
|
|
||||||
|
|
||||||
- The "persistent" (i.e. stored) settings approach has changed to use two
|
|
||||||
different metadata objects: ``sm-settings`` and ``persistent-sm-settings``.
|
|
||||||
Changes in the former are applied in the current session but not stored,
|
|
||||||
while changes in the latter are stored and restored at startup. Some work
|
|
||||||
was also done to expose a ``wpctl`` interface to read and change these
|
|
||||||
settings, but more is underway
|
|
||||||
|
|
||||||
- Several WirePlumber-specific node properties that used to be called
|
|
||||||
``target.*`` have been renamed to ``node.*`` to match the PipeWire
|
|
||||||
convention of ``node.dont-reconnect``. These are also now fully documented
|
|
||||||
|
|
||||||
Other changes:
|
|
||||||
|
|
||||||
- Many documentation updates
|
|
||||||
|
|
||||||
- Added support for SNAP container permissions
|
|
||||||
|
|
||||||
- Fixed multiple issues related to restoring the Route parameter of devices,
|
|
||||||
which includes volume state (#551)
|
|
||||||
|
|
||||||
- Smart filters can now be targetted by specific streams directly when
|
|
||||||
the ``filter.smart.targetable`` property is set (#554)
|
|
||||||
|
|
||||||
- Ported the mechanism to override device profile priorities in the
|
|
||||||
configuration, which is used to re-prioritize Bluetooth codecs
|
|
||||||
|
|
||||||
- WpSettings is no longer a singleton class and there is a built-in component
|
|
||||||
to preload an instance of it
|
|
||||||
|
|
||||||
WirePlumber 0.4.81
|
WirePlumber 0.4.81
|
||||||
..................
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
This is a preliminary release of WirePlumber 0.5.0, which is made available
|
This is a preliminary release of WirePlumber 0.5.0, which is made available
|
||||||
for testing purposes. Please test it and report feedback (merge requests are
|
for testing purposes. Please test it and report feedback (merge requests are
|
||||||
|
|
@ -704,6 +55,9 @@ Highlights:
|
||||||
the same camera device, which can cause confusion when looking at the list
|
the same camera device, which can cause confusion when looking at the list
|
||||||
of available cameras in applications.
|
of available cameras in applications.
|
||||||
|
|
||||||
|
Past releases
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
WirePlumber 0.4.17
|
WirePlumber 0.4.17
|
||||||
..................
|
..................
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ WirePlumber
|
||||||
.. image:: https://scan.coverity.com/projects/21488/badge.svg
|
.. image:: https://scan.coverity.com/projects/21488/badge.svg
|
||||||
:alt: Coverity Scan Build Status
|
:alt: Coverity Scan Build Status
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/tokei/lines/gitlab.freedesktop.org/pipewire/wireplumber
|
||||||
|
:alt: Lines of code
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/license-MIT-green
|
.. image:: https://img.shields.io/badge/license-MIT-green
|
||||||
:alt: License
|
:alt: License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1240,6 +1240,15 @@ HTML_COLORSTYLE_SAT = 100
|
||||||
|
|
||||||
HTML_COLORSTYLE_GAMMA = 80
|
HTML_COLORSTYLE_GAMMA = 80
|
||||||
|
|
||||||
|
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
|
||||||
|
# page will contain the date and time when the page was generated. Setting this
|
||||||
|
# to YES can help to show when doxygen was last run and thus if the
|
||||||
|
# documentation is up to date.
|
||||||
|
# The default value is: NO.
|
||||||
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
|
HTML_TIMESTAMP = NO
|
||||||
|
|
||||||
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
|
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
|
||||||
# documentation will contain a main index with vertical navigation menus that
|
# documentation will contain a main index with vertical navigation menus that
|
||||||
# are dynamically created via JavaScript. If disabled, the navigation index will
|
# are dynamically created via JavaScript. If disabled, the navigation index will
|
||||||
|
|
@ -1534,6 +1543,17 @@ HTML_FORMULA_FORMAT = png
|
||||||
|
|
||||||
FORMULA_FONTSIZE = 10
|
FORMULA_FONTSIZE = 10
|
||||||
|
|
||||||
|
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
|
||||||
|
# generated for formulas are transparent PNGs. Transparent PNGs are not
|
||||||
|
# supported properly for IE 6.0, but are supported on all modern browsers.
|
||||||
|
#
|
||||||
|
# Note that when changing this option you need to delete any form_*.png files in
|
||||||
|
# the HTML output directory before the changes have effect.
|
||||||
|
# The default value is: YES.
|
||||||
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
|
FORMULA_TRANSPARENT = YES
|
||||||
|
|
||||||
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
|
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
|
||||||
# to create new LaTeX commands to be used in formulas as building blocks. See
|
# to create new LaTeX commands to be used in formulas as building blocks. See
|
||||||
# the section "Including formulas" for details.
|
# the section "Including formulas" for details.
|
||||||
|
|
@ -1845,6 +1865,14 @@ LATEX_HIDE_INDICES = NO
|
||||||
|
|
||||||
LATEX_BIB_STYLE = plain
|
LATEX_BIB_STYLE = plain
|
||||||
|
|
||||||
|
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
|
||||||
|
# page will contain the date and time when the page was generated. Setting this
|
||||||
|
# to NO can help when comparing the output of multiple runs.
|
||||||
|
# The default value is: NO.
|
||||||
|
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||||
|
|
||||||
|
LATEX_TIMESTAMP = NO
|
||||||
|
|
||||||
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
|
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
|
||||||
# path from which the emoji images will be read. If a relative path is entered,
|
# path from which the emoji images will be read. If a relative path is entered,
|
||||||
# it will be relative to the LATEX_OUTPUT directory. If left blank the
|
# it will be relative to the LATEX_OUTPUT directory. If left blank the
|
||||||
|
|
@ -2237,6 +2265,23 @@ HAVE_DOT = YES
|
||||||
|
|
||||||
DOT_NUM_THREADS = 0
|
DOT_NUM_THREADS = 0
|
||||||
|
|
||||||
|
# When you want a differently looking font in the dot files that doxygen
|
||||||
|
# generates you can specify the font name using DOT_FONTNAME. You need to make
|
||||||
|
# sure dot is able to find the font, which can be done by putting it in a
|
||||||
|
# standard location or by setting the DOTFONTPATH environment variable or by
|
||||||
|
# setting DOT_FONTPATH to the directory containing the font.
|
||||||
|
# The default value is: Helvetica.
|
||||||
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
|
DOT_FONTNAME = Helvetica
|
||||||
|
|
||||||
|
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
|
||||||
|
# dot graphs.
|
||||||
|
# Minimum value: 4, maximum value: 24, default value: 10.
|
||||||
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
|
DOT_FONTSIZE = 10
|
||||||
|
|
||||||
# By default doxygen will tell dot to use the default font as specified with
|
# By default doxygen will tell dot to use the default font as specified with
|
||||||
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
|
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
|
||||||
# the path where dot can find it using this tag.
|
# the path where dot can find it using this tag.
|
||||||
|
|
@ -2473,6 +2518,18 @@ DOT_GRAPH_MAX_NODES = 50
|
||||||
|
|
||||||
MAX_DOT_GRAPH_DEPTH = 0
|
MAX_DOT_GRAPH_DEPTH = 0
|
||||||
|
|
||||||
|
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
|
||||||
|
# background. This is disabled by default, because dot on Windows does not seem
|
||||||
|
# to support this out of the box.
|
||||||
|
#
|
||||||
|
# Warning: Depending on the platform used, enabling this option may lead to
|
||||||
|
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
|
||||||
|
# read).
|
||||||
|
# The default value is: NO.
|
||||||
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
|
DOT_TRANSPARENT = NO
|
||||||
|
|
||||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'WirePlumber'
|
project = 'WirePlumber'
|
||||||
copyright = '2020-2025, Collabora & contributors'
|
copyright = '2021-2024, Collabora'
|
||||||
author = 'The WirePlumber Developers'
|
author = 'Collabora'
|
||||||
release = '@VERSION@'
|
release = '@VERSION@'
|
||||||
version = '@VERSION@'
|
version = '@VERSION@'
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
|
||||||
|
|
||||||
smartquotes = False
|
|
||||||
|
|
||||||
# -- Breathe configuration ---------------------------------------------------
|
# -- Breathe configuration ---------------------------------------------------
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
|
|
@ -47,9 +43,3 @@ html_css_files = ['custom.css']
|
||||||
graphviz_output_format = "svg"
|
graphviz_output_format = "svg"
|
||||||
|
|
||||||
pygments_style = "friendly"
|
pygments_style = "friendly"
|
||||||
|
|
||||||
# -- Options for manual page output -----------------------------------------
|
|
||||||
|
|
||||||
man_pages = [
|
|
||||||
('tools/wpctl', 'wpctl', 'WirePlumber Control CLI', ['The WirePlumber Developers'], 1)
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ class DoxygenProcess(object):
|
||||||
def __process_element(self, xml):
|
def __process_element(self, xml):
|
||||||
s = ""
|
s = ""
|
||||||
|
|
||||||
if xml.text and re.search(r'\S', xml.text):
|
if xml.text and re.search('\S', xml.text):
|
||||||
s += xml.text
|
s += xml.text
|
||||||
for n in xml.getchildren():
|
for n in xml.getchildren():
|
||||||
if n.tag == "emphasis":
|
if n.tag == "emphasis":
|
||||||
|
|
@ -143,7 +143,7 @@ class DoxygenProcess(object):
|
||||||
s += " - " + self.__process_element(n)
|
s += " - " + self.__process_element(n)
|
||||||
if n.tag == "para":
|
if n.tag == "para":
|
||||||
p = self.__process_element(n)
|
p = self.__process_element(n)
|
||||||
if re.search(r'\S', p):
|
if re.search('\S', p):
|
||||||
s += p + "\n"
|
s += p + "\n"
|
||||||
if n.tag == "ref":
|
if n.tag == "ref":
|
||||||
s += n.text if n.text else ""
|
s += n.text if n.text else ""
|
||||||
|
|
@ -168,7 +168,7 @@ class DoxygenProcess(object):
|
||||||
if n.tag == "htmlonly":
|
if n.tag == "htmlonly":
|
||||||
s += ""
|
s += ""
|
||||||
if n.tail:
|
if n.tail:
|
||||||
if re.search(r'\S', n.tail):
|
if re.search('\S', n.tail):
|
||||||
s += n.tail
|
s += n.tail
|
||||||
if n.tag.startswith("param"):
|
if n.tag.startswith("param"):
|
||||||
pass # parameters are handled separately in DoxyFunction::from_memberdef()
|
pass # parameters are handled separately in DoxyFunction::from_memberdef()
|
||||||
|
|
@ -319,8 +319,6 @@ class DoxyFunction(DoxyElement):
|
||||||
d = normalize_text(d)
|
d = normalize_text(d)
|
||||||
|
|
||||||
e = DoxyFunction(name, d)
|
e = DoxyFunction(name, d)
|
||||||
if (xml.get("prot") == "private"):
|
|
||||||
e.extra = "(skip)"
|
|
||||||
e.add_brief(xml.find("briefdescription"))
|
e.add_brief(xml.find("briefdescription"))
|
||||||
e.add_detail(xml.find("detaileddescription"))
|
e.add_detail(xml.find("detaileddescription"))
|
||||||
for p in xml.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
|
for p in xml.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
|
||||||
|
|
|
||||||
|
|
@ -102,26 +102,6 @@ if build_doc
|
||||||
install_dir: wireplumber_doc_dir,
|
install_dir: wireplumber_doc_dir,
|
||||||
build_by_default: true,
|
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
|
endif
|
||||||
|
|
||||||
# Build GObject introspection
|
# Build GObject introspection
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@ the various options available.
|
||||||
|
|
||||||
configuration/conf_file.rst
|
configuration/conf_file.rst
|
||||||
configuration/components_and_profiles.rst
|
configuration/components_and_profiles.rst
|
||||||
configuration/configuration_option_types.rst
|
|
||||||
configuration/modifying_configuration.rst
|
|
||||||
configuration/migration.rst
|
|
||||||
configuration/features.rst
|
configuration/features.rst
|
||||||
configuration/settings.rst
|
configuration/settings.rst
|
||||||
|
configuration/locations.rst
|
||||||
|
configuration/main.rst
|
||||||
|
configuration/multi_instance.rst
|
||||||
configuration/alsa.rst
|
configuration/alsa.rst
|
||||||
configuration/bluetooth.rst
|
configuration/bluetooth.rst
|
||||||
|
configuration/policy.rst
|
||||||
configuration/access.rst
|
configuration/access.rst
|
||||||
|
|
|
||||||
|
|
@ -3,123 +3,56 @@
|
||||||
Access configuration
|
Access configuration
|
||||||
====================
|
====================
|
||||||
|
|
||||||
WirePlumber includes a "client access" policy which defines access control
|
wireplumber.conf.d/access.conf
|
||||||
rules for PipeWire clients.
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Rules
|
Using a similar format as the :ref:`ALSA monitor <config_alsa>`, this
|
||||||
-----
|
configuration file is charged to configure the client objects created by
|
||||||
|
PipeWire.
|
||||||
|
|
||||||
This policy can be configured with rules that can be used to match clients and
|
* *Settings*
|
||||||
apply default permissions to them.
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
access.rules = [
|
wireplumber.settings = {
|
||||||
{
|
access-enable-flatpak-portal = true
|
||||||
matches = [
|
}
|
||||||
{
|
|
||||||
access = "flatpak"
|
|
||||||
media.category = "Manager"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
access = "flatpak-manager"
|
|
||||||
default_permissions = "all",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
access = "flatpak"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
default_permissions = "rx"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Possible permissions are any combination of:
|
The above example sets to ``true`` the ``access-enable-flatpak-portal``
|
||||||
|
property.
|
||||||
|
|
||||||
* ``r``: client is allowed to **read** objects, i.e. "see" them on the registry
|
The list of valid properties are:
|
||||||
and list their properties
|
|
||||||
* ``w``: client is allowed to **write** objects, i.e. call methods that modify
|
|
||||||
their state
|
|
||||||
* ``x``: client is allowed to **execute** methods on objects; the ``w`` flag
|
|
||||||
must also be present to call methods that modify the object
|
|
||||||
* ``m``: client is allowed to set **metadata** on objects
|
|
||||||
* ``l``: nodes of this client are allowed to **link** to other nodes that the
|
|
||||||
client can't "see" (i.e. the client doesn't have ``r`` permission on them)
|
|
||||||
|
|
||||||
The special value ``all`` is also supported and it is synonym for ``rwxm``
|
.. code-block::
|
||||||
|
|
||||||
Permission Managers
|
access-enable-flatpak-portal = true,
|
||||||
-------------------
|
|
||||||
|
|
||||||
For more advanced use cases, WirePlumber supports *permission managers* that can
|
Whether to enable the flatpak portal or not.
|
||||||
apply per-object permissions dynamically based on rules and object interests.
|
|
||||||
Permission managers are defined in the ``access.permission-managers`` section
|
|
||||||
and then referenced by name in ``access.rules``.
|
|
||||||
|
|
||||||
Example:
|
* *rules*
|
||||||
|
|
||||||
.. code-block::
|
Example::
|
||||||
|
|
||||||
access.permission-managers = [
|
access = [
|
||||||
{
|
{
|
||||||
name = "custom"
|
matches = [
|
||||||
default_permissions = "all"
|
{
|
||||||
core_permissions = "rx"
|
pipewire.access = "flatpak"
|
||||||
rules = [
|
}
|
||||||
{
|
]
|
||||||
matches = [
|
actions = {
|
||||||
{
|
update-props = {
|
||||||
media.class = "Audio/Source"
|
default_permissions = "rx"
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
actions = {
|
}
|
||||||
set-permissions = "-"
|
]
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
access.rules = [
|
This grants read and execute permissions to all clients that have the
|
||||||
{
|
``pipewire.access`` property set to ``flatpak``.
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
application.name = "paplay"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
permission_manager_name = "custom"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Each permission manager supports the following properties:
|
Possible permissions are any combination of ``r``, ``w`` and ``x`` for read,
|
||||||
|
write and execute; or ``all`` for all kind of permissions.
|
||||||
|
|
||||||
* ``name``: (required) a unique name used to reference the manager from
|
|
||||||
``access.rules``
|
|
||||||
* ``default_permissions``: the fallback permissions applied to all objects
|
|
||||||
that don't match any rule (applied as ``PW_ID_ANY``)
|
|
||||||
* ``core_permissions``: permissions applied specifically to the PipeWire core
|
|
||||||
object (``PW_ID_CORE``, ID 0). This is useful when you want to allow a
|
|
||||||
client to interact with the core (e.g. enumerate objects, subscribe to
|
|
||||||
events) while restricting access to individual objects. If not set, the
|
|
||||||
``default_permissions`` value is used for the core as well.
|
|
||||||
* ``rules``: a list of match rules with ``set-permissions`` actions that
|
|
||||||
grant specific permissions to objects matching the given constraints
|
|
||||||
|
|
||||||
When both ``default_permissions`` and ``permission_manager_name`` are set in
|
|
||||||
a rule's ``update-props`` action, ``default_permissions`` takes precedence and
|
|
||||||
the permission manager is ignored.
|
|
||||||
|
|
|
||||||
|
|
@ -3,423 +3,384 @@
|
||||||
ALSA configuration
|
ALSA configuration
|
||||||
==================
|
==================
|
||||||
|
|
||||||
One of the components of WirePlumber is the ALSA monitor. This monitor is
|
Modifying the default configuration
|
||||||
responsible for creating PipeWire devices and nodes for all the ALSA cards that
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
are available on the system. It also manages the configuration of these devices.
|
|
||||||
|
|
||||||
The ALSA monitor is enabled by default and can be disabled using the
|
ALSA devices are created and managed by the session manager with the *alsa.lua*
|
||||||
``monitor.alsa`` :ref:`feature <config_features>` in the configuration file.
|
monitor script. In the default configuration, this script is loaded by
|
||||||
|
``wireplumber.conf.d/alsa.conf``, which also specifies its settings and
|
||||||
|
rules.
|
||||||
|
|
||||||
The monitor, as with all device monitors, is implemented as a SPA plugin and is
|
* *Settings*
|
||||||
part of PipeWire. WirePlumber merely loads the plugin and lets it do its work.
|
|
||||||
The plugin then monitors UDev and creates device and node objects for all the
|
|
||||||
ALSA cards that are available on the system.
|
|
||||||
|
|
||||||
.. note::
|
Example:
|
||||||
|
|
||||||
One thing worth remembering here is that in ALSA, a "card" represents a
|
.. code-block::
|
||||||
physical sound controller device, and a "device" is a logical access point
|
|
||||||
that represents a set of inputs and/or outputs that are part of the card. In
|
|
||||||
PipeWire, a "device" is the direct equivalent of an ALSA "card" and a "node"
|
|
||||||
is almost equivalent (close, but not quite) of an ALSA "device".
|
|
||||||
|
|
||||||
Properties
|
wireplumber.settings = {
|
||||||
----------
|
alsa_monitor.alsa.jack-device = true
|
||||||
|
alsa_monitor.alsa.reserve = true
|
||||||
|
}
|
||||||
|
|
||||||
The ALSA monitor SPA plugin (``api.alsa.enum.udev``) supports properties that
|
The above example will configure the ALSA monitor to not enable the JACK
|
||||||
can be used to configure it when it is loaded. These properties can be set in
|
device, and do ALSA device reservation using the mentioned DBus interface.
|
||||||
the ``monitor.alsa.properties`` section of the WirePlumber configuration file.
|
|
||||||
|
|
||||||
Example:
|
A list of valid settings are:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
monitor.alsa.properties = {
|
alsa_monitor.alsa.jack-device = true
|
||||||
alsa.use-acp = true
|
|
||||||
}
|
|
||||||
|
|
||||||
.. describe:: alsa.use-acp
|
Creates a JACK device if set to ``true``. This is not enabled by default
|
||||||
|
because it requires that the PipeWire JACK replacement libraries are not used
|
||||||
|
by the session manager, in order to be able to connect to the real JACK
|
||||||
|
server.
|
||||||
|
|
||||||
A boolean that controls whether the ACP (alsa card profile) code is to be
|
.. code-block::
|
||||||
the default manager of the device. This will probe the device and configure
|
|
||||||
the available profiles, ports and mixer settings. The code to do this is
|
|
||||||
taken directly from PulseAudio and provides devices that look and feel
|
|
||||||
exactly like the PulseAudio devices.
|
|
||||||
|
|
||||||
Rules
|
alsa_monitor.alsa.reserve = true
|
||||||
-----
|
|
||||||
|
|
||||||
When device and node objects are created by the ALSA monitor, they can be
|
Reserve ALSA devices via *org.freedesktop.ReserveDevice1* on D-Bus.
|
||||||
configured using rules. These rules allow matching the existing properties of
|
|
||||||
these objects and updating them with new values. This is the main way of
|
|
||||||
configuring ALSA device settings.
|
|
||||||
|
|
||||||
These rules can be set in the ``monitor.alsa.rules`` section of the WirePlumber
|
.. code-block::
|
||||||
configuration file.
|
|
||||||
|
|
||||||
Example:
|
alsa_monitor.alsa.reserve-priority = -20
|
||||||
|
|
||||||
.. code-block::
|
The used ALSA device reservation priority.
|
||||||
|
|
||||||
monitor.alsa.rules = [
|
.. code-block::
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
# This matches the value of the 'device.name' property of the device.
|
|
||||||
device.name = "~alsa_card.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
# Apply all the desired device settings here.
|
|
||||||
api.alsa.use-acp = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
# This matches the value of the 'node.name' property of the node.
|
|
||||||
{
|
|
||||||
node.name = "~alsa_output.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
# Apply all the desired node specific settings here.
|
|
||||||
update-props = {
|
|
||||||
node.nick = "My Node"
|
|
||||||
priority.driver = 100
|
|
||||||
session.suspend-timeout-seconds = 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Device properties
|
alsa_monitor.alsa.reserve-application-name = WirePlumber
|
||||||
|
|
||||||
|
The used ALSA device reservation application name.
|
||||||
|
|
||||||
|
|
||||||
|
* *rules*
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
alsa_monitor = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
# This matches the needed sound card.
|
||||||
|
device.name = "<sound_card_name>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
# Apply all the desired device settings here.
|
||||||
|
api.alsa.use-acp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
# This matches the needed node.
|
||||||
|
{
|
||||||
|
node.name = "<node_name>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
# Apply all the desired node specific settings here.
|
||||||
|
update-props = {
|
||||||
|
node.nick = "My Node"
|
||||||
|
priority.driver = 100
|
||||||
|
session.suspend-timeout-seconds = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Device settings
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
The following properties can be configured on devices created by the monitor:
|
All the possible settings that you can apply to devices and nodes of the
|
||||||
|
ALSA monitor are described here.
|
||||||
|
|
||||||
.. describe:: api.alsa.use-acp
|
PipeWire devices correspond to the ALSA cards.
|
||||||
|
The following settings can be configured on devices created by the monitor:
|
||||||
|
|
||||||
Use the ACP (alsa card profile) code to manage this device. This will probe
|
.. code-block::
|
||||||
the device and configure the available profiles, ports and mixer settings.
|
|
||||||
The code to do this is taken directly from PulseAudio and provides devices
|
|
||||||
that look and feel exactly like the PulseAudio devices.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
api.alsa.use-acp = true
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: api.alsa.use-ucm
|
Use the ACP (alsa card profile) code to manage the device. This will probe the
|
||||||
|
device and configure the available profiles, ports and mixer settings. The
|
||||||
|
code to do this is taken directly from PulseAudio and provides devices that
|
||||||
|
look and feel exactly like the PulseAudio devices.
|
||||||
|
|
||||||
When ACP is enabled and a UCM configuration is available for a device, by
|
.. code-block::
|
||||||
default it is used instead of the ACP profiles. This option allows you to
|
|
||||||
disable this and use the ACP profiles instead.
|
|
||||||
|
|
||||||
This option does nothing if ``api.alsa.use-acp`` is set to ``false``.
|
api.alsa.use-ucm = true
|
||||||
|
|
||||||
:Default value: ``true``
|
By default, the UCM configuration is used when it is available for your device.
|
||||||
:Type: boolean
|
With this option you can disable this and use the ACP profiles instead.
|
||||||
|
|
||||||
.. describe:: api.alsa.soft-mixer
|
.. code-block::
|
||||||
|
|
||||||
Setting this option to ``true`` will disable the hardware mixer for volume
|
api.alsa.soft-mixer = false
|
||||||
control and mute. All volume handling will then use software volume and mute,
|
|
||||||
leaving the hardware mixer untouched. The hardware mixer will still be used
|
|
||||||
to mute unused audio paths in the device.
|
|
||||||
|
|
||||||
:Type: boolean
|
Setting this option to true will disable the hardware mixer for volume control
|
||||||
|
and mute. All volume handling will then use software volume and mute, leaving
|
||||||
|
the hardware mixer untouched. The hardware mixer will still be used to mute
|
||||||
|
unused audio paths in the device.
|
||||||
|
|
||||||
.. describe:: api.alsa.ignore-dB
|
.. code-block::
|
||||||
|
|
||||||
Setting this option to ``true`` will ignore the decibel setting configured by
|
api.alsa.ignore-dB = false
|
||||||
the driver. Use this when the driver reports wrong settings.
|
|
||||||
|
|
||||||
:Type: boolean
|
Setting this option to true will ignore the decibel setting configured by the
|
||||||
|
driver. Use this when the driver reports wrong settings.
|
||||||
|
|
||||||
.. describe:: device.profile-set
|
.. code-block::
|
||||||
|
|
||||||
This option can be used to select a custom ACP profile-set name for the
|
device.profile-set = "profileset-name"
|
||||||
device. This can be configured in UDev rules, but it can also be specified
|
|
||||||
here. The default is to use "default.conf".
|
|
||||||
|
|
||||||
:Type: string
|
This option can be used to select a custom profile set name for the device.
|
||||||
|
Usually this is configured in Udev rules but it can also be specified here.
|
||||||
|
|
||||||
.. describe:: device.profile
|
.. code-block::
|
||||||
|
|
||||||
The initial active profile name. The default is to start from the "Off"
|
device.profile = "default profile name"
|
||||||
profile and then let WirePlumber select the best profile based on its
|
|
||||||
policy.
|
|
||||||
|
|
||||||
:Type: string
|
The default active profile name.
|
||||||
|
|
||||||
.. describe:: api.acp.auto-profile
|
.. code-block::
|
||||||
|
|
||||||
Automatically select the best profile for the device. Normally this option is
|
api.acp.auto-profile = false
|
||||||
disabled because WirePlumber will manage the profile of the device.
|
|
||||||
WirePlumber can save and load previously selected profiles. Enable this in
|
|
||||||
custom configurations where the relevant WirePlumber components are disabled.
|
|
||||||
|
|
||||||
:Type: boolean
|
Automatically select the best profile for the device. Normally this option is
|
||||||
|
disabled because the session manager will manage the profile of the device.
|
||||||
|
The session manager can save and load previously selected profiles. Enable
|
||||||
|
this if your session manager does not handle this feature.
|
||||||
|
|
||||||
.. describe:: api.acp.auto-port
|
.. code-block::
|
||||||
|
|
||||||
Automatically select the highest priority port that is available ("port" is a
|
api.acp.auto-port = false
|
||||||
PulseAudio/ACP term, the equivalent of a "Route" in PipeWire). This is by
|
|
||||||
default disabled because WirePlumber handles the task of selecting and
|
|
||||||
restoring Routes. Enable this in custom configurations where the relevant
|
|
||||||
WirePlumber components are disabled.
|
|
||||||
|
|
||||||
:Type: boolean
|
Automatically select the highest priority port that is available. This is by
|
||||||
|
default disabled because the session manager handles the task of selecting and
|
||||||
|
restoring ports. It can, for example, restore previously saved volumes. Enable
|
||||||
|
this here when the session manager does not handle port restore.
|
||||||
|
|
||||||
.. describe:: api.acp.probe-rate
|
.. code-block:: lua
|
||||||
|
|
||||||
Sets the samplerate used for probing the ALSA devices and collecting the
|
["api.acp.probe-rate"] = 48000
|
||||||
profiles and ports.
|
|
||||||
|
|
||||||
:Type: integer
|
Sets the samplerate used for probing the ALSA devices and collecting the profiles
|
||||||
|
and ports.
|
||||||
|
|
||||||
.. describe:: api.acp.pro-channels
|
.. code-block:: lua
|
||||||
|
|
||||||
Sets the number of channels to use when probing the "Pro Audio" profile.
|
["api.acp.pro-channels"] = 64
|
||||||
Normally, the maximum amount of channels will be used but with this setting
|
|
||||||
this can be reduced, which can make it possible to use other samplerates on
|
|
||||||
some devices.
|
|
||||||
|
|
||||||
:Type: integer
|
Sets the number of channels to use when probing the Pro Audio profile. Normally,
|
||||||
|
the maximum amount of channels will be used but with this setting this can be
|
||||||
|
reduced, which can make it possible to use other samplerates on some devices.
|
||||||
|
|
||||||
Some of the other properties that can be configured on devices:
|
Some of the other settings that might be configured on devices:
|
||||||
|
|
||||||
.. describe:: device.nick
|
.. code-block::
|
||||||
|
|
||||||
A short name for the device.
|
device.nick = "My Device",
|
||||||
|
device.description = "My Device"
|
||||||
|
|
||||||
.. describe:: device.description
|
``device.description`` will show up in most apps when a device name is shown.
|
||||||
|
|
||||||
A longer, user-friendly name of the device. This will show up in most
|
Node Settings
|
||||||
user interfaces as the device's name.
|
|
||||||
|
|
||||||
.. describe:: device.disabled
|
|
||||||
|
|
||||||
Disables the device. PipeWire will remove it from the list of cards or
|
|
||||||
devices.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
Node properties
|
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
The following properties can be configured on nodes created by the monitor:
|
Nodes are sinks or sources in the PipeWire graph. They correspond to the ALSA
|
||||||
|
devices. In addition to the generic stream node configuration options, there are
|
||||||
|
some alsa specific options as well:
|
||||||
|
|
||||||
.. describe:: priority.driver
|
.. code-block::
|
||||||
|
|
||||||
This configures the node driver priority. Nodes with higher priority will be
|
priority.driver = 2000
|
||||||
used as a driver in the graph. Other nodes with lower priority will have to
|
|
||||||
resample to the driver node when they are joined in the same graph. The
|
|
||||||
default value is set based on some heuristics.
|
|
||||||
|
|
||||||
:Type: integer
|
This configures the node driver priority. Nodes with higher priority will be
|
||||||
|
used as a driver in the graph. Other nodes with lower priority will have to
|
||||||
|
resample to the driver node when they are joined in the same graph. The default
|
||||||
|
value is set based on some heuristics.
|
||||||
|
|
||||||
.. describe:: priority.session
|
.. code-block::
|
||||||
|
|
||||||
This configures the priority of the node when selecting a default node
|
priority.session = 1200
|
||||||
(default sink/source as a link target for streams). Higher priority nodes
|
|
||||||
will be more likely candidates for becoming the default node.
|
|
||||||
|
|
||||||
:Type: integer
|
This configures the priority of the node when selecting a default node.
|
||||||
|
Higher priority nodes will be more likely candidates as a default node.
|
||||||
.. note::
|
|
||||||
|
|
||||||
By default, sources have a ``priority.session`` value around 1600-2000 and
|
|
||||||
sinks have a value around 600-1000. If you are increasing the priority of
|
|
||||||
a sink, it is **not advised** to use a value higher than 1500, as it may
|
|
||||||
cause a sink's monitor to be selected as the default source.
|
|
||||||
|
|
||||||
.. describe:: node.pause-on-idle
|
|
||||||
|
|
||||||
Pause the node when nothing is linked to it anymore. This is by default false
|
|
||||||
because some devices make a "pop" sound when they are opened/closed.
|
|
||||||
The node will normally pause and suspend after a timeout (see below).
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: session.suspend-timeout-seconds
|
|
||||||
|
|
||||||
This option configures a different suspend timeout on the node. By default
|
|
||||||
this is ``5`` seconds. For some devices (HiFi amplifiers, for example) it
|
|
||||||
might make sense to set a higher timeout because they might require some time
|
|
||||||
to restart after being idle.
|
|
||||||
|
|
||||||
A value of ``0`` disables suspend for a node and will leave the ALSA device
|
|
||||||
busy. The device can then be manually suspended with
|
|
||||||
``pactl suspend-sink|source``.
|
|
||||||
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
.. describe:: audio.format
|
|
||||||
|
|
||||||
The sample format of the device. By default, PipeWire will use a 32 bits
|
|
||||||
sample format but a different format can be set here.
|
|
||||||
|
|
||||||
:Type: string (``"S16LE"``, ``"S32LE"``, ``"F32LE"``, ...)
|
|
||||||
|
|
||||||
.. describe:: audio.rate
|
|
||||||
|
|
||||||
The sample rate of the device. By default, the ALSA device will be configured
|
|
||||||
with the same samplerate as the global graph. If this is not supported, or a
|
|
||||||
custom value is set here, resampling will be used to match the graph rate.
|
|
||||||
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
.. describe:: audio.channels
|
|
||||||
|
|
||||||
The number of channels of the device. By default the channels and their
|
|
||||||
position are determined by the selected device profile. You can override
|
|
||||||
this setting here.
|
|
||||||
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
.. describe:: audio.position
|
|
||||||
|
|
||||||
The position of the channels. By default the number of channels and their
|
|
||||||
position are determined by the selected device profile. You can override
|
|
||||||
this setting here and optionally swap or reconfigure the channel positions.
|
|
||||||
|
|
||||||
:Type: array of strings (example: ``["FL", "FR", "LFE", "FC", "RL", "RR"]``)
|
|
||||||
|
|
||||||
.. describe:: api.alsa.use-chmap
|
|
||||||
|
|
||||||
Use the channel map as reported by the driver. This is disabled by default
|
|
||||||
because it is often wrong and the ACP code handles this better.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: api.alsa.disable-mmap
|
|
||||||
|
|
||||||
Disable the use of mmap for the ALSA device. By default, PipeWire will access
|
|
||||||
the memory of the device using mmap. This can be disabled and force the usage
|
|
||||||
of the slower read and write access modes, in case the mmap support of the
|
|
||||||
device is not working properly.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: channelmix.normalize
|
|
||||||
|
|
||||||
Normalize the channel volumes when mixing & resampling, making sure that the
|
|
||||||
original 0 dB level is preserved so that nothing sounds wildly
|
|
||||||
quieter/louder. This is disabled by default.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: channelmix.mix-lfe
|
|
||||||
|
|
||||||
Creates a "center" channel for X.0 recordings from the front stereo on X.1
|
|
||||||
setups and pushes some low-frequency/bass from the "center" of X.1 recordings
|
|
||||||
into the front stereo on X.0 setups. This is disabled by default.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: monitor.channel-volumes
|
|
||||||
|
|
||||||
By default, the volume of the sink/source does not influence the volume on
|
|
||||||
the monitor ports. Set this option to true to change this. PulseAudio has
|
|
||||||
inconsistent behaviour regarding this option, it applies channel-volumes only
|
|
||||||
when the sink/source is using software volumes.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: node.disabled
|
|
||||||
|
|
||||||
Disables the node. Pipewire will remove it from the list of the nodes.
|
|
||||||
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
ALSA buffer properties
|
|
||||||
......................
|
|
||||||
|
|
||||||
PipeWire by default uses a timer to consume and produce samples to/from ALSA
|
|
||||||
devices. After every timeout, it queries the hardware pointers of the device and
|
|
||||||
uses this information to set a new timeout. This works well for most devices,
|
|
||||||
but there is a class of devices, so called "batch" devices, that need extra
|
|
||||||
buffering and timing tweaks to work properly. This is because batch devices only
|
|
||||||
get their hardware pointers updated after each hardware interrupt. When the
|
|
||||||
hardware interrupt frequency and the timer frequency are aligned, it is possible
|
|
||||||
for the hardware pointers to be updated just after the timer has expired,
|
|
||||||
resulting in sometimes wrong timing information being returned by the query. In
|
|
||||||
contrast, non-batch devices get pointer updates independent of the interrupt.
|
|
||||||
|
|
||||||
This means that for batch devices we need to set the interrupt at a sufficiently
|
|
||||||
high frequency, at the cost of CPU usage, while for non-batch devices we want to
|
|
||||||
set the interrupt frequency as low as possible to save CPU. For batch devices
|
|
||||||
we also need to take the extra buffering into account caused by the delayed
|
|
||||||
updates of the hardware pointers.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Most USB devices are batch devices and will be handled as such by PipeWire by
|
By default, sources have a ``priority.session`` value around 1600-2000 and
|
||||||
default.
|
sinks have a value around 600-1000. If you are increasing the priority of a
|
||||||
|
sink, it is **not advised** to use a value higher than 1500, as it may cause
|
||||||
|
a sink's monitor to be selected as a default source.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
node.pause-on-idle = false
|
||||||
|
|
||||||
|
Pause-on-idle will stop the node when nothing is linked to it anymore.
|
||||||
|
This is by default false because some devices cause a pop when they are
|
||||||
|
opened/closed. The node will, normally, pause and suspend after a timeout
|
||||||
|
(see suspend-node.lua).
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
session.suspend-timeout-seconds = 5 -- 0 disables suspend
|
||||||
|
|
||||||
|
This option configures a different suspend timeout on the node.
|
||||||
|
By default this is 5 seconds. For some devices (HiFi amplifiers, for example)
|
||||||
|
it might make sense to set a higher timeout because they might require some
|
||||||
|
time to restart after being idle.
|
||||||
|
|
||||||
|
A value of 0 disables suspend for a node and will leave the ALSA device busy.
|
||||||
|
The device can then manually be suspended with ``pactl suspend-sink|source``.
|
||||||
|
|
||||||
|
**The following properties can be used to configure the format used by the
|
||||||
|
ALSA device:**
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
audio.format = "S16LE"
|
||||||
|
|
||||||
|
By default, PipeWire will use a 32 bits sample format but a different format
|
||||||
|
can be set here.
|
||||||
|
|
||||||
|
The Audio rate of a device can be set here:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
audio.rate = 44100
|
||||||
|
|
||||||
|
By default, the ALSA device will be configured with the same samplerate as the
|
||||||
|
global graph. If this is not supported, or a custom values is set here,
|
||||||
|
resampling will be used to match the graph rate.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
audio.channels = 2
|
||||||
|
audio.position = "FL,FR"
|
||||||
|
|
||||||
|
By default the channels and their position are determined by the selected
|
||||||
|
Device profile. You can override this setting here and optionally swap or
|
||||||
|
reconfigure the channel positions.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
api.alsa.use-chmap = false
|
||||||
|
|
||||||
|
Use the channel map as reported by the driver. This is disabled by default
|
||||||
|
because it is often wrong and the ACP code handles this better.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
api.alsa.disable-mmap = true
|
||||||
|
|
||||||
|
PipeWire will by default access the memory of the device using mmap.
|
||||||
|
This can be disabled and force the usage of the slower read and write access
|
||||||
|
modes in case the mmap support of the device is not working properly.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
channelmix.normalize = true
|
||||||
|
|
||||||
|
Makes sure that during such mixing & resampling original 0 dB level is
|
||||||
|
preserved, so nothing sounds wildly quieter/louder.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
channelmix.mix-lfe = true
|
||||||
|
|
||||||
|
Creates "center" channel for X.0 recordings from front stereo on X.1 setups and
|
||||||
|
pushes some low-frequency/bass from "center" from X.1 recordings into front
|
||||||
|
stereo on X.0 setups.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
monitor.channel-volumes = false
|
||||||
|
|
||||||
|
By default, the volume of the sink/source does not influence the volume on the
|
||||||
|
monitor ports. Set this option to true to change this. PulseAudio has
|
||||||
|
inconsistent behaviour regarding this option, it applies channel-volumes only
|
||||||
|
when the sink/source is using software volumes.
|
||||||
|
|
||||||
|
ALSA buffer properties
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
PipeWire uses a timer to consume and produce samples to/from ALSA devices.
|
||||||
|
After every timeout, it queries the device hardware pointers of the device and
|
||||||
|
uses this information to set a new timeout. See also this example program.
|
||||||
|
|
||||||
|
By default, PipeWire handles ALSA batch devices differently from non-batch
|
||||||
|
devices. Batch devices only get their hardware pointers updated after each
|
||||||
|
hardware interrupt. Non-batch devices get updates independent of the interrupt.
|
||||||
|
This means that for batch devices we need to set the interrupt at a sufficiently
|
||||||
|
high frequency (at the cost of CPU usage) while for non-batch devices we want to
|
||||||
|
set the interrupt frequency as low as possible (to save CPU).
|
||||||
|
|
||||||
|
For batch devices we also need to take the extra buffering into account caused
|
||||||
|
by the delayed updates of the hardware pointers.
|
||||||
|
|
||||||
|
Most USB devices are batch devices and will be handled as such by PipeWire by
|
||||||
|
default.
|
||||||
|
|
||||||
There are 2 tunable parameters to control the buffering and timeouts in a
|
There are 2 tunable parameters to control the buffering and timeouts in a
|
||||||
device:
|
device
|
||||||
|
|
||||||
.. describe:: api.alsa.period-size
|
.. code-block::
|
||||||
|
|
||||||
This sets the device interrupt to every period-size samples for non-batch
|
api.alsa.period-size = 1024
|
||||||
devices and to half of this for batch devices. For batch devices, the other
|
|
||||||
half of the period-size is used as extra buffering to compensate for the
|
|
||||||
delayed update. So, for batch devices, there is an additional period-size/2
|
|
||||||
delay. It makes sense to lower the period-size for batch devices to reduce
|
|
||||||
this delay.
|
|
||||||
|
|
||||||
:Type: integer (samples)
|
This sets the device interrupt to every period-size samples for non-batch
|
||||||
|
devices and to half of this for batch devices. For batch devices, the other
|
||||||
|
half of the period-size is used as extra buffering to compensate for the delayed
|
||||||
|
update. So, for batch devices, there is an additional period-size/2 delay.
|
||||||
|
It makes sense to lower the period-size for batch devices to reduce this delay.
|
||||||
|
|
||||||
.. describe:: api.alsa.headroom
|
.. code-block::
|
||||||
|
|
||||||
This adds extra delay between the hardware pointers and software pointers.
|
api.alsa.headroom = 0
|
||||||
In most cases this can be set to 0. For very bad devices or emulated devices
|
|
||||||
(like in a VM) it might be necessary to increase the headroom value.
|
|
||||||
|
|
||||||
:Type: integer (samples)
|
|
||||||
|
|
||||||
.. describe:: api.alsa.period-num
|
|
||||||
|
|
||||||
This configures the number of periods in the hardware buffer, which controls
|
|
||||||
its size. Note that this is multiplied by the period of the device to
|
|
||||||
determine the size, so for batch devices, the total buffer size is
|
|
||||||
effectively period-num * period-size/2.
|
|
||||||
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
|
This adds extra delay between the hardware pointers and software pointers.
|
||||||
|
In most cases this can be set to 0. For very bad devices or emulated devices
|
||||||
|
(like in a VM) it might be necessary to increase the headroom value.
|
||||||
In summary, this is the overview of buffering and timings:
|
In summary, this is the overview of buffering and timings:
|
||||||
|
|
||||||
============== ============================================ ==========================================
|
|
||||||
Property Batch Non-Batch
|
|
||||||
============== ============================================ ==========================================
|
|
||||||
IRQ Frequency api.alsa.period-size/2 api.alsa.period-size
|
|
||||||
Extra Delay api.alsa.headroom + api.alsa.period-size/2 api.alsa.headroom
|
|
||||||
Buffer Size api.alsa.period-num * api.alsa.period-size/2 api.alsa.period-num * api.alsa.period-size
|
|
||||||
============== ============================================ ==========================================
|
|
||||||
|
|
||||||
Finally, it is possible to disable the batch device tweaks with:
|
============== ========================================== =========
|
||||||
|
Property Batch Non-Batch
|
||||||
|
============== ========================================== =========
|
||||||
|
IRQ Frequency api.alsa.period-size/2 api.alsa.period-size
|
||||||
|
Extra Delay api.alsa.headroom + api.alsa.period-size/2 api.alsa.headroom
|
||||||
|
============== ========================================== =========
|
||||||
|
|
||||||
.. describe:: api.alsa.disable-batch
|
It is possible to disable the batch device tweaks with:
|
||||||
|
|
||||||
This disables the batch device tweaks. It removes the extra delay added of
|
.. code-block::
|
||||||
period-size/2 if the device can support this. For batch devices it is also a
|
|
||||||
good idea to lower the period-size (and increase the IRQ frequency) to get
|
|
||||||
smaller batch updates and lower latency.
|
|
||||||
|
|
||||||
:Type: boolean
|
api.alsa.disable-batch"] = true
|
||||||
|
|
||||||
|
It removes the extra delay added of period-size/2 if the device can support this.
|
||||||
|
For batch devices it is also a good idea to lower the period-size
|
||||||
|
(and increase the IRQ frequency) to get smaller batch updates and lower latency.
|
||||||
|
|
||||||
ALSA extra latency properties
|
ALSA extra latency properties
|
||||||
.............................
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Extra internal delay in the DAC and ADC converters of the device itself can be
|
Extra internal delay in the DAC and ADC converters of the device itself can be
|
||||||
set with the ``latency.internal.*`` properties:
|
set with the ``latency.internal.*`` properties:
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
latency.internal.rate = 256
|
latency.internal.rate"] = 256
|
||||||
latency.internal.ns = 0
|
latency.internal.ns"] = 0
|
||||||
|
|
||||||
You can configure a latency in samples (relative to rate with
|
You can configure a latency in samples (relative to rate with
|
||||||
``latency.internal.rate``) or in nanoseconds (``latency.internal.ns``).
|
``latency.internal.rate``) or in nanoseconds (``latency.internal.ns``).
|
||||||
|
|
@ -458,28 +419,44 @@ Set the internal latency to 256 samples:
|
||||||
remote 0 port 76 changed
|
remote 0 port 76 changed
|
||||||
|
|
||||||
Startup tweaks
|
Startup tweaks
|
||||||
..............
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. describe:: api.alsa.start-delay
|
Some devices need some time before they can report accurate hardware pointer
|
||||||
|
positions. In those cases, an extra start delay can be added that is used to
|
||||||
|
compensate for this startup delay:
|
||||||
|
|
||||||
Some devices need some time before they can report accurate hardware pointer
|
.. code-block::
|
||||||
positions. In those cases, an extra start delay can be added to compensate
|
|
||||||
for this startup delay. This sets the startup delay in samples. The default
|
|
||||||
is 0.
|
|
||||||
|
|
||||||
:Type: integer (samples)
|
["api.alsa.start-delay"] = 0
|
||||||
|
|
||||||
|
It is unsure when this tunable should be used.
|
||||||
|
|
||||||
IEC958 (S/PDIF) passthrough
|
IEC958 (S/PDIF) passthrough
|
||||||
...........................
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. describe:: iec958.codecs
|
S/PDIF passthrough will only be enabled when the accepted codecs are configured
|
||||||
|
on the ALSA device.
|
||||||
|
|
||||||
S/PDIF passthrough will only be enabled when the accepted codecs are configured
|
This can be done in 3 different ways:
|
||||||
on the ALSA device. This can be done by setting the list of supported codecs
|
|
||||||
on this property.
|
|
||||||
|
|
||||||
Note that it is possible to also configure this property at runtime, either
|
1. Use pavucontrol and toggle the codecs in the output advanced section.
|
||||||
with tools like pavucontrol or with the ``pw-cli`` tool, like this:
|
|
||||||
``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'``
|
|
||||||
|
|
||||||
:Type: array of strings (example: ``[ "PCM", "DTS", "AC3", "EAC3", "TrueHD", "DTS-HD" ]``)
|
2. Modify the ``["iec958.codecs"]`` node property to contain supported codecs.
|
||||||
|
|
||||||
|
Example ``~/.config/wireplumber/main.lua.d/51-alsa-spdif.lua``:
|
||||||
|
|
||||||
|
.. code-block:: lua
|
||||||
|
|
||||||
|
table.insert (alsa_monitor.rules, {
|
||||||
|
matches = {
|
||||||
|
{
|
||||||
|
{ "node.name", "matches", "alsa_output.*" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apply_properties = {
|
||||||
|
["iec958.codecs"] = "[ PCM DTS AC3 EAC3 TrueHD DTS-HD ]",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify
|
||||||
|
the codecs at runtime.
|
||||||
|
|
|
||||||
|
|
@ -3,438 +3,174 @@
|
||||||
Bluetooth configuration
|
Bluetooth configuration
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
Bluetooth audio and MIDI devices are managed by the BlueZ and BlueZ-MIDI
|
Using the same format as the :ref:`ALSA monitor <config_alsa>`, the
|
||||||
monitors, respectively.
|
configuration file ``wireplumber.conf.d/bluetooth.conf`` is charged
|
||||||
|
to configure the Bluetooth devices and nodes created by WirePlumber.
|
||||||
|
|
||||||
Both monitors are enabled by default and can be disabled using the
|
* *Settings*
|
||||||
``monitor.bluez`` and ``monitor.bluez-midi`` :ref:`features <config_features>`
|
|
||||||
in the configuration file.
|
|
||||||
|
|
||||||
As with all device monitors, both of these monitors are implemented as SPA
|
Example:
|
||||||
plugins and are part of PipeWire. WirePlumber merely loads the plugins and lets
|
|
||||||
them do their work. These plugins then monitor the BlueZ system-wide D-Bus
|
|
||||||
service and create device and node objects for all the connected Bluetooth audio
|
|
||||||
and MIDI devices.
|
|
||||||
|
|
||||||
Logind integration
|
.. code-block::
|
||||||
------------------
|
|
||||||
|
|
||||||
The BlueZ monitors are integrated with logind to ensure that only one user at a
|
wireplumber.properties = {
|
||||||
time can use the Bluetooth audio devices. This is because on most Linux desktop
|
bluez5.enable-msbc = true,
|
||||||
systems, the graphical login manager (GDM, SDDM, etc.) is running as a separate
|
}
|
||||||
user and runs its own instance of PipeWire and Wireplumber. This means that if a
|
|
||||||
user logs in graphically, the Bluetooth audio devices will be automatically
|
|
||||||
grabbed by the PipeWire/WirePlumber instance of the graphical login manager,
|
|
||||||
and the user that logs in will not get access to them.
|
|
||||||
|
|
||||||
To overcome this, the BlueZ monitors are integrated with logind and are only
|
This example will enable the MSBC codec in connected Bluetooth devices that
|
||||||
allowed to create device and node objects for Bluetooth audio devices if the
|
support it.
|
||||||
user is currently on the "active" logind session.
|
|
||||||
|
|
||||||
In some cases, however, this behavior is not desired. For example, if you
|
The list of all valid properties are:
|
||||||
manually switch to a TTY and log in there, you may want to keep the Bluetooth
|
|
||||||
audio devices connected to the now inactive graphical session. Or you may want
|
|
||||||
to have a dedicated user that is always allowed to use the Bluetooth audio
|
|
||||||
devices, regardless of the active logind session, for example for a (possibly
|
|
||||||
headless) music player daemon.
|
|
||||||
|
|
||||||
To disable this behavior, you can set the ``monitor.bluez.seat-monitoring``
|
.. code-block::
|
||||||
:ref:`feature <config_features>` to ``disabled``.
|
|
||||||
|
|
||||||
Example configuration :ref:`fragment <config_conf_file_fragments>` file:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
monitor.bluez.seat-monitoring = disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If logind is not installed on the system, this functionality is disabled
|
|
||||||
automatically.
|
|
||||||
|
|
||||||
Monitor Properties
|
|
||||||
------------------
|
|
||||||
|
|
||||||
The BlueZ monitor SPA plugin (``api.bluez5.enum.dbus``) supports properties that
|
|
||||||
can be used to configure it when it is loaded. These properties can be set in
|
|
||||||
the ``monitor.bluez.properties`` section of the WirePlumber configuration file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.bluez.properties = {
|
|
||||||
bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
|
|
||||||
bluez5.codecs = [ sbc sbc_xq aac ]
|
|
||||||
bluez5.enable-sbc-xq = true
|
bluez5.enable-sbc-xq = true
|
||||||
|
|
||||||
|
Enables the SBC-XQ codec in connected Blueooth devices that support it
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
bluez5.enable-msbc = true
|
||||||
|
|
||||||
|
Enables the MSBC codec in connected Blueooth devices that support it
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
bluez5.enable-hw-volume = true
|
||||||
|
|
||||||
|
Enables hardware volume controls in Bluetooth devices that support it
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
bluez5.headset-roles = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]"
|
||||||
|
|
||||||
|
Enabled headset roles (default: [ hsp_hs hfp_ag ]), this property only applies
|
||||||
|
to native backend. Currently some headsets (Sony WH-1000XM3) are not working
|
||||||
|
with both hsp_ag and hfp_ag enabled, disable either hsp_ag or hfp_ag to work
|
||||||
|
around it.
|
||||||
|
|
||||||
|
Supported headset roles: ``hsp_hs`` (HSP Headset), ``hsp_ag`` (HSP Audio
|
||||||
|
Gateway), ``hfp_hf`` (HFP Hands-Free) and ``hfp_ag`` (HFP Audio Gateway)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
bluez5.codecs = "[ sbc sbc_xq aac ]"
|
||||||
|
|
||||||
|
Enables ``sbc``, ``sbc_zq`` and ``aac`` A2DP codecs.
|
||||||
|
|
||||||
|
Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
|
||||||
|
``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
|
||||||
|
``faststream_duplex``.
|
||||||
|
|
||||||
|
All codecs are supported by default.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
bluez5.hfphsp-backend = "native"
|
bluez5.hfphsp-backend = "native"
|
||||||
}
|
|
||||||
|
|
||||||
.. describe:: bluez5.roles
|
HFP/HSP backend (default: native). Available values: ``any``, ``none``,
|
||||||
|
``hsphfpd``, ``ofono`` or ``native``.
|
||||||
|
|
||||||
Enabled roles.
|
.. code-block::
|
||||||
|
|
||||||
Currently some headsets (e.g. Sony WH-1000XM3) do not work with both
|
bluez5.default.rate = 48000
|
||||||
``hsp_ag`` and ``hfp_ag`` enabled, so by default we enable only HFP.
|
|
||||||
|
|
||||||
Supported roles:
|
The bluetooth default audio rate.
|
||||||
|
|
||||||
- ``hsp_hs`` (HSP Headset)
|
.. code-block::
|
||||||
- ``hsp_ag`` (HSP Audio Gateway),
|
|
||||||
- ``hfp_hf`` (HFP Hands-Free),
|
|
||||||
- ``hfp_ag`` (HFP Audio Gateway)
|
|
||||||
- ``a2dp_sink`` (A2DP Audio Sink)
|
|
||||||
- ``a2dp_source`` (A2DP Audio Source)
|
|
||||||
- ``bap_sink`` (LE Audio Basic Audio Profile Sink)
|
|
||||||
- ``bap_source`` (LE Audio Basic Audio Profile Source)
|
|
||||||
|
|
||||||
:Default value: ``[ a2dp_sink a2dp_source bap_sink bap_source hfp_hf hfp_ag ]``
|
bluez5.default.channels = 2
|
||||||
:Type: array of strings
|
|
||||||
|
|
||||||
.. describe:: bluez5.codecs
|
The bluetooth default number of channels.
|
||||||
|
|
||||||
Enabled A2DP codecs.
|
* *Rules*
|
||||||
|
|
||||||
Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
|
Example:
|
||||||
``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
|
|
||||||
``faststream_duplex``, ``lc3plus_h3``, ``opus_05``, ``opus_05_51``,
|
|
||||||
``opus_05_71``, ``opus_05_duplex``, ``opus_05_pro``, ``lc3``.
|
|
||||||
|
|
||||||
:Default value: all available codecs
|
.. code-block::
|
||||||
:Type: array of strings
|
|
||||||
|
|
||||||
.. describe:: bluez5.enable-msbc
|
wireplumber.settings = {
|
||||||
|
bluez_monitor = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
# This matches the needed sound card.
|
||||||
|
device.name = "<bluez_sound_card_name>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
# Apply all the desired device settings here.
|
||||||
|
bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
# This matches the needed node.
|
||||||
|
{
|
||||||
|
node.name = "<node_name>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
# Apply all the desired node specific settings here.
|
||||||
|
update-props = {
|
||||||
|
node.nick = "My Node"
|
||||||
|
priority.driver = 100
|
||||||
|
session.suspend-timeout-seconds = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
Enable mSBC codec (wideband speech codec for HFP/HSP).
|
This will set the auto-connect property to ``hfp_hf``, ``hsp_hs`` and
|
||||||
|
``a2dp_sink`` on bluetooth devices whose name matches the ``bluez_card.*``
|
||||||
|
pattern.
|
||||||
|
|
||||||
This does not work on all headsets, so it is enabled based on the hardware
|
A list of valid properties are:
|
||||||
quirks database. By explicitly setting this option you can force it to be
|
|
||||||
enabled or disabled regardless.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
.. code-block::
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: bluez5.enable-sbc-xq
|
bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
|
||||||
|
|
||||||
Enable SBC-XQ codec (high quality SBC codec for A2DP).
|
Auto-connect device profiles on start up or when only partial profiles have
|
||||||
|
connected. Disabled by default if the property is not specified.
|
||||||
|
|
||||||
This does not work on all headsets, so it is enabled based on the hardware
|
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
||||||
quirks database. By explicitly setting this option you can force it to be
|
``hsp_ag`` and ``a2dp_source``.
|
||||||
enabled or disabled regardless.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
.. code-block::
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: bluez5.enable-hw-volume
|
bluez5.hw-volume = "[ hfp_ag hsp_ag a2dp_source ]"
|
||||||
|
|
||||||
Enable hardware volume controls.
|
Hardware volume controls (default: ``hfp_ag``, ``hsp_ag``, and ``a2dp_source``)
|
||||||
|
|
||||||
This does not work on all headsets, so it is enabled based on the hardware
|
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
||||||
quirks database. By explicitly setting this option you can force it to be
|
``hsp_ag`` and ``a2dp_source``.
|
||||||
enabled or disabled regardless.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
.. code-block::
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: bluez5.hfphsp-backend
|
bluez5.a2dp.ldac.quality = "auto"
|
||||||
|
|
||||||
HFP/HSP backend.
|
LDAC encoding quality.
|
||||||
|
|
||||||
Available values: ``any``, ``none``, ``hsphfpd``, ``ofono`` or ``native``.
|
Available values: ``auto`` (Adaptive Bitrate, default),
|
||||||
|
``hq`` (High Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and
|
||||||
|
``mq`` (Mobile use Quality, 330/303kbps).
|
||||||
|
|
||||||
:Default value: ``native``
|
.. code-block::
|
||||||
:Type: string
|
|
||||||
|
|
||||||
.. describe:: bluez5.hfphsp-backend-native-modem
|
bluez5.a2dp.aac.bitratemode = 0
|
||||||
|
|
||||||
Modem to use for native HFP/HSP backend ModemManager support. When enabled,
|
AAC variable bitrate mode.
|
||||||
PipeWire will forward HFP commands to the specified ModemManager device.
|
|
||||||
This corresponds to the 'Device' property of the
|
|
||||||
``org.freedesktop.ModemManager1.Modem`` interface. May also be ``any`` to
|
|
||||||
use any available modem device.
|
|
||||||
|
|
||||||
:Default value: ``none``
|
Available values: 0 (cbr, default), 1-5 (quality level).
|
||||||
:Type: string
|
|
||||||
|
|
||||||
.. describe:: bluez5.hw-offload-sco
|
.. code-block::
|
||||||
|
|
||||||
HFP/HSP hardware offload SCO support.
|
device.profile = "a2dp-sink"
|
||||||
|
|
||||||
Using this feature requires a custom WirePlumber script that handles audio
|
Profile connected first.
|
||||||
routing in a platform-specific way. See ``tests/examples/bt-pinephone.lua``
|
|
||||||
for an example.
|
|
||||||
|
|
||||||
:Default value: ``false``
|
Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: bluez5.default.rate
|
|
||||||
|
|
||||||
The default audio rate for the A2DP codec configuration.
|
|
||||||
|
|
||||||
:Default value: ``48000``
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
.. describe:: bluez5.default.channels
|
|
||||||
|
|
||||||
The default number of channels for the A2DP codec configuration.
|
|
||||||
|
|
||||||
:Default value: ``2``
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
.. describe:: bluez5.dummy-avrcp-player
|
|
||||||
|
|
||||||
Register dummy AVRCP player. Some devices have wrongly functioning volume or
|
|
||||||
playback controls if this is not enabled. Disabled by default.
|
|
||||||
|
|
||||||
:Default value: ``false``
|
|
||||||
:Type: boolean
|
|
||||||
|
|
||||||
.. describe:: Opus Pro Audio mode settings
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
bluez5.a2dp.opus.pro.channels = 3
|
|
||||||
bluez5.a2dp.opus.pro.coupled-streams = 1
|
|
||||||
bluez5.a2dp.opus.pro.locations = [ FL,FR,LFE ]
|
|
||||||
bluez5.a2dp.opus.pro.max-bitrate = 600000
|
|
||||||
bluez5.a2dp.opus.pro.frame-dms = 50
|
|
||||||
bluez5.a2dp.opus.pro.bidi.channels = 1
|
|
||||||
bluez5.a2dp.opus.pro.bidi.coupled-streams = 0
|
|
||||||
bluez5.a2dp.opus.pro.bidi.locations = [ FC ]
|
|
||||||
bluez5.a2dp.opus.pro.bidi.max-bitrate = 160000
|
|
||||||
bluez5.a2dp.opus.pro.bidi.frame-dms = 400
|
|
||||||
|
|
||||||
Options for the PipeWire-specific multichannel Opus codec, which can be used
|
|
||||||
to transport audio over Bluetooth between devices running PipeWire.
|
|
||||||
|
|
||||||
MIDI Monitor Properties
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
The BlueZ MIDI monitor SPA plugin (``api.bluez5.midi.enum``) may, in the future,
|
|
||||||
support properties that can be used to configure it when it is loaded. These
|
|
||||||
properties can be set in the ``monitor.bluez-midi.properties`` section of the
|
|
||||||
WirePlumber configuration file. At the moment of writing, there are no
|
|
||||||
properties that can be set there.
|
|
||||||
|
|
||||||
In addition, the BlueZ MIDI monitor supports a list of MIDI server node names
|
|
||||||
that can be used to create Bluetooth LE MIDI service instances. These
|
|
||||||
server node names can be set in the ``monitor.bluez-midi.servers`` section of
|
|
||||||
the WirePlumber configuration file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.bluez-midi.servers = [ "bluez_midi.server" ]
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Typical BLE MIDI instruments have one service instance, so adding more than
|
|
||||||
one here may confuse some clients.
|
|
||||||
|
|
||||||
Rules
|
|
||||||
-----
|
|
||||||
|
|
||||||
When device and node objects are created by the BlueZ monitor, they can be
|
|
||||||
configured using rules. These rules allow matching the existing properties of
|
|
||||||
these objects and updating them with new values. This is the main way of
|
|
||||||
configuring Bluetooth device settings.
|
|
||||||
|
|
||||||
These rules can be set in the ``monitor.bluez.rules`` section of the WirePlumber
|
|
||||||
configuration file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.bluez.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
## This matches all bluetooth devices.
|
|
||||||
device.name = "~bluez_card.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
|
|
||||||
bluez5.hw-volume = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
|
|
||||||
bluez5.a2dp.ldac.quality = "auto"
|
|
||||||
bluez5.a2dp.aac.bitratemode = 0
|
|
||||||
bluez5.a2dp.opus.pro.application = "audio"
|
|
||||||
bluez5.a2dp.opus.pro.bidi.application = "audio"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
## Matches all sources.
|
|
||||||
node.name = "~bluez_input.*"
|
|
||||||
}
|
|
||||||
{
|
|
||||||
## Matches all sinks.
|
|
||||||
node.name = "~bluez_output.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
bluez5.media-source-role = "input"
|
|
||||||
|
|
||||||
# Common node & audio adapter properties may also be set here
|
|
||||||
node.nick = "My Node"
|
|
||||||
priority.driver = 100
|
|
||||||
priority.session = 100
|
|
||||||
node.pause-on-idle = false
|
|
||||||
resample.quality = 4
|
|
||||||
channelmix.normalize = false
|
|
||||||
channelmix.mix-lfe = false
|
|
||||||
session.suspend-timeout-seconds = 5
|
|
||||||
monitor.channel-volumes = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Device properties
|
|
||||||
^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
The following properties can be set on device objects:
|
|
||||||
|
|
||||||
.. describe:: bluez5.auto-connect
|
|
||||||
|
|
||||||
Auto-connect device profiles on start up or when only partial profiles have
|
|
||||||
connected. Disabled by default if the property is not specified.
|
|
||||||
|
|
||||||
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
|
||||||
``hsp_ag`` and ``a2dp_source``.
|
|
||||||
|
|
||||||
:Default value: ``[]``
|
|
||||||
:Type: array of strings
|
|
||||||
|
|
||||||
.. describe:: bluez5.hw-volume
|
|
||||||
|
|
||||||
Enable hardware volume controls on these profiles.
|
|
||||||
|
|
||||||
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
|
||||||
``hsp_ag`` and ``a2dp_source``.
|
|
||||||
|
|
||||||
:Default value: ``[ hfp_ag hsp_ag a2dp_source ]``
|
|
||||||
:Type: array of strings
|
|
||||||
|
|
||||||
.. describe:: bluez5.a2dp.ldac.quality
|
|
||||||
|
|
||||||
LDAC encoding quality.
|
|
||||||
|
|
||||||
Available values: ``auto`` (Adaptive Bitrate, default), ``hq`` (High
|
|
||||||
Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and ``mq``
|
|
||||||
(Mobile use Quality, 330/303kbps).
|
|
||||||
|
|
||||||
:Default value: ``auto``
|
|
||||||
:Type: string
|
|
||||||
|
|
||||||
.. describe:: bluez5.a2dp.aac.bitratemode
|
|
||||||
|
|
||||||
AAC variable bitrate mode.
|
|
||||||
|
|
||||||
Available values: 0 (cbr, default), 1-5 (quality level).
|
|
||||||
|
|
||||||
:Default value: ``0``
|
|
||||||
:Type: integer
|
|
||||||
|
|
||||||
.. describe:: bluez5.a2dp.opus.pro.application
|
|
||||||
|
|
||||||
Opus Pro Audio encoding mode.
|
|
||||||
|
|
||||||
Available values: ``audio``, ``voip``, ``lowdelay``.
|
|
||||||
|
|
||||||
:Default value: ``audio``
|
|
||||||
:Type: string
|
|
||||||
|
|
||||||
.. describe:: bluez5.a2dp.opus.pro.bidi.application
|
|
||||||
|
|
||||||
Opus Pro Audio encoding mode for bidirectional audio.
|
|
||||||
|
|
||||||
Available values: ``audio``, ``voip``, ``lowdelay``.
|
|
||||||
|
|
||||||
:Default value: ``audio``
|
|
||||||
:Type: string
|
|
||||||
|
|
||||||
.. describe:: device.profile
|
|
||||||
|
|
||||||
The profile that is activated initially when the device is connected.
|
|
||||||
|
|
||||||
Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.
|
|
||||||
|
|
||||||
:Default value: ``a2dp-sink``
|
|
||||||
:Type: string
|
|
||||||
|
|
||||||
Node properties
|
|
||||||
^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
The following properties can be set on node objects:
|
|
||||||
|
|
||||||
.. describe:: bluez5.media-source-role
|
|
||||||
|
|
||||||
Media source role, ``input`` or ``playback``. This controls how a media
|
|
||||||
source device, such as a smartphone, is used by the system. Defaults to
|
|
||||||
``playback``, playing the incoming stream out to speakers. Set to ``input``
|
|
||||||
to use the smartphone as an input for apps (like a microphone).
|
|
||||||
|
|
||||||
:Default value: ``playback``
|
|
||||||
:Type: string
|
|
||||||
|
|
||||||
MIDI Rules
|
|
||||||
----------
|
|
||||||
|
|
||||||
Similarly to the above rules, the BlueZ MIDI monitor also supports rules that
|
|
||||||
can be used to configure MIDI nodes when they are created.
|
|
||||||
|
|
||||||
These rules can be set in the ``monitor.bluez-midi.rules`` section of the
|
|
||||||
WirePlumber configuration file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.bluez-midi.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
node.name = "~bluez_midi.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
node.nick = "My Node"
|
|
||||||
priority.driver = 100
|
|
||||||
priority.session = 100
|
|
||||||
node.pause-on-idle = false
|
|
||||||
session.suspend-timeout-seconds = 5
|
|
||||||
node.latency-offset-msec = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
It is possible to also match MIDI server nodes by testing the ``node.name``
|
|
||||||
property against the server node names that were set in the
|
|
||||||
``monitor.bluez-midi.servers`` section of the WirePlumber configuration file.
|
|
||||||
|
|
||||||
MIDI-specific properties
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. describe:: node.latency-offset-msec
|
|
||||||
|
|
||||||
Latency adjustment to apply on the node. Larger values add a
|
|
||||||
constant latency, but reduces timing jitter caused by Bluetooth
|
|
||||||
transport.
|
|
||||||
|
|
||||||
:Default value: ``0``
|
|
||||||
:Type: integer (milliseconds)
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ The main types of components are:
|
||||||
A PipeWire module, which is also a shared library that can be loaded
|
A PipeWire module, which is also a shared library that can be loaded
|
||||||
dynamically, but extends the functionality of the underlying *libpipewire*
|
dynamically, but extends the functionality of the underlying *libpipewire*
|
||||||
library. Loading PipeWire modules in the WirePlumber context can be useful
|
library. Loading PipeWire modules in the WirePlumber context can be useful
|
||||||
to load custom protocol extensions or to offload some functionality from
|
to load custom protocol extensions or to offload some funcitonality from
|
||||||
the PipeWire daemon.
|
the PipeWire daemon.
|
||||||
|
|
||||||
* **virtual**
|
* **virtual**
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
.. _config_conf_file:
|
.. _config_conf_file:
|
||||||
|
|
||||||
The configuration file
|
Configuration file
|
||||||
======================
|
==================
|
||||||
|
|
||||||
WirePlumber's configuration file is by default ``wireplumber.conf`` and resides
|
WirePlumber's configuration file is by default ``wireplumber.conf`` and resides
|
||||||
in one of the WirePlumber specific
|
in the ``pipewire`` configuration directory (see :ref:`config_locations` for
|
||||||
:ref:`configuration file search locations <config_locations>`.
|
more details on that).
|
||||||
|
|
||||||
The default configuration file can be changed on the command line by passing
|
The default configuration file can be changed on the command line by passing
|
||||||
the ``--config-file`` or ``-c`` option:
|
the ``--config-file`` or ``-c`` option:
|
||||||
|
|
@ -14,20 +14,19 @@ the ``--config-file`` or ``-c`` option:
|
||||||
|
|
||||||
$ wireplumber --config-file=custom.conf
|
$ wireplumber --config-file=custom.conf
|
||||||
|
|
||||||
.. important::
|
.. note::
|
||||||
|
|
||||||
Starting with WirePlumber 0.5, this is the only file that WirePlumber reads
|
Starting with WirePlumber 0.5, this is the only file that WirePlumber reads
|
||||||
to load configuration (together with its fragments - see below). In the past,
|
to load configuration (together with its fragments - see below). In the past,
|
||||||
WirePlumber also used to read Lua configuration files that were referenced
|
WirePlumber also used to read Lua configuration files that were referenced
|
||||||
from ``wireplumber.conf`` and all the heavy lifting was done in Lua. This is
|
from ``wireplumber.conf`` and all the heavy lifting was done in Lua. This is
|
||||||
no longer the case, and the **Lua configuration files are no longer supported.**
|
no longer the case, and the Lua configuration files are no longer supported.
|
||||||
See :ref:`config_migration`.
|
|
||||||
|
|
||||||
Note that Lua is still the scripting language for WirePlumber, but it is only
|
Note that Lua is still the scripting language for WirePlumber, but it is only
|
||||||
used for actual scripting and not for configuration.
|
used for actual scripting and not for configuration.
|
||||||
|
|
||||||
The SPA-JSON Format
|
Format
|
||||||
-------------------
|
------
|
||||||
|
|
||||||
The format of this configuration file is a variant of JSON that is also
|
The format of this configuration file is a variant of JSON that is also
|
||||||
used in PipeWire configuration files (also known as SPA-JSON). The file consists
|
used in PipeWire configuration files (also known as SPA-JSON). The file consists
|
||||||
|
|
@ -90,8 +89,6 @@ Examples of valid SPA-JSON files:
|
||||||
"val1", "val2", "val3"
|
"val1", "val2", "val3"
|
||||||
]
|
]
|
||||||
|
|
||||||
.. _config_conf_file_fragments:
|
|
||||||
|
|
||||||
Fragments
|
Fragments
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|
@ -104,18 +101,17 @@ When loading the configuration file, WirePlumber will also look for
|
||||||
additional files in the directory that has the same name as the configuration
|
additional files in the directory that has the same name as the configuration
|
||||||
file suffixed with ``.d`` and will load all of them as well. For example,
|
file suffixed with ``.d`` and will load all of them as well. For example,
|
||||||
loading ``wireplumber.conf`` will also load any ``.conf`` files under
|
loading ``wireplumber.conf`` will also load any ``.conf`` files under
|
||||||
``wireplumber.conf.d/``. This directory is searched in all the configuration
|
``wireplumber.conf.d/``. This directory is searched in all the search paths
|
||||||
search locations and the fragments are loaded from *all* of them, starting
|
for configuration files (see :ref:`config_locations`) and the fragments are
|
||||||
from the most system-wide locations and moving towards the most user-specific
|
loaded from *all* of them.
|
||||||
locations, in alphanumerical order within each location (see also
|
|
||||||
:ref:`config_locations_fragments`).
|
|
||||||
|
|
||||||
When a JSON object appears in multiple files, the properties of the objects are
|
The fragments are loaded in alphabetical order, after the main configuration
|
||||||
merged together. When a JSON array appears in multiple files, the arrays are
|
file. When a JSON object appears in multiple files, the properties of the
|
||||||
concatenated together. When merging objects, if specific properties appear in
|
objects are merged together. When a JSON array appears in multiple files, the
|
||||||
many of those objects, the last one to be parsed always overwrites previous
|
arrays are concatenated together. When merging objects, if specific properties
|
||||||
ones, unless the value is also an object or array; if it is, then the value is
|
appear in many of those objects, the last one to be parsed always overwrites
|
||||||
recursively merged using the same rules.
|
previous ones, unless the value is also an object or array; if it is, then the
|
||||||
|
value is recursively merged using the same rules.
|
||||||
|
|
||||||
Sections
|
Sections
|
||||||
--------
|
--------
|
||||||
|
|
@ -128,12 +124,6 @@ file:
|
||||||
This section is an array that lists components that can be loaded by
|
This section is an array that lists components that can be loaded by
|
||||||
WirePlumber. For more information, see :ref:`config_components_and_profiles`.
|
WirePlumber. For more information, see :ref:`config_components_and_profiles`.
|
||||||
|
|
||||||
* *wireplumber.components.rules*
|
|
||||||
|
|
||||||
This section is an array containing rules that can be used to modify entries
|
|
||||||
of the *wireplumber.components* array. This is useful to inject changes
|
|
||||||
to the components list without having to modify the main configuration file.
|
|
||||||
|
|
||||||
* *wireplumber.profiles*
|
* *wireplumber.profiles*
|
||||||
|
|
||||||
This section is an object that defines profiles that can be loaded by
|
This section is an object that defines profiles that can be loaded by
|
||||||
|
|
@ -144,13 +134,6 @@ file:
|
||||||
This section is an object that defines settings that can be used to
|
This section is an object that defines settings that can be used to
|
||||||
alter WirePlumber's behavior. For more information, see :ref:`config_settings`.
|
alter WirePlumber's behavior. For more information, see :ref:`config_settings`.
|
||||||
|
|
||||||
* *wireplumber.settings.schema*
|
|
||||||
|
|
||||||
This section is an object that defines the schema for the settings that
|
|
||||||
can be listed in *wireplumber.settings*. This is used to validate the
|
|
||||||
settings when they are modified at runtime. For more information, see
|
|
||||||
:ref:`config_configuration_option_types`.
|
|
||||||
|
|
||||||
In addition, there are many sections that are specific to certain components,
|
In addition, there are many sections that are specific to certain components,
|
||||||
mostly hardware monitors, such as *monitor.alsa.properties*,
|
mostly hardware monitors, such as *monitor.alsa.properties*,
|
||||||
*monitor.alsa.rules*, etc. These are documented further on, in the respective
|
*monitor.alsa.rules*, etc. These are documented further on, in the respective
|
||||||
|
|
@ -204,18 +187,9 @@ by libpipewire to configure the PipeWire context:
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
PipeWire modules can also be loaded as :ref:`components <config_components_and_profiles>`,
|
PipeWire modules can also be loaded as :ref:`components <config_components_and_profiles>`,
|
||||||
which may be preferable since it allows you to load them conditionally
|
which may be preferrable since it allows you to load them conditionally
|
||||||
based on the profile and component dependencies.
|
based on the profile and component dependencies.
|
||||||
|
|
||||||
.. admonition:: Remember
|
|
||||||
|
|
||||||
Modules listed in *context.modules* are always loaded before attempting a
|
|
||||||
connection to the PipeWire daemon, while modules listed in
|
|
||||||
*wireplumber.components* are always loaded after the connection is
|
|
||||||
established. It is important to load the PipeWire protocol-native module
|
|
||||||
and any extensions (such as module-metadata) in the *context.modules*
|
|
||||||
section, so that the connection can be done properly.
|
|
||||||
|
|
||||||
Each module is described by a JSON object containing the module's *name*,
|
Each module is described by a JSON object containing the module's *name*,
|
||||||
its arguments (*args*) and a combination of *flags*, which can be ``ifexists``
|
its arguments (*args*) and a combination of *flags*, which can be ``ifexists``
|
||||||
and ``nofail``.
|
and ``nofail``.
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
.. _config_configuration_option_types:
|
|
||||||
|
|
||||||
Configuration option types
|
|
||||||
==========================
|
|
||||||
|
|
||||||
As seen in the previous sections, WirePlumber can be partly configured by
|
|
||||||
enabling or disabling features, which affect which components are getting
|
|
||||||
loaded. These components, however, can be further configured to fine-tune their
|
|
||||||
behavior. This section describes the different types of configuration options
|
|
||||||
that can be used to configure WirePlumber components.
|
|
||||||
|
|
||||||
Dynamic options ("Settings")
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
Dynamic options (also simply referred to as "settings") are configuration
|
|
||||||
options that can be changed at runtime. They are typically simple values like
|
|
||||||
booleans, integers, strings, etc. and are all located under the
|
|
||||||
``wireplumber.settings`` section in the configuration file. Their purpose is to
|
|
||||||
allow the user to change simple behavioral aspects of WirePlumber.
|
|
||||||
|
|
||||||
As the name suggests, these options are dynamic and can be changed at runtime
|
|
||||||
using ``wpctl`` or the :ref:`settings_api` API. For example, setting the
|
|
||||||
``device.routes.default-sink-volume`` setting to ``0.5`` can be done like this:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings device.routes.default-sink-volume 0.5
|
|
||||||
|
|
||||||
Under the hood, when WirePlumber starts, the ``metadata.sm-settings`` component
|
|
||||||
(provided by ``libwireplumber-module-settings``) reads this section from the
|
|
||||||
configuration file and populates the ``sm-settings`` metadata object, which is
|
|
||||||
exported to PipeWire. In addition, it reads the ``wireplumber.settings.schema``
|
|
||||||
section and populates the ``schema-sm-settings`` metadata object, which is used
|
|
||||||
by the API to validate the settings. Any options that are missing from
|
|
||||||
``wireplumber.settings`` are also populated in ``sm-settings`` from their
|
|
||||||
default values in the schema. Then the rest of the components read their
|
|
||||||
configuration options from this metadata object via the :ref:`settings_api` API.
|
|
||||||
|
|
||||||
Most of the components that use such dynamic options make sure to listen
|
|
||||||
to changes in the metadata object so that they can immediately adapt their
|
|
||||||
behavior. Other components, however, do not react immediately and the changes
|
|
||||||
only take effect the next time the option is needed. For instance, some options
|
|
||||||
affect created objects in a way that cannot be changed after the object has been
|
|
||||||
created, so when the option is changed it applies only to new objects and not
|
|
||||||
existing ones.
|
|
||||||
|
|
||||||
Changing the settings at runtime in the ``sm-settings`` metadata object is
|
|
||||||
a non-persistent change. The changes will be lost when WirePlumber is
|
|
||||||
restarted. However, the :ref:`settings_api` API also supports saving settings
|
|
||||||
to a state file, which will be loaded again when WirePlumber starts and
|
|
||||||
override the settings from the configuration file. This is done by using yet
|
|
||||||
another metadata object called ``persistent-sm-settings``. When a setting is
|
|
||||||
changed in the ``persistent-sm-settings`` metadata object, WirePlumber
|
|
||||||
automatically saves the change to the state file and also changes the value in
|
|
||||||
the ``sm-settings`` metadata object immediately.
|
|
||||||
|
|
||||||
To make such a persistent change using ``wpctl``, the ``--save`` option can be
|
|
||||||
used. For example, to set the ``device.routes.default-sink-volume`` setting to
|
|
||||||
``0.5`` and save it to the state file:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings --save device.routes.default-sink-volume 0.5
|
|
||||||
|
|
||||||
With ``wpctl``, it is also possible to restore a setting to its default value
|
|
||||||
(taken from the schema), by using the ``--reset`` option. For example, to reset
|
|
||||||
the ``device.routes.default-sink-volume`` setting, the following command can be
|
|
||||||
used:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings --reset device.routes.default-sink-volume
|
|
||||||
|
|
||||||
In addition, the ``--delete`` option can be used to delete a setting from the
|
|
||||||
``persistent-sm-settings`` metadata object, which will also remove it from the
|
|
||||||
state file. After deleting, the value from the ``wireplumber.settings`` section
|
|
||||||
of the configuration file will be used again. For example, to delete the
|
|
||||||
``device.routes.default-sink-volume`` setting, the following command can be
|
|
||||||
used:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings --delete device.routes.default-sink-volume
|
|
||||||
|
|
||||||
A list of all the available settings can be found in the :ref:`config_settings`
|
|
||||||
section.
|
|
||||||
|
|
||||||
Static options
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Static options are more complex configuration structures that reside only in the
|
|
||||||
configuration file and cannot be changed at runtime. They are typically used to
|
|
||||||
configure device monitors and provide rules that match objects and perform
|
|
||||||
actions such as update their properties.
|
|
||||||
|
|
||||||
While these options could also in theory be stored in the metadata object and
|
|
||||||
be made dynamic, this is not supported because these options are both complex
|
|
||||||
and therefore hard to change on the command line, but also because they are
|
|
||||||
typically used to configure objects that are created at startup and cannot be
|
|
||||||
changed later.
|
|
||||||
|
|
||||||
Static options are located in their own top-level sections. Examples of such
|
|
||||||
sections are ``monitor.alsa.properties`` and ``monitor.alsa.rules`` that are
|
|
||||||
used to configure the ``monitor.alsa`` component. The next sections of this
|
|
||||||
documentation describe in detail all the available static options.
|
|
||||||
|
|
||||||
Component arguments
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Components can also be configured statically by passing arguments to them when
|
|
||||||
they are loaded. This is done by adding an ``arguments`` key to the component
|
|
||||||
description in the ``wireplumber.components`` section (see
|
|
||||||
:ref:`config_components_and_profiles`).
|
|
||||||
|
|
||||||
The arguments are mostly meant as a way to instantiate multiple instances of the
|
|
||||||
same module or script with slightly different configuration to create a new
|
|
||||||
unique component. For example, the ``metadata.lua`` script can be instantiated
|
|
||||||
multiple times to create multiple metadata objects, each with a different name.
|
|
||||||
The name of the metadata object is passed as an argument to the script.
|
|
||||||
|
|
||||||
While many more static options could be passed as arguments, this is not
|
|
||||||
recommended because it is not possible to override the arguments by adding
|
|
||||||
:ref:`fragment<config_conf_file_fragments>` configuration files. Therefore, it
|
|
||||||
is recommended to use component-specific top-level sections, unless the option
|
|
||||||
is not meant to be changed by the user.
|
|
||||||
|
|
@ -11,9 +11,6 @@ can be confusing to go through them. This list here is meant to be a quick
|
||||||
reference for the most common ones that actually make sense to be toggled in
|
reference for the most common ones that actually make sense to be toggled in
|
||||||
a configuration file in order to customize WirePlumber's behavior.
|
a configuration file in order to customize WirePlumber's behavior.
|
||||||
|
|
||||||
For more information on what features are and how they work, refer to the
|
|
||||||
previous section: :ref:`config_components_and_profiles`.
|
|
||||||
|
|
||||||
Hardware monitors
|
Hardware monitors
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
@ -42,12 +39,6 @@ Audio
|
||||||
|
|
||||||
Enables the ALSA MIDI device monitor.
|
Enables the ALSA MIDI device monitor.
|
||||||
|
|
||||||
.. describe:: node.software-dsp
|
|
||||||
|
|
||||||
Enables software DSP based on pre-configured hardware rules.
|
|
||||||
|
|
||||||
See :ref:`policies_software_dsp` for more information.
|
|
||||||
|
|
||||||
Bluetooth
|
Bluetooth
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
|
|
@ -140,9 +131,9 @@ Policies
|
||||||
for enabling devices, linking streams, granting permissions to clients,
|
for enabling devices, linking streams, granting permissions to clients,
|
||||||
etc, as appropriate for a desktop system.
|
etc, as appropriate for a desktop system.
|
||||||
|
|
||||||
.. describe:: policy.role-based
|
.. describe:: policy.role-priority-system
|
||||||
|
|
||||||
Enables the role based priority system policy. This system creates virtual sinks
|
Enables the role priority system policy. This system creates virtual sinks
|
||||||
that group streams based on their ``media.role`` property, and assigns a
|
that group streams based on their ``media.role`` property, and assigns a
|
||||||
priority to each role. Depending on the priority configuration, lower
|
priority to each role. Depending on the priority configuration, lower
|
||||||
priority roles may be corked or ducked when a higher priority role stream
|
priority roles may be corked or ducked when a higher priority role stream
|
||||||
|
|
|
||||||
90
docs/rst/daemon/configuration/locations.rst
Normal file
90
docs/rst/daemon/configuration/locations.rst
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
.. _config_locations:
|
||||||
|
|
||||||
|
Locations of files
|
||||||
|
==================
|
||||||
|
|
||||||
|
Location of configuration files
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
WirePlumber's default locations of its configuration files are the same as
|
||||||
|
pipewire. Typically, those end up being
|
||||||
|
``$XDG_CONFIG_DIR/pipewire``, ``/etc/pipewire``, and
|
||||||
|
``/usr/share/pipewire``, in that order of priority.
|
||||||
|
|
||||||
|
The three locations are intended for custom user configuration,
|
||||||
|
host-specific configuration and distribution-provided configuration,
|
||||||
|
respectively. At runtime, WirePlumber will search the directories
|
||||||
|
for the highest-priority directory to contain the needed configuration file.
|
||||||
|
This allows a user or system administrator to easily override the distribution
|
||||||
|
provided configuration files by placing an equally named file in the respective
|
||||||
|
directory.
|
||||||
|
|
||||||
|
It is also possible to override the configuration directory by setting the
|
||||||
|
``WIREPLUMBER_CONFIG_DIR`` environment variable::
|
||||||
|
|
||||||
|
WIREPLUMBER_CONFIG_DIR=src/config wireplumber
|
||||||
|
|
||||||
|
For convenience, the behaviour of the ``WIREPLUMBER_CONFIG_DIR`` environment
|
||||||
|
variable is the same as the ``PIPEWIRE_CONFIG_DIR`` environment variable.
|
||||||
|
If ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
|
||||||
|
configuration files are *only* looked up in this directory.
|
||||||
|
|
||||||
|
|
||||||
|
Location of scripts
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
WirePlumber's default locations of its scripts are the same ones as for the
|
||||||
|
configuration files, but with the ``scripts`` directory appended.
|
||||||
|
Typically, these end up being ``$XDG_CONFIG_DIR/wireplumber/scripts``,
|
||||||
|
``/etc/wireplumber/scripts``, and ``/usr/share/wireplumber/scripts``,
|
||||||
|
in that order of priority.
|
||||||
|
|
||||||
|
The three locations are intended for custom user scripts,
|
||||||
|
host-specific scripts and distribution-provided scripts, respectively.
|
||||||
|
At runtime, WirePlumber will search the directories for the highest-priority
|
||||||
|
directory to contain the needed script.
|
||||||
|
|
||||||
|
It is also possible to override the scripts directory by setting the
|
||||||
|
``WIREPLUMBER_DATA_DIR`` environment variable::
|
||||||
|
|
||||||
|
WIREPLUMBER_DATA_DIR=src wireplumber
|
||||||
|
|
||||||
|
The "data" directory is a somewhat more generic path that may be used for
|
||||||
|
other kinds of data files in the future. For scripts, WirePlumber still expects
|
||||||
|
to find a ``scripts`` subdirectory in this "data" directory, so in the above
|
||||||
|
example the scripts would be in ``src/scripts``.
|
||||||
|
|
||||||
|
If ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
|
||||||
|
scripts are *only* looked up in this directory.
|
||||||
|
|
||||||
|
Location of modules
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
WirePlumber modules
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Like with configuration files, WirePlumber's default location of its modules is
|
||||||
|
determined at compile time by the build system. Typically, it ends up being
|
||||||
|
``/usr/lib/wireplumber-0.4`` (or ``/usr/lib/<arch-triplet>/wireplumber-0.4`` on
|
||||||
|
multiarch systems)
|
||||||
|
|
||||||
|
In more detail, this is controlled by the ``--libdir`` meson option. When
|
||||||
|
this is set to an absolute path, such as ``/lib``, the location of the
|
||||||
|
modules is set to be ``$libdir/wireplumber-$abi_version``. When this is set
|
||||||
|
to a relative path, such as ``lib``, then the installation prefix (``--prefix``)
|
||||||
|
is prepended to the path: ``$prefix/$libdir/wireplumber-$abi_version``.
|
||||||
|
|
||||||
|
It is possible to override this directory at runtime by setting the
|
||||||
|
``WIREPLUMBER_MODULE_DIR`` environment variable::
|
||||||
|
|
||||||
|
WIREPLUMBER_MODULE_DIR=build/modules wireplumber
|
||||||
|
|
||||||
|
PipeWire and SPA modules
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
PipeWire and SPA modules are not loaded from the same location as WirePlumber's
|
||||||
|
modules. They are loaded from the location that PipeWire loads them.
|
||||||
|
|
||||||
|
It is also possible to override these locations by using environment variables:
|
||||||
|
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
|
||||||
|
PipeWire's documentation.
|
||||||
478
docs/rst/daemon/configuration/main.rst
Normal file
478
docs/rst/daemon/configuration/main.rst
Normal file
|
|
@ -0,0 +1,478 @@
|
||||||
|
.. _config_main:
|
||||||
|
|
||||||
|
Main configuration file
|
||||||
|
=======================
|
||||||
|
|
||||||
|
The main configuration file is by default called ``wireplumber.conf``. This can
|
||||||
|
be changed on the command line by passing the ``--config-file`` or ``-c`` option::
|
||||||
|
|
||||||
|
wireplumber --config-file=bluetooth.conf
|
||||||
|
|
||||||
|
The ``--config-file`` option is useful to run multiple instances of wireplumber
|
||||||
|
that do separate tasks each. For more information on this subject, see the
|
||||||
|
:ref:`Multiple Instances <config_multi_instance>` section.
|
||||||
|
|
||||||
|
The format of this configuration file is the variant of JSON that is also
|
||||||
|
used in PipeWire configuration files. Note that this is subject to change
|
||||||
|
in the future.
|
||||||
|
|
||||||
|
All sections are essentially JSON objects. Lines starting with *#* are treated
|
||||||
|
as comments and ignored. The list of all possible section JSON objects are:
|
||||||
|
|
||||||
|
Common configs are present in the main configuration file(wireplumber.conf),
|
||||||
|
rest of the configs that can be grouped logically are grouped into separate
|
||||||
|
files and are placed under ``wireplumber.conf.d/``. More on this below.
|
||||||
|
|
||||||
|
* *context.properties*
|
||||||
|
|
||||||
|
Used to define properties to configure the PipeWire context and some modules.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
context.properties = {
|
||||||
|
application.name = WirePlumber
|
||||||
|
log.level = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
This sets the daemon's name to *WirePlumber* and the log level to *2*, which
|
||||||
|
only displays errors and warnings. See the
|
||||||
|
:ref:`Debug Logging <daemon_logging>` section for more details.
|
||||||
|
|
||||||
|
* *context.spa-libs*
|
||||||
|
|
||||||
|
Used to find spa factory names. It maps a spa factory name regular expression
|
||||||
|
to a library name that should contain that factory. The object property names
|
||||||
|
are the regular expression, and the object property values are the actual
|
||||||
|
library name::
|
||||||
|
|
||||||
|
<factory-name regex> = <library-name>
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
context.spa-libs = {
|
||||||
|
api.alsa.* = alsa/libspa-alsa
|
||||||
|
audio.convert.* = audioconvert/libspa-audioconvert
|
||||||
|
}
|
||||||
|
|
||||||
|
In this example, we instruct wireplumber to only any *api.alsa.** factory name
|
||||||
|
from the *libspa-alsa* library, and also any *audio.convert.** factory name
|
||||||
|
from the *libspa-audioconvert* library.
|
||||||
|
|
||||||
|
* *context.modules*
|
||||||
|
|
||||||
|
Used to load PipeWire modules. This does not affect the PipeWire daemon by any
|
||||||
|
means. It exists simply to allow loading *libpipewire* modules in the PipeWire
|
||||||
|
core that runs inside WirePlumber. This is usually useful to load PipeWire
|
||||||
|
protocol extensions, so that you can export custom objects to PipeWire and
|
||||||
|
other clients.
|
||||||
|
|
||||||
|
Users can also pass key-value pairs if the specific module has arguments, and
|
||||||
|
a combination of 2 flags: ``ifexists`` flag is given, the module is ignored when
|
||||||
|
not found; if ``nofail`` is given, module initialization failures are ignored::
|
||||||
|
|
||||||
|
{
|
||||||
|
name = <module-name>
|
||||||
|
[ args = { <key> = <value> ... } ]
|
||||||
|
[ flags = [ [ ifexists ] [ nofail ] ]
|
||||||
|
}
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
{ name = libpipewire-module-adapter }
|
||||||
|
{
|
||||||
|
name = libpipewire-module-metadata,
|
||||||
|
flags = [ ifexists ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
The above example loads both PipeWire adapter and metadata modules. The
|
||||||
|
metadata module will be ignored if not found because of its ``ifexists`` flag.
|
||||||
|
|
||||||
|
* *wireplumber.components*
|
||||||
|
|
||||||
|
Used to load WirePlumber components. Components can be either WirePlumber
|
||||||
|
modules written in C or WirePlumber scripts written in Lua.
|
||||||
|
|
||||||
|
Syntax::
|
||||||
|
|
||||||
|
{ name = <component-name>, type = <component-type>, deps = <dependent-setting>, flags = <flags> }
|
||||||
|
|
||||||
|
* type:
|
||||||
|
|
||||||
|
Valid component types include:
|
||||||
|
|
||||||
|
* ``module``: A WirePlumber shared object module
|
||||||
|
* ``script/lua``: A WirePlumber Lua script
|
||||||
|
(all Lua Scripts implicitly requires libwireplumber-module-lua-scripting module)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
wireplumber.components = [
|
||||||
|
{ name = libwireplumber-module-lua-scripting, type = module }
|
||||||
|
{ name = monitors/alsa.lua, type = script/lua }
|
||||||
|
]
|
||||||
|
|
||||||
|
* deps: components can be loaded with a dependency on a wireplumber setting.
|
||||||
|
* flags: ifexists & nofail flags are supported in this section as well.
|
||||||
|
|
||||||
|
|
||||||
|
* `ifexists` - signals wireplumber to ignore if the module is not found.
|
||||||
|
* `nofail` - signals wireplumber to ignore module initialization failures.
|
||||||
|
|
||||||
|
More Examples::
|
||||||
|
|
||||||
|
wireplumber.components = [
|
||||||
|
# Load `libwireplumber-module-si-node` which is of type `module`.
|
||||||
|
{ name = libwireplumber-module-si-node , type = module }
|
||||||
|
|
||||||
|
# Load `libwireplumber-module-reserve-device` module, only if the setting `alsa_monitor.alsa.reserve` is defined as true.
|
||||||
|
{ name = libwireplumber-module-reserve-device , type = module, deps = alsa_monitor.alsa.reserve }
|
||||||
|
|
||||||
|
# Load `alsa.lua` which is of type `script/lua`.
|
||||||
|
{ name = monitors/alsa.lua, type = script/lua }
|
||||||
|
|
||||||
|
# Load `alsa-midi.lua` Lua Script only if `alsa_monitor.alsa.midi` setting is defined as true.
|
||||||
|
{ name = monitors/alsa-midi.lua, type = script/lua, deps = alsa_monitor.alsa.midi }
|
||||||
|
|
||||||
|
# Load `libwireplumber-module-logind` module if the setting `bluez-enable-logind` is true.
|
||||||
|
{ name = libwireplumber-module-logind , type = module, deps = bluez-enable-logind, flags = [ ifexists ] }
|
||||||
|
]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
- `name` & `type` keys are mandatory, while `deps` and `flags` keys are optional
|
||||||
|
- All the components are loaded during the bootup and failure in finding them or any error during the loading process is a fatal error and WirePlumber will exit.
|
||||||
|
|
||||||
|
|
||||||
|
* *wireplumber.settings*
|
||||||
|
|
||||||
|
All the Wireplumber configuration settings are now grouped under this
|
||||||
|
section. They are moved away from Lua.
|
||||||
|
|
||||||
|
All the default settings are distributed into different
|
||||||
|
files(\*settings.conf) under ``wireplumber.conf.d\``
|
||||||
|
|
||||||
|
All the settings are loaded into ``sm-settings`` metadata. Apart from the
|
||||||
|
settings JSON files, Metadata interface can be used to change them.
|
||||||
|
|
||||||
|
:ref:`WpSettings <settings_api>` provides APIs to its clients
|
||||||
|
(modules, lua scripts etc) to access and track them.
|
||||||
|
|
||||||
|
Settings can be persistent, more on this below.
|
||||||
|
|
||||||
|
There can be two types of settings namely plain settings(called just settings
|
||||||
|
for reasons of simplicity) and rules.
|
||||||
|
|
||||||
|
* `Settings`
|
||||||
|
|
||||||
|
Syntax::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
<setting1> = <value>
|
||||||
|
<setting2> = <value>
|
||||||
|
..
|
||||||
|
}
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
alsa_monitor.alsa.reserve = true
|
||||||
|
alsa_monitor.alsa.midi = "true"
|
||||||
|
default-policy-duck.level = 0.3
|
||||||
|
bt-policy-media-role.applications = ["Firefox", "Chromium input"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Value can be string, int, float, boolean and can even be a JSON array.
|
||||||
|
|
||||||
|
WpSettings exposes the `wp_settings_get_{string|int|float|boolean}()` APIs
|
||||||
|
to access the values.
|
||||||
|
|
||||||
|
Lua scripts, modules use these APIs to access settings.
|
||||||
|
The client accessing the setting should know which API to use to access
|
||||||
|
the setting accurately.
|
||||||
|
|
||||||
|
If the Setting is a JSON array like `bt-policy-media-role.applications`
|
||||||
|
_get_string() API need to be used and the obtained JSON element will have
|
||||||
|
to be parsed using the :ref:`JSON APIs. <spa_json_api>`
|
||||||
|
|
||||||
|
Persistent Behavior::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
persistent.settings = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Persistent behavior can be enabled with the above syntax.
|
||||||
|
|
||||||
|
When enabled, the settings will be read from conf file only once and for
|
||||||
|
subsequent reboots they will be read from the state(cache) files, till the
|
||||||
|
time the setting is set back to false in the .conf file.
|
||||||
|
|
||||||
|
Settings can be changed through metadata, so when they are updated through
|
||||||
|
metadata and if the user desires those settings to be persistent between
|
||||||
|
reboots this persistent option can be used.
|
||||||
|
|
||||||
|
wp_settings_register_{callback|closure} () API can be used by clients to
|
||||||
|
keep track of the changes to settings.
|
||||||
|
|
||||||
|
The persistent behavior is disabled by default.
|
||||||
|
|
||||||
|
* `Rules`
|
||||||
|
|
||||||
|
Rules are dynamic logic based settings.
|
||||||
|
|
||||||
|
Syntax
|
||||||
|
|
||||||
|
Simple Syntax::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
<rule-name> = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
<pipewire property1> = <value>
|
||||||
|
<pipewire property2> = <value>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
<pipewire property> = <value>,
|
||||||
|
<wireplumber setting> = <value>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Simple Example::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
stream_default = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
# Matches all devices
|
||||||
|
{ application.name = "pw-play" }
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
state.restore-props = false
|
||||||
|
state.restore-target = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream_default rule scans for pw-play app and if found it applies the two
|
||||||
|
properties listed above.
|
||||||
|
|
||||||
|
Advanced Syntax::
|
||||||
|
|
||||||
|
# Nested behavior
|
||||||
|
wireplumber.settings = {
|
||||||
|
<rule-name> = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
# Logical AND behavior with the JSON object
|
||||||
|
<pipewire property1> = <value>
|
||||||
|
<pipewire property2> = <value>
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logical OR behavior across the JSON objects.
|
||||||
|
{
|
||||||
|
<pipewire property3> = <value>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
<pipewire property> = <value>,
|
||||||
|
<wireplumber setting> = <value>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use of regular expressions
|
||||||
|
wireplumber.settings = {
|
||||||
|
<rule-name> = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
# if a value starts with ``~`` it triggers regular expression evaluation
|
||||||
|
<pipewire property1> = <~value*>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
<pipewire property> = <value>,
|
||||||
|
<wireplumber setting> = <value>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Multiple Matches with in a single rule is possible.
|
||||||
|
wireplumber.settings = {
|
||||||
|
<rule-name> = [
|
||||||
|
{
|
||||||
|
# Match 1
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
<pipewire property1> = <~value*>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
<pipewire property1> = <value>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Match 2
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
<pipewire property2> = <~value*>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
<pipewire property2> = <value>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Advanced Example::
|
||||||
|
|
||||||
|
wireplumber.settings = {
|
||||||
|
|
||||||
|
alsa_monitor = [
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
# This matches all sound cards.
|
||||||
|
device.name = "~alsa_card.*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
# and applies these properties.
|
||||||
|
api.alsa.use-acp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
# Matches either input nodes or output nodes
|
||||||
|
{
|
||||||
|
node.name = "~alsa_input.*"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
node.name = "~alsa_output.*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
node.nick = "My Node"
|
||||||
|
priority.driver = 100
|
||||||
|
session.suspend-timeout-seconds = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
* wp_settings_apply_rule () is WpSettings API for rules.
|
||||||
|
|
||||||
|
|
||||||
|
* *wireplumber.virtuals*
|
||||||
|
|
||||||
|
Virtual session items are a way of grouping different kinds of clients or
|
||||||
|
applications(for example Music, Voice, Navigation, Gaming etc).
|
||||||
|
The actual grouping is done based on the `media.role` of the client
|
||||||
|
stream node.
|
||||||
|
|
||||||
|
Virtual session items allows for that actions to be taken up at group level
|
||||||
|
rather than at individual stream level, which can be cumbersome.
|
||||||
|
|
||||||
|
For example imagine the following scenarios.
|
||||||
|
* Incoming Navigation message needs to duck the volume of
|
||||||
|
Audio playback(all the apps playing audio).
|
||||||
|
* Incoming voice/voip call needs to stop(cork) the Audio playback.
|
||||||
|
|
||||||
|
Virtual session items realize this functionality with ease.
|
||||||
|
|
||||||
|
* *Defining Virtual session items*
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
virtual-items = {
|
||||||
|
virtual-item.capture = {
|
||||||
|
media.class = "Audio/Source"
|
||||||
|
role = "Capture"
|
||||||
|
}
|
||||||
|
virtual-item.multimedia = {
|
||||||
|
media.class = "Audio/Sink"
|
||||||
|
role = "Multimedia"
|
||||||
|
}
|
||||||
|
virtual-item.navigation = {
|
||||||
|
media.class = "Audio/Sink"
|
||||||
|
role = "Navigation"
|
||||||
|
}
|
||||||
|
|
||||||
|
This example creates 3 virtual session items, with names
|
||||||
|
``virtual-item.capture``, ``virtual-item.multimedia`` and
|
||||||
|
``virtual-item.navigation`` and assigned roles ``Capture``, ``Multimedia``
|
||||||
|
and ``Navigation`` respectively.
|
||||||
|
|
||||||
|
First virtual item has a media class of ``Audio/Source`` used for capture
|
||||||
|
and rest of the virtual items have ``Audio/Sink`` media class, and so are
|
||||||
|
only used for playback.
|
||||||
|
|
||||||
|
* *Virtual session items config*
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
Capture = {
|
||||||
|
alias = [ "Multimedia", "Music", "Voice", "Capture" ]
|
||||||
|
priority = 25
|
||||||
|
action.default = "cork"
|
||||||
|
action.capture = "mix"
|
||||||
|
media.class = "Audio/Source"
|
||||||
|
}
|
||||||
|
Multimedia = {
|
||||||
|
alias = [ "Movie" "Music" "Game" ]
|
||||||
|
priority = 25
|
||||||
|
action.default = "cork"
|
||||||
|
}
|
||||||
|
Navigation = {
|
||||||
|
priority = 50
|
||||||
|
action.default = "duck"
|
||||||
|
action.Navigation = "mix"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
The above example defines actions for both ``Multimedia`` and ``Navigation``
|
||||||
|
roles. Since the Navigation role has more priority than the Multimedia
|
||||||
|
role, when a client connects to the Navigation virtual session item, it
|
||||||
|
will ``duck`` the volume of all Multimedia clients. If Multiple Navigation
|
||||||
|
clients want to play audio, their audio will be mixed.
|
||||||
|
|
||||||
|
Possible values of actions are: ``mix`` (Mixes audio),
|
||||||
|
``duck`` (Mixes and lowers the audio volume) or ``cork`` (Pauses audio).
|
||||||
|
|
||||||
|
Virtual session items are not used for desktop use cases, it is more suitable
|
||||||
|
for embedded use cases.
|
||||||
|
|
||||||
|
* *Split Configuration files*
|
||||||
|
|
||||||
|
The Main configuration file is split into multiple files. When loading the main
|
||||||
|
JSON configuration file, WirePlumber will also look for additional files in the
|
||||||
|
same directory suffixed with ``.d`` and will load all of them as well. For
|
||||||
|
example, loading ``wireplumber.conf`` will also load any files under
|
||||||
|
``wireplumber.conf.d/``. It will load all the JSON config files there. All the
|
||||||
|
configurations are logically split into files and placed in this directory.
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
sphinx_files += files(
|
sphinx_files += files(
|
||||||
'conf_file.rst',
|
'conf_file.rst',
|
||||||
'components_and_profiles.rst',
|
'components_and_profiles.rst',
|
||||||
'configuration_option_types.rst',
|
|
||||||
'modifying_configuration.rst',
|
|
||||||
'migration.rst',
|
|
||||||
'features.rst',
|
'features.rst',
|
||||||
'settings.rst',
|
'settings.rst',
|
||||||
|
'locations.rst',
|
||||||
|
'main.rst',
|
||||||
|
'multi_instance.rst',
|
||||||
'alsa.rst',
|
'alsa.rst',
|
||||||
'bluetooth.rst',
|
'bluetooth.rst',
|
||||||
|
'policy.rst',
|
||||||
'access.rst',
|
'access.rst',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
.. _config_migration:
|
|
||||||
|
|
||||||
Migrating configuration from 0.4
|
|
||||||
================================
|
|
||||||
|
|
||||||
The configuration file format has changed in version 0.5. No automatic migration
|
|
||||||
of old configuration files is performed, so you will have to manually update
|
|
||||||
them. This document describes the changes and how to update your configuration.
|
|
||||||
|
|
||||||
wireplumber.conf
|
|
||||||
----------------
|
|
||||||
|
|
||||||
In WirePlumber 0.4, there used to be a ``.conf`` file, typically
|
|
||||||
``wireplumber.conf``, using the SPA-JSON format, that would list some Lua
|
|
||||||
scripts in the ``wireplumber.components`` section. These scripts were of type
|
|
||||||
``config/lua`` and they were called by default ``main.lua``, ``policy.lua`` and
|
|
||||||
``bluetooth.lua``.
|
|
||||||
|
|
||||||
Typical ``wireplumber.components`` section of a ``wireplumber.conf`` file in 0.4
|
|
||||||
would look like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.components = [
|
|
||||||
#{ name = <component-name>, type = <component-type> }
|
|
||||||
#
|
|
||||||
# WirePlumber components to load
|
|
||||||
#
|
|
||||||
|
|
||||||
# The lua scripting engine
|
|
||||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
|
||||||
|
|
||||||
# The lua configuration file(s)
|
|
||||||
# Other components are loaded from there
|
|
||||||
{ name = main.lua, type = config/lua }
|
|
||||||
{ name = policy.lua, type = config/lua }
|
|
||||||
{ name = bluetooth.lua, type = config/lua }
|
|
||||||
]
|
|
||||||
|
|
||||||
These Lua "configuration" scripts were then looked up in the standard
|
|
||||||
configuration directories (``/usr/share/wireplumber``, ``/etc/wireplumber`` and
|
|
||||||
``~/.config/wireplumber``). The system also supported fragments of these scripts
|
|
||||||
to be placed in directories called ``main.lua.d``, ``policy.lua.d`` and
|
|
||||||
``bluetooth.lua.d`` respectively, in the same locations.
|
|
||||||
|
|
||||||
.. attention::
|
|
||||||
|
|
||||||
Starting with WirePlumber 0.5, Lua "configuration" files are **no longer
|
|
||||||
supported**.
|
|
||||||
|
|
||||||
If you attempt to start it with a ``wireplumber.conf`` that still
|
|
||||||
lists ``config/lua`` components in its ``wireplumber.components`` section, you
|
|
||||||
will see the following error message on the output:
|
|
||||||
|
|
||||||
Failed to load configuration: The configuration file at '...' is likely an
|
|
||||||
old WirePlumber 0.4 config and is not supported anymore. Try removing it.
|
|
||||||
|
|
||||||
As the message says, to resolve this you should remove the old
|
|
||||||
``wireplumber.conf`` file from the designated location. This should allow the
|
|
||||||
new WirePlumber to start using the default configuration that it ships with.
|
|
||||||
|
|
||||||
Lua configuration scripts
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
If you had custom Lua configuration scripts in the standard configuration
|
|
||||||
directories, such as *"main.lua.d"*, *"policy.lua.d"* or *"bluetooth.lua.d"*,
|
|
||||||
**you need to port them**.
|
|
||||||
|
|
||||||
Locations of files
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The first thing you need to know is that the new files should be placed in the
|
|
||||||
``~/.config/wireplumber/wireplumber.conf.d/`` directory instead of
|
|
||||||
``~/.config/wireplumber/main.lua.d/`` and such ...
|
|
||||||
|
|
||||||
In addition, since the new files are in the SPA-JSON format, they should have
|
|
||||||
the ``.conf`` extension instead of ``.lua``.
|
|
||||||
|
|
||||||
See also :ref:`config_locations`.
|
|
||||||
|
|
||||||
Porting device/node rules
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
One of the most common use-cases for these scripts was to set up properties
|
|
||||||
for devices and nodes using rules. Here is an example of an old rules script:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/main.lua.d/51-alsa-pro-audio.lua
|
|
||||||
|
|
||||||
local rule = {
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
{ "device.name", "matches", "alsa_card.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
["api.alsa.use-acp"] = false,
|
|
||||||
["device.profile"] = "pro-audio",
|
|
||||||
["api.acp.auto-profile"] = false,
|
|
||||||
["api.acp.auto-port"] = false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
table.insert(alsa_monitor.rules, rule)
|
|
||||||
|
|
||||||
This equivalent of this script in the new configuration format would look like
|
|
||||||
this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/51-alsa-pro-audio.conf
|
|
||||||
|
|
||||||
monitor.alsa.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
device.name = "~alsa_card.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
api.alsa.use-acp = false,
|
|
||||||
device.profile = "pro-audio"
|
|
||||||
api.acp.auto-profile = false
|
|
||||||
api.acp.auto-port = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Another example of Bluetooth node rules:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/bluetooth.lua.d/51-headphones.lua
|
|
||||||
|
|
||||||
local rule = {
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
{ "node.name", "equals", "bluez_output.02_11_45_A0_B3_27.a2dp-sink" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
["node.nick"] = "Headphones",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
table.insert(bluez_monitor.rules, rule)
|
|
||||||
|
|
||||||
This equivalent of this script in the new configuration format would look like:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/51-headphones.conf
|
|
||||||
|
|
||||||
monitor.bluez.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
node.name = "bluez_output.02_11_45_A0_B3_27.a2dp-sink"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
node.nick = "Headphones"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
See also :ref:`config_modifying_configuration_rules`.
|
|
||||||
|
|
||||||
Porting properties configuration
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
If you had configuration scripts that were setting properties in tables such
|
|
||||||
as ``alsa_monitor.properties`` or ``bluez_monitor.properties``, then in many
|
|
||||||
cases porting to the new format can be done as follows:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/bluetooth.lua.d/80-bluez-properties.lua
|
|
||||||
|
|
||||||
bluez_monitor.properties["bluez5.roles"] = "[ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]"
|
|
||||||
bluez_monitor.properties["bluez5.hfphsp-backend"] = "native"
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-bluez-properties.conf
|
|
||||||
|
|
||||||
monitor.bluez.properties = {
|
|
||||||
bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
|
|
||||||
bluez5.hfphsp-backend = "native"
|
|
||||||
}
|
|
||||||
|
|
||||||
See also :ref:`config_modifying_configuration_static`.
|
|
||||||
|
|
||||||
In a lot of cases, however, these properties have been promoted to become either
|
|
||||||
:ref:`Settings <config_modifying_configuration_settings>` or
|
|
||||||
:ref:`Features <config_modifying_configuration_features>`.
|
|
||||||
Here are some common examples:
|
|
||||||
|
|
||||||
Disabling the D-Bus device reservation API in the ALSA monitor:
|
|
||||||
|
|
||||||
* Old format:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/main.lua.d/80-disable-alsa-reserve.lua
|
|
||||||
|
|
||||||
alsa_monitor.properties["alsa.reserve"] = false
|
|
||||||
|
|
||||||
* New format:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-disable-alsa-reserve.conf
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
monitor.alsa.reserve-device = disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Disabling seat monitoring via logind in the BlueZ monitor:
|
|
||||||
|
|
||||||
* Old format:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/bluetooth.lua.d/80-disable-logind.lua
|
|
||||||
|
|
||||||
bluez_monitor.properties["with-logind"] = false
|
|
||||||
|
|
||||||
* New format:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-disable-logind.conf
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
monitor.bluez.seat-monitoring = disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
See also :ref:`config_modifying_configuration_features`.
|
|
||||||
|
|
||||||
Linking policy configuration (moved to settings and renamed):
|
|
||||||
|
|
||||||
* Old format:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/policy.lua.d/80-policy.lua
|
|
||||||
|
|
||||||
default_policy.policy = {
|
|
||||||
["move"] = false,
|
|
||||||
["follow"] = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
* New format:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-policy.conf
|
|
||||||
|
|
||||||
wireplumber.settings = {
|
|
||||||
linking.allow-moving-streams = false
|
|
||||||
linking.follow-default-target = false
|
|
||||||
}
|
|
||||||
|
|
||||||
See also :ref:`config_modifying_configuration_settings` and remember that
|
|
||||||
settings can also be changed at runtime via :command:`wpctl`.
|
|
||||||
|
|
||||||
Loading custom scripts
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
If you had custom Lua scripts that were loaded by the old configuration file,
|
|
||||||
you need to port the old ``load_script()`` commands into component descriptions.
|
|
||||||
|
|
||||||
For example, if you had a script that was loaded like this:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
:caption: ~/.config/wireplumber/main.lua.d/99-my-script.lua
|
|
||||||
|
|
||||||
load_script("my-script.lua")
|
|
||||||
|
|
||||||
You should now create a new component description in the configuration file
|
|
||||||
and also make sure to require it in the profile:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/99-my-script.conf
|
|
||||||
|
|
||||||
wireplumber.components = [
|
|
||||||
{
|
|
||||||
name = my-script.lua, type = script/lua
|
|
||||||
provides = custom.my-script
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
custom.my-script = required
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.. attention::
|
|
||||||
|
|
||||||
Another important thing to mention here is the location of custom scripts. In
|
|
||||||
0.4, scripts could be loaded in configuration locations such as
|
|
||||||
``~/.config/wireplumber/scripts/`` and ``/etc/wireplumber/scripts/``. In 0.5,
|
|
||||||
the XDG base directory specification for data files is honored, so the new
|
|
||||||
location for custom scripts is ``~/.local/share/wireplumber/scripts/`` and
|
|
||||||
anything else specified in ``$XDG_DATA_HOME`` and ``$XDG_DATA_DIRS``. See
|
|
||||||
:ref:`daemon_file_locations` for more information.
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
.. _config_modifying_configuration:
|
|
||||||
|
|
||||||
Modifying configuration
|
|
||||||
=======================
|
|
||||||
|
|
||||||
WirePlumber is a heavily modular daemon that depends on its configuration
|
|
||||||
file to operate. If you were to start WirePlumber with an empty configuration
|
|
||||||
file, it would fail to start. This is why the default configuration file is
|
|
||||||
installed in the system-wide application data directory, which prevents it from
|
|
||||||
being modified by the user.
|
|
||||||
|
|
||||||
It is technically possible, if you wish, to copy the default configuration
|
|
||||||
file in one of the other :ref:`configuration search locations <config_locations>`
|
|
||||||
and modify it. However, this is **not recommended**, as it may lead to issues
|
|
||||||
when upgrading WirePlumber.
|
|
||||||
|
|
||||||
In the :ref:`Configuration file <config_conf_file>` section, we saw that
|
|
||||||
configuration files support fragments, which allow you to override or extend the
|
|
||||||
default configuration. This is the recommended way to modify the configuration.
|
|
||||||
|
|
||||||
Working with fragments
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
The easiest way to add :ref:`fragments <config_conf_file_fragments>` to
|
|
||||||
modify the default configuration is to create a directory called
|
|
||||||
``~/.config/wireplumber/wireplumber.conf.d`` and place your fragments there.
|
|
||||||
|
|
||||||
All fragment files need to have the ``.conf`` extension and must be valid
|
|
||||||
SPA-JSON files. The fragments are loaded in alphanumerical order, so you can
|
|
||||||
control the order in which they are loaded by naming them accordingly. It is
|
|
||||||
recommended to use a numeric prefix for the file names, e.g.
|
|
||||||
``10-my-fragment.conf``, ``20-my-other-fragment.conf``, etc., so that you can
|
|
||||||
easily control the order in which they are loaded.
|
|
||||||
|
|
||||||
.. _config_modifying_configuration_features:
|
|
||||||
|
|
||||||
Customizing the loaded features
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
As seen in the :ref:`Components & Profiles <config_components_and_profiles>`
|
|
||||||
section, the list of components that are loaded can be customized by enabling or
|
|
||||||
disabling :ref:`well-known features <config_features>` in the profile that is
|
|
||||||
in use by WirePlumber.
|
|
||||||
|
|
||||||
The default profile of WirePlumber is called ``main``, so a fragment that
|
|
||||||
enables or disables a specific feature in the default configuration should look
|
|
||||||
like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
some.feature.name = disabled
|
|
||||||
some.other.feature.name = required
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Remember that features can be ``required``, ``optional`` or ``disabled``. See
|
|
||||||
the :ref:`Components & Profiles <config_components_and_profiles>` for details.
|
|
||||||
|
|
||||||
.. _config_modifying_configuration_settings:
|
|
||||||
|
|
||||||
Modifying dynamic options ("settings")
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
As seen in the :ref:`Configuration option types <config_configuration_option_types>`
|
|
||||||
section, WirePlumber components can be partly configured with dynamic options
|
|
||||||
(referred to as "settings"). These settings can either be modified permanently
|
|
||||||
in the configuration file, or they can be modified at runtime using the
|
|
||||||
``wpctl`` command-line tool.
|
|
||||||
|
|
||||||
To modify a setting in the configuration file, you can use a fragment like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.settings = {
|
|
||||||
some.setting.name = value
|
|
||||||
}
|
|
||||||
|
|
||||||
For example, setting the ``device.routes.default-sink-volume`` setting to
|
|
||||||
``0.5`` can be done like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.settings = {
|
|
||||||
device.routes.default-sink-volume = 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Since the configuration file is only read at startup, this will only take
|
|
||||||
effect after restarting WirePlumber.
|
|
||||||
|
|
||||||
If you would prefer to change the setting at runtime, you can use ``wpctl`` as
|
|
||||||
follows:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings device.routes.default-sink-volume 0.5
|
|
||||||
Updated setting 'device.routes.default-sink-volume' to: 0.5
|
|
||||||
|
|
||||||
The above command changes the setting immediately, but for the current
|
|
||||||
WirePlumber instance only. If you want the setting to be applied every time
|
|
||||||
WirePlumber is started, you may also use the ``--save`` option:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings --save device.routes.default-sink-volume 0.5
|
|
||||||
Updated and saved setting 'device.routes.default-sink-volume' to: 0.5
|
|
||||||
|
|
||||||
This will save the setting persistently in WirePlumber's state storage.
|
|
||||||
Even though it is not in the configuration file, this saved value will be
|
|
||||||
applied automatically when WirePlumber is started.
|
|
||||||
|
|
||||||
.. attention::
|
|
||||||
|
|
||||||
When a setting's value is saved, it will override the value from the
|
|
||||||
configuration file. Changing the value in the configuration file will
|
|
||||||
have no effect until the saved value is removed. Use the ``--delete``
|
|
||||||
switch in ``wpctl`` to remove a saved value (see below).
|
|
||||||
|
|
||||||
With ``wpctl``, it is also possible to restore a setting to its default value
|
|
||||||
(taken from the schema), by using the ``--reset`` option. For example, to reset
|
|
||||||
the ``device.routes.default-sink-volume`` setting, the following command can be
|
|
||||||
used:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings --reset device.routes.default-sink-volume
|
|
||||||
Reset setting 'device.routes.default-sink-volume' successfully
|
|
||||||
$ wpctl settings device.routes.default-sink-volume
|
|
||||||
Value: 0.064 (Saved: 0.5)
|
|
||||||
|
|
||||||
Note that the ``--reset`` option will only reset the setting to its default
|
|
||||||
value, but it will not remove the saved value from the state file. If you want
|
|
||||||
to remove the saved value, you can use the ``--delete`` option:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wpctl settings --delete device.routes.default-sink-volume
|
|
||||||
Deleted setting 'device.routes.default-sink-volume' successfully
|
|
||||||
$ wpctl settings device.routes.default-sink-volume
|
|
||||||
Value: 0.064
|
|
||||||
|
|
||||||
A list of all the available settings can be found in the :ref:`config_settings`
|
|
||||||
section.
|
|
||||||
|
|
||||||
.. _config_modifying_configuration_static:
|
|
||||||
|
|
||||||
Modifying static options
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
Static options always live in their own section of the configuration file.
|
|
||||||
Sections can be of two types: either a JSON object or a JSON array.
|
|
||||||
|
|
||||||
When dealing with a **JSON object**, you can add or modify a key-value pair by
|
|
||||||
creating a fragment like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.some-section = {
|
|
||||||
some.option = new_value
|
|
||||||
}
|
|
||||||
|
|
||||||
This is similar to what we have seen also above for modifying profile features
|
|
||||||
and settings (because both are JSON objects).
|
|
||||||
|
|
||||||
When dealing with a **JSON array**, any values that you define in a fragment
|
|
||||||
will be appended to the array. For example, to add a new rule to the
|
|
||||||
``monitor.alsa.rules`` array, you can create a fragment like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.alsa.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
device.name = "~alsa_card.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
api.alsa.use-ucm = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
This will add a new rule to the ``monitor.alsa.rules`` array, which will
|
|
||||||
be evaluated **after** all other rules that were parsed before. This is where
|
|
||||||
the order in which fragments are loaded actually matters.
|
|
||||||
|
|
||||||
If you don't want to append a new rule, but rather override the entire array
|
|
||||||
with a new one, you can do so by using the ``override.`` prefix on the array
|
|
||||||
name:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
override.monitor.alsa.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
device.name = "~alsa_card.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
api.alsa.use-ucm = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
This will now replace the entire ``monitor.alsa.rules`` array with this new one.
|
|
||||||
|
|
||||||
.. attention::
|
|
||||||
|
|
||||||
If you want to remove a rule from the array, you will need to override the
|
|
||||||
whole array with a new one that does not contain the rule you want to remove.
|
|
||||||
There is no way to remove a specific element from an array using fragments.
|
|
||||||
|
|
||||||
Another thing worth remembering here is that this behavior of appending values
|
|
||||||
to arrays also works in arrays that are nested inside other arrays or objects.
|
|
||||||
For example, consider this fragment:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.bluez.properties = {
|
|
||||||
bluez5.codecs = [ sbc_xq aac ldac ]
|
|
||||||
}
|
|
||||||
|
|
||||||
If this is the first time that the ``bluez5.codecs`` array is being defined, it
|
|
||||||
will be created with the given values. If it already exists, the given values
|
|
||||||
will be appended to the existing array. If you want to make sure that this
|
|
||||||
fragment will override the existing array, you need to use the ``override.``
|
|
||||||
prefix on the array name:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
monitor.bluez.properties = {
|
|
||||||
override.bluez5.codecs = [ sbc_xq aac ldac ]
|
|
||||||
}
|
|
||||||
|
|
||||||
The ``override.`` prefix may also be used in JSON object keys, to override the
|
|
||||||
entire object with a new one. For example, to override the entire
|
|
||||||
``monitor.bluez.properties`` object, you can use a fragment like this:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
override.monitor.bluez.properties = {
|
|
||||||
bluez5.codecs = [ sbc_xq aac ldac ]
|
|
||||||
}
|
|
||||||
|
|
||||||
Here, the entire ``monitor.bluez.properties`` object will be replaced with the
|
|
||||||
new one, and all previous key-value pairs configured will be discarded. This
|
|
||||||
also means that the ``bluez5.codecs`` array will be replaced with the new one
|
|
||||||
and does not require the ``override.`` prefix.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Even though WirePlumber uses PipeWire's syntax for configuration files, the
|
|
||||||
``override.`` prefix is a WirePlumber extension and does not work in
|
|
||||||
PipeWire.
|
|
||||||
|
|
||||||
.. _config_modifying_configuration_rules:
|
|
||||||
|
|
||||||
Working with rules
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Some of the static option sections in the configuration file are used to define
|
|
||||||
rules that are evaluated by WirePlumber at runtime. These rules are typically
|
|
||||||
used to match objects and perform actions on them. For example, the
|
|
||||||
``monitor.alsa.rules`` section is used to define rules that are evaluated by
|
|
||||||
the ALSA monitor to match ALSA devices and update their properties.
|
|
||||||
|
|
||||||
The syntax of these rules is the same as the syntax of
|
|
||||||
`PipeWire's rules <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire#rules>`_.
|
|
||||||
|
|
||||||
A rule is always a JSON object with two keys: ``matches`` and ``actions``. The
|
|
||||||
``matches`` key is used to define the conditions that need to be met for the
|
|
||||||
rule to be evaluated as true, and the ``actions`` key is used to define the
|
|
||||||
actions that are performed when the rule is evaluated as true.
|
|
||||||
|
|
||||||
The ``matches`` key is always a JSON array of objects, where each object
|
|
||||||
defines a condition that needs to be met. Each condition is a list of key-value
|
|
||||||
pairs, where the key is the name of the property that is being matched, and the
|
|
||||||
value is the value that the property needs to have. Within a condition, all
|
|
||||||
the key-value pairs are combined with a logical AND, and all the conditions in
|
|
||||||
the ``matches`` array are combined with a logical OR.
|
|
||||||
|
|
||||||
The ``actions`` key is always a JSON object, where each key-value pair defines
|
|
||||||
an action that is performed when the rule is evaluated as true. The action
|
|
||||||
name is specific to the rule and is defined by the rule's documentation, but
|
|
||||||
most frequently you will see the ``update-props`` action, which is used to
|
|
||||||
update the properties of the matched object.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
some.theoretical.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
object.name = "my_object"
|
|
||||||
object.profile.name = "my_profile"
|
|
||||||
}
|
|
||||||
{
|
|
||||||
object.name = "other_object"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
actions = {
|
|
||||||
update-props = {
|
|
||||||
object.tag = "matched_by_my_rule"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
This rule is equivalent to the following expression:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
if (properties["object.name"] == "my_object" and properties["object.profile.name"] == "my_profile") or (properties["object.name"] == "other_object"):
|
|
||||||
properties["object.tag"] = "matched_by_my_rule"
|
|
||||||
|
|
||||||
In the ``matches`` array, it is also possible to use regular expressions to match
|
|
||||||
property values. For example, to match all nodes with a name that starts with
|
|
||||||
``my_``, you can use the following condition:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
node.name = "~my_.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
The ``~`` character signifies that the value is a regular expression. The exact
|
|
||||||
syntax of the regular expressions is the POSIX extended regex syntax, as
|
|
||||||
described in the `regex (7)` man page.
|
|
||||||
|
|
||||||
In addition to regular expressions, you may also use the ``!`` character to
|
|
||||||
negate a condition. For example, to match all nodes with a name that does not
|
|
||||||
start with ``my_``, you can use the following condition:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
node.name = "!~my_.*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
The ``!`` character can be used with or without a regular expression. For
|
|
||||||
example, to match all nodes with a name that is not equal to ``my_node``,
|
|
||||||
you can use the following condition:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
matches = [
|
|
||||||
{
|
|
||||||
node.name = "!my_node"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
45
docs/rst/daemon/configuration/multi_instance.rst
Normal file
45
docs/rst/daemon/configuration/multi_instance.rst
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
.. _config_multi_instance:
|
||||||
|
|
||||||
|
Running multiple instances
|
||||||
|
==========================
|
||||||
|
|
||||||
|
WirePlumber has the ability to run either as a single instance daemon or as
|
||||||
|
multiple instances, meaning that there can be multiple processes, each one
|
||||||
|
doing a different task.
|
||||||
|
|
||||||
|
In the default configuration, both setups are supported. The default is to run
|
||||||
|
in single-instance mode.
|
||||||
|
|
||||||
|
In single-instance mode, WirePlumber reads ``wireplumber.conf``, which is the
|
||||||
|
default configuration file, and from there it loads ``main.lua``, ``policy.lua``
|
||||||
|
and ``bluetooth.lua``, which are lua configuration files (deployed as directories)
|
||||||
|
that enable all the relevant functionality.
|
||||||
|
|
||||||
|
In multi-instance mode, WirePlumber is meant to be started with the
|
||||||
|
``--config-file`` command line option 3 times:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ wireplumber --config-file=main.conf
|
||||||
|
$ wireplumber --config-file=policy.conf
|
||||||
|
$ wireplumber --config-file=bluetooth.conf
|
||||||
|
|
||||||
|
That loads one process which reads ``main.conf``, which then loads ``main.lua``
|
||||||
|
and enables core functionality. Then another process that reads ``policy.conf``,
|
||||||
|
which then loads ``policy.lua`` and enables policy functionality... and so on.
|
||||||
|
|
||||||
|
To make this easier to work with, a template systemd unit is provided, which is
|
||||||
|
meant to be started with the name of the main configuration file as a
|
||||||
|
template argument:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ systemctl --user disable wireplumber # disable the single instance
|
||||||
|
|
||||||
|
$ systemctl --user enable wireplumber@main
|
||||||
|
$ systemctl --user enable wireplumber@policy
|
||||||
|
$ systemctl --user enable wireplumber@bluetooth
|
||||||
|
|
||||||
|
It is obviously possible to start as many instances as desired, with manually
|
||||||
|
crafted configuration files, as long as it is ensured that these instances
|
||||||
|
serve a different purpose and they do not conflict with each other.
|
||||||
347
docs/rst/daemon/configuration/policy.rst
Normal file
347
docs/rst/daemon/configuration/policy.rst
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
.. _config_policy:
|
||||||
|
|
||||||
|
Policy Configuration
|
||||||
|
====================
|
||||||
|
|
||||||
|
wireplumber.conf.d/policy.conf
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This file contains generic default policy properties that can be configured.
|
||||||
|
|
||||||
|
* *Settings*
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
wireplumber.properties = {
|
||||||
|
default-policy-move = true
|
||||||
|
}
|
||||||
|
|
||||||
|
The above example will set the ``move`` policy property to ``true``.
|
||||||
|
|
||||||
|
The list of supported properties are:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
default-policy-move = true
|
||||||
|
|
||||||
|
Moves session items when metadata ``target.node`` changes.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
default-policy-follow = true
|
||||||
|
|
||||||
|
Moves session items to the default device when it has changed.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
default-policy-audio.no-dsp = false
|
||||||
|
|
||||||
|
Set to ``true`` to disable channel splitting & merging on nodes and enable
|
||||||
|
passthrough of audio in the same format as the format of the device. Note that
|
||||||
|
this breaks JACK support; it is generally not recommended.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
default-policy-duck.level = 0.3
|
||||||
|
|
||||||
|
How much to lower the volume of lower priority streams when ducking. Note that
|
||||||
|
this is a linear volume modifier (not cubic as in PulseAudio).
|
||||||
|
|
||||||
|
|
||||||
|
Filters
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
* *Introduction*
|
||||||
|
|
||||||
|
A pair of nodes will be considered filter nodes by wireplumber if they have the
|
||||||
|
"node.link-group" property set to a common value. This propery is always set by
|
||||||
|
PipeWire when creating filter nodes if they are defined in the PipeWire's
|
||||||
|
configuration file. The pair of nodes always consist of a stream node, and a
|
||||||
|
main node. When using the filter nodes, the main node acts as a virtual device,
|
||||||
|
where the audio is sent or captured to/from; and the stream node acts as a
|
||||||
|
virtual stream, where the audio is sent or received to/from the next node in the
|
||||||
|
graph.
|
||||||
|
|
||||||
|
For example, the media class of the nodes for a input filter would be:
|
||||||
|
|
||||||
|
- main node: Audio/Sink
|
||||||
|
- stream node: Stream/Output/Audio
|
||||||
|
|
||||||
|
And, if this filter is used between an application stream, and the default audio
|
||||||
|
device, the graph would look like this:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
application stream node -> filter main node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
filter stream node -> default device node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
|
||||||
|
On the other hand, the media class of the nodes for an output filter would be:
|
||||||
|
|
||||||
|
- main node: Audio/Source
|
||||||
|
- stream node: Stream/Input/Audio
|
||||||
|
|
||||||
|
And the same logic is applied if they are used, but in the opposite direction.
|
||||||
|
This is how the graph would look like if an application wants to capture audio
|
||||||
|
from a device that uses an input filter.
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
application stream node <- filter main node
|
||||||
|
(Stream/Input/Audio) (Audio/Source)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
filter stream node <- default device node
|
||||||
|
(Stream/Input/Audio) (Audio/Source)
|
||||||
|
|
||||||
|
Finally, if multiple filters have the same direction, they can also be chained
|
||||||
|
together so that the audio of a filter is sent to the input of the next filter.
|
||||||
|
|
||||||
|
Example of existing filters in PipeWire are echo-cancel, filter-chain and
|
||||||
|
loopback nodes.
|
||||||
|
|
||||||
|
The next section will describe how we can define filter properties so that they
|
||||||
|
are automatically linked by the wirepluber policy in any way we want.
|
||||||
|
|
||||||
|
|
||||||
|
* *Filter properties*
|
||||||
|
|
||||||
|
Currently, if a filter node is created, wireplumber will check the following
|
||||||
|
optional node properties on the main node:
|
||||||
|
|
||||||
|
- filter.smart:
|
||||||
|
Boolean indicating whether smart policy will be used in the filter nodes or
|
||||||
|
not. This is disabled by default, therefore filter nodes will be treated as
|
||||||
|
regular nodes, without applying any kid of extra logic. On the other hand, if
|
||||||
|
this property is set to true, automatic (smart) filter policy will be used
|
||||||
|
when linking filters. The properties below will instruct the smart policy how
|
||||||
|
to link the filters automatically.
|
||||||
|
|
||||||
|
- filter.smart.name:
|
||||||
|
The unique name of the filter. WirePlumber will use the "node.link-group"
|
||||||
|
property as filter name if this property is not set.
|
||||||
|
|
||||||
|
- filter.smart.disabled:
|
||||||
|
Boolean indicating whether the filter should be disabled at all or not. A
|
||||||
|
disabled filter will never be used in any circumstances. If the property is
|
||||||
|
not set, wireplumber will consider the filter not disabled by default.
|
||||||
|
|
||||||
|
- filter.smart.target:
|
||||||
|
A JSON object that defines the matching properties of the filter's target node.
|
||||||
|
A filter target can never be another filter node (wireplumber will ignore it),
|
||||||
|
and must always be a device node. If this property is not set, WirePlumber will
|
||||||
|
use the default node as target.
|
||||||
|
|
||||||
|
- filter.smart.before:
|
||||||
|
A JSON array with the filters names that are supposed to be used before this
|
||||||
|
filter. If not set, wireplumber will link the filters by order of creation.
|
||||||
|
|
||||||
|
- filter.smart.after:
|
||||||
|
A JSON array with the filters names that are supposed to be used after this
|
||||||
|
filter. If not set, wireplumber will link the filters by order of creation.
|
||||||
|
|
||||||
|
Note that these properties must be set in the filter's main node, not the
|
||||||
|
filter's stream node.
|
||||||
|
|
||||||
|
As an example, we will describe here how to create 2 loopback filters in the
|
||||||
|
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
|
||||||
|
linked with the default audio device, and use loopback-2 filter as the last
|
||||||
|
filter in the chain.
|
||||||
|
|
||||||
|
The PipeWire configuration files for the 2 filters should be like this:
|
||||||
|
|
||||||
|
- /usr/share/pipewire/pipewire.conf.d/loopback-1.conf:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
{ name = libpipewire-module-loopback
|
||||||
|
args = {
|
||||||
|
node.name = loopback-1-sink
|
||||||
|
node.description = "Loopback 1 Sink"
|
||||||
|
capture.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
media.class = Audio/Sink
|
||||||
|
filter.smart.name = loopback-1
|
||||||
|
filter.smart.disabled = false
|
||||||
|
filter.smart.before = [ loopback-2 ]
|
||||||
|
}
|
||||||
|
playback.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
node.passive = true
|
||||||
|
node.dont-remix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
- /usr/share/pipewire/pipewire.conf.d/loopback-2.conf:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
{ name = libpipewire-module-loopback
|
||||||
|
args = {
|
||||||
|
node.name = loopback-2-sink
|
||||||
|
node.description = "Loopback 2 Sink"
|
||||||
|
capture.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
media.class = Audio/Sink
|
||||||
|
filter.smart.name = loopback-2
|
||||||
|
filter.smart.disabled = false
|
||||||
|
}
|
||||||
|
playback.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
node.passive = true
|
||||||
|
node.dont-remix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Finally, if we restart PipeWire and WirePlumber to apply the configuration
|
||||||
|
changes, and play a test.wave audio file with paplay to see if wireplumber links
|
||||||
|
the filter nodes properly, the graph should look like this:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
paplay node -> loopback-1 main node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
loopback-1 stream node -> loopback-1 main node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
loopback-2 stream node -> default device node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
|
||||||
|
If we remove `filter.smart.before = [ loopback-2 ]` property from the loopback-1
|
||||||
|
filter, and add a `filter.smart.before = [ loopback-1 ]` property in the loopback-2
|
||||||
|
filter configuration file. WirePlumber should link the loopback-1 filter as the last
|
||||||
|
filter in the chain, like this:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
paplay node -> loopback-2 main node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
loopback-2 stream node -> loopback-1 main node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
loopback-1 stream node -> default device node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
|
||||||
|
On the other hand, the filters can have different targets. For example, we can
|
||||||
|
define the filters like this:
|
||||||
|
|
||||||
|
- `/usr/share/pipewire/pipewire.conf.d/loopback-1.conf`:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
{ name = libpipewire-module-loopback
|
||||||
|
args = {
|
||||||
|
node.name = loopback-1-sink
|
||||||
|
node.description = "Loopback 1 Sink"
|
||||||
|
capture.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
media.class = Audio/Sink
|
||||||
|
filter.smart.name = loopback-1
|
||||||
|
filter.smart.disabled = false
|
||||||
|
filter.smart.before = [ loopback-2 ]
|
||||||
|
filter.smart.target = { node.name = "not-default-audio-device-name" }
|
||||||
|
}
|
||||||
|
playback.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
node.passive = true
|
||||||
|
node.dont-remix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
- `/usr/share/pipewire/pipewire.conf.d/loopback-2.conf`:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
{ name = libpipewire-module-loopback
|
||||||
|
args = {
|
||||||
|
node.name = loopback-2-sink
|
||||||
|
node.description = "Loopback 2 Sink"
|
||||||
|
capture.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
media.class = Audio/Sink
|
||||||
|
filter.smart.name = loopback-2
|
||||||
|
filter.smart.disabled = false
|
||||||
|
}
|
||||||
|
playback.props = {
|
||||||
|
audio.position = [ FL FR ]
|
||||||
|
node.passive = true
|
||||||
|
node.dont-remix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
If this is the case, WirePlumber will link the filters like this when using
|
||||||
|
paplay:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
paplay node -> loopback-2 main node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
loopback-2 stream node -> default device node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
loopback-1 stream node -> not-default-audio-device-name device node
|
||||||
|
(Stream/Output/Audio) (Audio/Sink)
|
||||||
|
|
||||||
|
The loopback-1 main node will only be used if an application wants to play audio
|
||||||
|
on the device node with node name "not-default-audio-device-name".
|
||||||
|
|
||||||
|
|
||||||
|
* *Filters metadata*
|
||||||
|
|
||||||
|
Similar to the default metadata, it is also possible to override the filter
|
||||||
|
properties by using the "filters" metadata. This allow users to change the filters
|
||||||
|
policy at runtime.
|
||||||
|
|
||||||
|
For example, if loopback-1 main node Id is `40`, we can disable the filter by
|
||||||
|
setting its "filter.smart.disabled" metadata key to true using the `pw-metadata`
|
||||||
|
tool:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
$ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON
|
||||||
|
|
||||||
|
We can also change the target of a filter at runtime:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
$ pw-metadata -n filters 40 "filter.smart.target" { node.name = "new-target-node-name" } Spa:String:JSON
|
||||||
|
|
||||||
|
Every time a key in the filters metadata changes, all filters are unlinked and
|
||||||
|
re-linked properly by the policy.
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
.. _config_settings:
|
.. _config_settings:
|
||||||
|
|
||||||
Well-known settings
|
WirePlumber Settings
|
||||||
===================
|
====================
|
||||||
|
|
||||||
This section describes the settings that can be configured on WirePlumber.
|
This section describes the settings that can be configured on WirePlumber.
|
||||||
|
|
||||||
|
|
@ -9,220 +9,4 @@ Settings can be either configured statically in the configuration file
|
||||||
by setting them under the ``wireplumber.settings`` section, or they can be
|
by setting them under the ``wireplumber.settings`` section, or they can be
|
||||||
configured dynamically at runtime by using metadata.
|
configured dynamically at runtime by using metadata.
|
||||||
|
|
||||||
For more information on what "settings" are and how they work, refer to the
|
.. include:: ../../../../src/scripts/lib/SETTINGS.rst
|
||||||
previous section: :ref:`config_configuration_option_types`.
|
|
||||||
|
|
||||||
.. describe:: device.restore-profile
|
|
||||||
|
|
||||||
When a device profile is changed manually (e.g. via pavucontrol), WirePlumber
|
|
||||||
stores the selected profile and restores it when the device appears again
|
|
||||||
(e.g. after a reboot). If this setting is disabled, WirePlumber will always
|
|
||||||
pick the best profile for the device based on profile priorities and
|
|
||||||
availability (or custom rules, if any).
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
||||||
.. describe:: device.restore-routes
|
|
||||||
|
|
||||||
When a device route is changed manually (e.g. via pavucontrol), WirePlumber
|
|
||||||
stores the selected route and restores it when the same profile is
|
|
||||||
selected for this device. If this setting is disabled, WirePlumber will
|
|
||||||
always pick the best route for this device profile based on route priorities
|
|
||||||
and availability (or custom rules, if any).
|
|
||||||
|
|
||||||
This setting also enables WirePlumber to restore properties of the device
|
|
||||||
route when the route is restored. This includes the volume levels of sources
|
|
||||||
and sinks, as well as the IEC958 codecs selected (for routes that support
|
|
||||||
encoded streams, such as HDMI).
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
||||||
.. describe:: device.routes.default-sink-volume
|
|
||||||
|
|
||||||
This option allows to set the default volume for sinks that are part of a
|
|
||||||
device route (e.g. ALSA PCM sinks). This is used when the route is restored
|
|
||||||
and the sink does not have a previously stored volume.
|
|
||||||
|
|
||||||
It is possible to override the value on a per-device basis with a property
|
|
||||||
(*not* a setting, so this would go into a configuration file) on the device
|
|
||||||
named ``device.routes.default-sink-volume``.
|
|
||||||
|
|
||||||
:Default value: ``0.4 ^ 3`` (40% on the cubic scale)
|
|
||||||
|
|
||||||
.. describe:: device.routes.default-source-volume
|
|
||||||
|
|
||||||
This option allows to set the default volume for sources that are part of a
|
|
||||||
device route (e.g. ALSA PCM sources). This is used when the route is restored
|
|
||||||
and the source does not have a previously stored volume.
|
|
||||||
|
|
||||||
It is possible to override the value on a per-device basis with a property
|
|
||||||
(*not* a setting, so this would go into a configuration file) on the device
|
|
||||||
named ``device.routes.default-source-volume``.
|
|
||||||
|
|
||||||
:Default value: ``1.0`` (100%)
|
|
||||||
|
|
||||||
.. describe:: linking.allow-moving-streams
|
|
||||||
|
|
||||||
This option allows moving streams by overriding their target via metadata.
|
|
||||||
When enabled, WirePlumber monitors the "default" metadata for changes in the
|
|
||||||
``target.object`` key of streams and if this key is set to a valid node name
|
|
||||||
(``node.name``) or serial (``object.serial``), the stream is moved to that
|
|
||||||
target node.
|
|
||||||
|
|
||||||
This is used by applications such as pavucontrol and is recommended for
|
|
||||||
compatibility with PulseAudio.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
On the metadata, the ``target.node`` key is also supported for
|
|
||||||
compatibility with older versions of PipeWire, but it is deprecated.
|
|
||||||
Please use the ``target.object`` key instead.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
:See also: ``node.stream.restore-target``
|
|
||||||
|
|
||||||
.. describe:: linking.follow-default-target
|
|
||||||
|
|
||||||
When a stream was started with the ``target.object`` property, WirePlumber
|
|
||||||
normally links that stream to that target node and ignores the "default"
|
|
||||||
target for that direction. However, if this option is enabled, WirePlumber
|
|
||||||
will check if the designated target node *is* the "default" target and if so,
|
|
||||||
it will act as if the stream did not have that property.
|
|
||||||
|
|
||||||
In practice, this means that if the "default" target changes at runtime,
|
|
||||||
the stream will be moved to the new "default" target.
|
|
||||||
|
|
||||||
This is what Pulseaudio does and is implemented here for compatibility
|
|
||||||
with some applications that do start with a ``target.object`` property
|
|
||||||
set to the "default" target and expect the stream to be moved when the
|
|
||||||
"default" target changes.
|
|
||||||
|
|
||||||
Note that this logic is only applied on client (i.e. application) streams
|
|
||||||
and *not* on filters.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
||||||
.. describe:: linking.pause-playback
|
|
||||||
|
|
||||||
When an audio sink is removed, pause media players that have streams
|
|
||||||
playing to it. Pausing is done via MPRIS interface.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
||||||
.. describe:: node.features.audio.no-dsp
|
|
||||||
|
|
||||||
When this option is set to ``true``, audio nodes will not be configured
|
|
||||||
in dsp mode, meaning that their channels will *not* be split into separate
|
|
||||||
ports and that the audio data will *not* be converted to the float 32 format
|
|
||||||
(F32P). Instead, devices will be configured in passthrough mode and streams
|
|
||||||
will be configured in convert mode, so that their audio data is converted
|
|
||||||
directly to the format that the device is expecting.
|
|
||||||
|
|
||||||
This may be useful if you are trying to minimize audio processing for an
|
|
||||||
embedded system, but it is not recommended for general use.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
This option **will break** compatibility with JACK applications
|
|
||||||
and may also break certain patchbay applications. Do not enable, unless
|
|
||||||
you understand what you are doing.
|
|
||||||
|
|
||||||
:Default value: ``false``
|
|
||||||
|
|
||||||
.. describe:: node.features.audio.monitor-ports
|
|
||||||
|
|
||||||
This enables the creation of "monitor" ports for audio nodes. Monitor ports
|
|
||||||
are created on nodes that have input ports (i.e. sinks and capture streams)
|
|
||||||
and allow monitoring of the audio data that is being sent to the node.
|
|
||||||
|
|
||||||
This is mostly used by monitoring applications, such as pavucontrol.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
||||||
.. describe:: node.features.audio.control-port
|
|
||||||
|
|
||||||
This enables the creation of a "control" port for audio nodes. Control ports
|
|
||||||
allow sending MIDI data to the node, allowing for control of certain node's
|
|
||||||
parameters (such as volume) via external controllers.
|
|
||||||
|
|
||||||
:Default value: ``false``
|
|
||||||
|
|
||||||
.. describe:: node.stream.restore-props
|
|
||||||
|
|
||||||
WirePlumber stores stream parameters such as volume and mute status for each
|
|
||||||
client (i.e. application) stream. If this setting is enabled, WirePlumber
|
|
||||||
will restore the previously stored stream parameters when the stream is
|
|
||||||
activated. If it is disabled, stream parameters will be initialized to their
|
|
||||||
default values.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
||||||
.. describe:: node.stream.restore-target
|
|
||||||
|
|
||||||
When a client (i.e. application) stream is manually moved to a different
|
|
||||||
target node (e.g. via pavucontrol), the target node is stored by WirePlumber.
|
|
||||||
If this setting is enabled, WirePlumber will restore the previously stored
|
|
||||||
target node when the stream is activated.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This does not restore manual links made by patchbay applications. This
|
|
||||||
is only meant to restore the ``target.object`` property in the "default"
|
|
||||||
metadata, which is manipulated by applications such as pavucontrol when
|
|
||||||
a stream is moved to a different target.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
:See also: ``linking.allow-moving-streams``
|
|
||||||
|
|
||||||
.. describe:: node.stream.default-playback-volume
|
|
||||||
|
|
||||||
The default volume for playback streams to be applied when the stream is
|
|
||||||
activated. This is only applied when ``node.stream.restore-props`` is
|
|
||||||
``true`` and the stream does not have a previously stored volume.
|
|
||||||
|
|
||||||
:Default value: ``1.0``
|
|
||||||
:Range: ``0.0`` to ``1.0``
|
|
||||||
|
|
||||||
.. describe:: node.stream.default-capture-volume
|
|
||||||
|
|
||||||
The default volume for capture streams to be applied when the stream is
|
|
||||||
activated. This is only applied when ``node.stream.restore-props`` is
|
|
||||||
``true`` and the stream does not have a previously stored volume.
|
|
||||||
|
|
||||||
:Default value: ``1.0``
|
|
||||||
:Range: ``0.0`` to ``1.0``
|
|
||||||
|
|
||||||
.. describe:: node.filter.forward-format
|
|
||||||
|
|
||||||
When a "filter" pair of nodes (such as echo-cancel or filter-chain) is
|
|
||||||
linked to a device node that has a different channel map than the filter
|
|
||||||
nodes, this option allows the channel map of the filter nodes to be changed
|
|
||||||
to match the channel map of the device node. The change is applied to both
|
|
||||||
ends of the "filter", so that any streams linked to the filter are also
|
|
||||||
reconfigured to match the target channel map.
|
|
||||||
|
|
||||||
This is useful, for instance, to make sure that an application will be
|
|
||||||
properly configured to output surround audio to a surround device, even
|
|
||||||
when going through a filter that was not explicitly configured to have
|
|
||||||
a surround channel map.
|
|
||||||
|
|
||||||
:Default value: ``false``
|
|
||||||
|
|
||||||
.. describe:: node.restore-default-targets
|
|
||||||
|
|
||||||
This setting enables WirePlumber to store and restore the "default" source
|
|
||||||
and sink targets of the graph. In PulseAudio terminology, this is also known
|
|
||||||
as the "fallback" source and sink.
|
|
||||||
|
|
||||||
When this setting is enabled, WirePlumber will store the "default" source
|
|
||||||
and sink targets when they are changed manually (e.g. via pavucontrol) and
|
|
||||||
restore them when the available nodes change or after a reload/restart.
|
|
||||||
It will also store a history of past selected "default" targets and restore
|
|
||||||
previously selected ones if the currently selected are not available.
|
|
||||||
|
|
||||||
If this is disabled, WirePlumber will pick the best available source
|
|
||||||
and sink targets based on their priorities, but it will also respect
|
|
||||||
manual user selections that are done at runtime - it will just not remember
|
|
||||||
them so that it can restore them at a later time.
|
|
||||||
|
|
||||||
:Default value: ``true``
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ Dependencies
|
||||||
|
|
||||||
In order to compile WirePlumber you will need:
|
In order to compile WirePlumber you will need:
|
||||||
|
|
||||||
* GLib >= 2.68
|
* GLib >= 2.62
|
||||||
* PipeWire >= 1.0
|
* PipeWire 0.3 (>= 0.3.43)
|
||||||
* Lua 5.3 or 5.4
|
* Lua 5.3 or 5.4
|
||||||
|
|
||||||
Lua is optional in the sense that if it is not found in the system, a bundled
|
Lua is optional in the sense that if it is not found in the system, a bundled
|
||||||
|
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
.. _daemon_file_locations:
|
|
||||||
|
|
||||||
Locations of WirePlumber's files
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. _config_locations:
|
|
||||||
|
|
||||||
Location of configuration files
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
WirePlumber's default locations of its configuration files are the following,
|
|
||||||
in order of priority:
|
|
||||||
|
|
||||||
1. ``$XDG_CONFIG_HOME/wireplumber``
|
|
||||||
2. ``$XDG_CONFIG_DIRS/wireplumber``
|
|
||||||
3. ``$sysconfdir/wireplumber``
|
|
||||||
4. ``$XDG_DATA_DIRS/wireplumber``
|
|
||||||
5. ``$datadir/wireplumber``
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
* ``$syscondir`` and ``$datadir`` refer to
|
|
||||||
`meson's directory options <https://mesonbuild.com/Builtin-options.html#directories>`_
|
|
||||||
and are hardcoded at build time
|
|
||||||
* ``$XDG_`` variables refer to the
|
|
||||||
`XDG Base Directory Specification <https://specifications.freedesktop.org/basedir-spec/latest/index.html>`_
|
|
||||||
|
|
||||||
It is recommended that user specific overrides are placed in
|
|
||||||
``$XDG_CONFIG_HOME/wireplumber``, while host-specific configuration is placed in
|
|
||||||
``$XDG_CONFIG_DIRS/wireplumber`` or ``$sysconfdir/wireplumber`` and
|
|
||||||
distribution-provided configuration is placed in ``$XDG_DATA_DIRS/wireplumber``
|
|
||||||
or ``$datadir/wireplumber``.
|
|
||||||
|
|
||||||
At runtime, WirePlumber will seek out the directory with the highest priority
|
|
||||||
that contains the required configuration file. This setup allows a user or
|
|
||||||
system administrator to effortlessly override the configuration files provided
|
|
||||||
by the distribution. They can achieve this by placing a file with an identical
|
|
||||||
name in a higher priority directory.
|
|
||||||
|
|
||||||
It is also possible to override the configuration directory by setting the
|
|
||||||
``WIREPLUMBER_CONFIG_DIR`` environment variable:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
WIREPLUMBER_CONFIG_DIR=src/config wireplumber
|
|
||||||
|
|
||||||
``WIREPLUMBER_CONFIG_DIR`` supports listing multiple directories, using the
|
|
||||||
standard path list separator ``:``. If multiple directories are specified,
|
|
||||||
the first one has the highest priority and the last one has the lowest.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
When the configuration directory is overridden with
|
|
||||||
``WIREPLUMBER_CONFIG_DIR``, the default locations are ignored and
|
|
||||||
configuration files are *only* looked up in the directories specified by this
|
|
||||||
variable.
|
|
||||||
|
|
||||||
.. _config_locations_fragments:
|
|
||||||
|
|
||||||
Configuration fragments
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
WirePlumber also supports configuration fragments. These are configuration files
|
|
||||||
that are loaded in addition to the main configuration file, allowing to
|
|
||||||
override or extend the configuration without having to copy the whole file.
|
|
||||||
See also the :ref:`config_conf_file_fragments` section for semantics.
|
|
||||||
|
|
||||||
Configuration fragments are always loaded from subdirectories of the main search
|
|
||||||
directories that have the same name as the configuration file, with the ``.d``
|
|
||||||
suffix appended. For example, if WirePlumber loads ``wireplumber.conf``, it will
|
|
||||||
also load ``wireplumber.conf.d/*.conf``. Note also that the fragment files need
|
|
||||||
to have the ``.conf`` suffix.
|
|
||||||
|
|
||||||
When WirePlumber loads a configuration file from the default locations, it will
|
|
||||||
also load all configuration fragments that are present in all of the default
|
|
||||||
locations, but following the reverse order of priority. This allows
|
|
||||||
configuration fragments that are installed in more system-wide locations to be
|
|
||||||
overridden by the system administrator or the users.
|
|
||||||
|
|
||||||
For example, assuming WirePlumber loads ``wireplumber.conf``, from any of the
|
|
||||||
search locations, it will also locate and load the following fragments, in this
|
|
||||||
order:
|
|
||||||
|
|
||||||
1. ``$datadir/wireplumber/wireplumber.conf.d/*.conf``
|
|
||||||
2. ``$XDG_DATA_DIRS/wireplumber/wireplumber.conf.d/*.conf``
|
|
||||||
3. ``$sysconfdir/wireplumber/wireplumber.conf.d/*.conf``
|
|
||||||
4. ``$XDG_CONFIG_DIRS/wireplumber/wireplumber.conf.d/*.conf``
|
|
||||||
5. ``$XDG_CONFIG_HOME/wireplumber/wireplumber.conf.d/*.conf``
|
|
||||||
|
|
||||||
Within each search location that contains fragments, the individual fragment
|
|
||||||
files are opened in alphanumerical order. This can be important to know, because
|
|
||||||
the parsing order matters in merging. See :ref:`config_conf_file_fragments`
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
When ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
|
|
||||||
fragment files are *only* looked up in the directories specified by this
|
|
||||||
variable.
|
|
||||||
|
|
||||||
.. _config_locations_scripts:
|
|
||||||
|
|
||||||
Location of scripts
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
WirePlumber's default locations of its data files are the following,
|
|
||||||
in order of priority:
|
|
||||||
|
|
||||||
1. ``$XDG_DATA_HOME/wireplumber``
|
|
||||||
2. ``$XDG_DATA_DIRS/wireplumber``
|
|
||||||
3. ``$datadir/wireplumber``
|
|
||||||
|
|
||||||
At runtime, WirePlumber will search the directories for the highest-priority
|
|
||||||
directory to contain the needed data file.
|
|
||||||
|
|
||||||
Scripts are a specific kind of "data" files and are expected to be located
|
|
||||||
within a ``scripts`` subdirectory in the above data search locations. The "data"
|
|
||||||
directory is a somewhat more generic path that may be used for other kinds of
|
|
||||||
data files in the future.
|
|
||||||
|
|
||||||
It is also possible to override the data directory by setting the
|
|
||||||
``WIREPLUMBER_DATA_DIR`` environment variable:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
WIREPLUMBER_DATA_DIR=src wireplumber
|
|
||||||
|
|
||||||
As with the default data directories, script files in particular are expected
|
|
||||||
to be located within a ``scripts`` subdirectory, so in the above example the
|
|
||||||
scripts would actually reside in ``src/scripts``.
|
|
||||||
|
|
||||||
``WIREPLUMBER_DATA_DIR`` supports listing multiple directories, using the
|
|
||||||
standard path list separator ``:``. If multiple directories are specified,
|
|
||||||
the first one has the highest priority and the last one has the lowest.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
When ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
|
|
||||||
scripts are *only* looked up in the directories specified by this variable.
|
|
||||||
|
|
||||||
Location of modules
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
WirePlumber modules
|
|
||||||
^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
WirePlumber's default location of its modules is
|
|
||||||
``$libdir/wireplumber-$api_version``, where ``$libdir`` is set at compile time
|
|
||||||
by the build system. Typically, it ends up being ``/usr/lib/wireplumber-0.5``
|
|
||||||
(or ``/usr/lib/<arch-triplet>/wireplumber-0.5`` on multiarch systems)
|
|
||||||
|
|
||||||
It is possible to override this directory at runtime by setting the
|
|
||||||
``WIREPLUMBER_MODULE_DIR`` environment variable:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
WIREPLUMBER_MODULE_DIR=build/modules wireplumber
|
|
||||||
|
|
||||||
``WIREPLUMBER_MODULE_DIR`` supports listing multiple directories, using the
|
|
||||||
standard path list separator ``:``. If multiple directories are specified, the
|
|
||||||
first one has the highest priority and the last one has the lowest.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
When ``WIREPLUMBER_MODULE_DIR`` is set, the default locations are ignored and
|
|
||||||
scripts are *only* looked up in the directories specified by this variable.
|
|
||||||
|
|
||||||
PipeWire and SPA modules
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
PipeWire and SPA modules are not loaded from the same location as WirePlumber's
|
|
||||||
modules. They are loaded from the location that PipeWire loads them.
|
|
||||||
|
|
||||||
It is also possible to override these locations by using environment variables:
|
|
||||||
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
|
|
||||||
PipeWire's documentation.
|
|
||||||
|
|
@ -127,42 +127,6 @@ Above, ``<ID>`` should be replaced by the WirePlumber daemon client ID.
|
||||||
Note that PipeWire daemon log levels must be specified by numbers, not
|
Note that PipeWire daemon log levels must be specified by numbers, not
|
||||||
letter codes.
|
letter codes.
|
||||||
|
|
||||||
Changing log level via static configuration
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
If you need to capture logs from WirePlumber at startup or in other circumstances
|
|
||||||
where changing the level at runtime or setting an environment variable is not
|
|
||||||
feasible, then you may also set the log level in the configuration file.
|
|
||||||
|
|
||||||
The log level changes via the ``log.level`` key in the ``context.properties``
|
|
||||||
section:
|
|
||||||
|
|
||||||
.. code::
|
|
||||||
|
|
||||||
context.properties = {
|
|
||||||
log.level = "D"
|
|
||||||
}
|
|
||||||
|
|
||||||
You may use the same syntax as in ``WIREPLUMBER_DEBUG`` to describe the exact
|
|
||||||
logging you want to achieve. For instance, to log debug messages from all
|
|
||||||
scripts and informational messages from everywhere else:
|
|
||||||
|
|
||||||
.. code::
|
|
||||||
|
|
||||||
context.properties = {
|
|
||||||
log.level = "I,s-*:D"
|
|
||||||
}
|
|
||||||
|
|
||||||
The easiest way to configure this is to drop a
|
|
||||||
:ref:`fragment file <config_conf_file_fragments>` that contains just this.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mkdir -p ~/.config/wireplumber/wireplumber.conf.d
|
|
||||||
$ echo 'context.properties = { log.level = "D" }' > ~/.config/wireplumber/wireplumber.conf.d/log.conf
|
|
||||||
|
|
||||||
See also :ref:`config_modifying_configuration`
|
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ sphinx_files += files(
|
||||||
'installing.rst',
|
'installing.rst',
|
||||||
'running.rst',
|
'running.rst',
|
||||||
'configuration.rst',
|
'configuration.rst',
|
||||||
'locations.rst',
|
|
||||||
'logging.rst',
|
'logging.rst',
|
||||||
'multi_instance.rst',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
subdir('configuration')
|
subdir('configuration')
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
.. _daemon_multi_instance:
|
|
||||||
|
|
||||||
Running multiple instances
|
|
||||||
==========================
|
|
||||||
|
|
||||||
WirePlumber has the ability to run either as a single instance daemon or as
|
|
||||||
multiple instances, meaning that there can be multiple processes, each one
|
|
||||||
doing a different task.
|
|
||||||
|
|
||||||
The most common use case for such a setup is to separate the graph orchestration
|
|
||||||
tasks from the device monitoring and object creation ones. This can be useful
|
|
||||||
for robustness and security reasons, as it allows restarting the device monitors
|
|
||||||
or running them in different security contexts without affecting the rest of the
|
|
||||||
session management functionality.
|
|
||||||
|
|
||||||
To achieve a multi-instance setup, WirePlumber can be started multiple times
|
|
||||||
with a different :ref:`profile<config_components_and_profiles>` loaded in each
|
|
||||||
instance. This can be achieved using the ``--profile`` command line option to
|
|
||||||
select the profile to load:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ wireplumber --profile=custom
|
|
||||||
|
|
||||||
When no particular profile is specified, the ``main`` profile is loaded.
|
|
||||||
|
|
||||||
For multi-instance configuration, the default ``wireplumber.conf`` specifies 4
|
|
||||||
profiles:
|
|
||||||
|
|
||||||
.. describe:: policy
|
|
||||||
|
|
||||||
This profile runs all the policy scripts, i.e. ones that monitor changes
|
|
||||||
in the graph and execute actions to link nodes, select default devices,
|
|
||||||
create new nodes or configure existing ones differently.
|
|
||||||
|
|
||||||
.. describe:: audio
|
|
||||||
|
|
||||||
The audio profile runs the ALSA and ALSA MIDI monitors, which make audio &
|
|
||||||
MIDI devices available to PipeWire.
|
|
||||||
|
|
||||||
.. describe:: bluetooth
|
|
||||||
|
|
||||||
The bluetooth profile runs the BlueZ and BlueZ MIDI monitors, which enable
|
|
||||||
Bluetooth audio & MIDI devices and other Bluetooth functionality tied to the
|
|
||||||
A2DP, HSP, HFP and BAP profiles, using BlueZ.
|
|
||||||
|
|
||||||
.. describe:: video-capture
|
|
||||||
|
|
||||||
The video-capture profile runs the V4L2 and libcamera monitors, which make
|
|
||||||
video capture devices, such as cameras and HDMI capture cards, available
|
|
||||||
to PipeWire.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The ``main`` profile includes all the functionality of the ``policy``,
|
|
||||||
``audio``, ``video-capture`` and ``bluetooth`` profiles combined (i.e. it is
|
|
||||||
the default for a standard single instance configuration). You should never
|
|
||||||
load the ``main`` profile alongside these other 4 profiles, as their
|
|
||||||
functionality will conflict.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Always ensure that the instances you load serve a different purpose and they
|
|
||||||
do not conflict with each other. Conflicting components executed in parallel
|
|
||||||
will have undefined behavior.
|
|
||||||
|
|
||||||
Systemd integration
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
To make this easier to work with, a template systemd unit is provided, which is
|
|
||||||
meant to be started with the name of the profile as a template argument:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ systemctl --user disable wireplumber # disable the "main" profile instance
|
|
||||||
$ systemctl --user enable wireplumber@policy
|
|
||||||
$ systemctl --user enable wireplumber@audio
|
|
||||||
$ systemctl --user enable wireplumber@video-capture
|
|
||||||
$ systemctl --user enable wireplumber@bluetooth
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
In WirePlumber 0.4, the template argument was the name of the configuration
|
|
||||||
file to load, since profiles did not exist. In WirePlumber 0.5, the template
|
|
||||||
argument is the name of the profile and the configuration file is always
|
|
||||||
``wireplumber.conf``. To change the name of the configuration file you need
|
|
||||||
to craft custom systemd unit files and use the ``--config-file`` command line
|
|
||||||
option as needed.
|
|
||||||
|
|
@ -40,7 +40,7 @@ Synopsis:
|
||||||
|
|
||||||
$ meson -Dsession-managers="[ 'wireplumber' ]" build
|
$ meson -Dsession-managers="[ 'wireplumber' ]" build
|
||||||
$ ninja -C build
|
$ ninja -C build
|
||||||
$ make -C build run
|
$ make run
|
||||||
|
|
||||||
Run independently or without installing
|
Run independently or without installing
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Events and Hooks
|
Events and Hooks
|
||||||
================
|
================
|
||||||
|
|
||||||
Session management is all about reacting to events and taking necessary
|
Session management is all about reacting to events and taking neccessary
|
||||||
actions. This is why WirePlumber's logic is all built on events and hooks.
|
actions. This is why WirePlumber's logic is all built on events and hooks.
|
||||||
|
|
||||||
Events
|
Events
|
||||||
|
|
@ -56,7 +56,7 @@ There are two main types of hooks: ``SimpleEventHook`` and ``AsyncEventHook``.
|
||||||
* ``AsyncEventHook`` contains multiple functions, combined together in a state
|
* ``AsyncEventHook`` contains multiple functions, combined together in a state
|
||||||
machine using ``WpTransition`` underneath. The hook is completed only after
|
machine using ``WpTransition`` underneath. The hook is completed only after
|
||||||
the state machine reaches its final state and this can take any amount of time
|
the state machine reaches its final state and this can take any amount of time
|
||||||
necessary.
|
neccessary.
|
||||||
|
|
||||||
Every hook also has a name, which can be an arbitrary string of characters.
|
Every hook also has a name, which can be an arbitrary string of characters.
|
||||||
Additionally, it has two arrays of names, which declare dependencies between
|
Additionally, it has two arrays of names, which declare dependencies between
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ Table of Contents
|
||||||
daemon/installing.rst
|
daemon/installing.rst
|
||||||
daemon/running.rst
|
daemon/running.rst
|
||||||
daemon/configuration.rst
|
daemon/configuration.rst
|
||||||
daemon/locations.rst
|
|
||||||
daemon/logging.rst
|
daemon/logging.rst
|
||||||
daemon/multi_instance.rst
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
@ -22,14 +20,6 @@ Table of Contents
|
||||||
design/understanding_wireplumber.rst
|
design/understanding_wireplumber.rst
|
||||||
design/events_and_hooks.rst
|
design/events_and_hooks.rst
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: WirePlumber's Policies
|
|
||||||
|
|
||||||
policies/linking.rst
|
|
||||||
policies/smart_filters.rst
|
|
||||||
policies/software_dsp.rst
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: The WirePlumber Library
|
:caption: The WirePlumber Library
|
||||||
|
|
@ -42,13 +32,6 @@ Table of Contents
|
||||||
|
|
||||||
scripting/lua_api.rst
|
scripting/lua_api.rst
|
||||||
scripting/existing_scripts.rst
|
scripting/existing_scripts.rst
|
||||||
scripting/custom_scripts.rst
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Tools
|
|
||||||
|
|
||||||
tools/wpctl.rst
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ C API Documentation
|
||||||
c_api/link_api.rst
|
c_api/link_api.rst
|
||||||
c_api/device_api.rst
|
c_api/device_api.rst
|
||||||
c_api/client_api.rst
|
c_api/client_api.rst
|
||||||
c_api/permission_manager_api.rst
|
|
||||||
c_api/metadata_api.rst
|
c_api/metadata_api.rst
|
||||||
c_api/spa_device_api.rst
|
c_api/spa_device_api.rst
|
||||||
c_api/impl_node_api.rst
|
c_api/impl_node_api.rst
|
||||||
|
|
@ -40,4 +39,3 @@ C API Documentation
|
||||||
c_api/si_interfaces_api.rst
|
c_api/si_interfaces_api.rst
|
||||||
c_api/si_factory_api.rst
|
c_api/si_factory_api.rst
|
||||||
c_api/state_api.rst
|
c_api/state_api.rst
|
||||||
c_api/base_dirs_api.rst
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
.. _base_dirs_api:
|
|
||||||
|
|
||||||
Base Directories File Lookup
|
|
||||||
============================
|
|
||||||
.. doxygengroup:: wpbasedirs
|
|
||||||
:content-only:
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
# you need to add here any files you add to the api directory as well
|
# you need to add here any files you add to the api directory as well
|
||||||
sphinx_files += files(
|
sphinx_files += files(
|
||||||
'base_dirs_api.rst',
|
|
||||||
'client_api.rst',
|
'client_api.rst',
|
||||||
'component_loader_api.rst',
|
'component_loader_api.rst',
|
||||||
'conf_api.rst',
|
'conf_api.rst',
|
||||||
|
|
@ -18,7 +17,6 @@ sphinx_files += files(
|
||||||
'obj_manager_api.rst',
|
'obj_manager_api.rst',
|
||||||
'object_api.rst',
|
'object_api.rst',
|
||||||
'pipewire_object_api.rst',
|
'pipewire_object_api.rst',
|
||||||
'permission_manager_api.rst',
|
|
||||||
'plugin_api.rst',
|
'plugin_api.rst',
|
||||||
'port_api.rst',
|
'port_api.rst',
|
||||||
'properties_api.rst',
|
'properties_api.rst',
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ PipeWire Metadata
|
||||||
|
|
||||||
digraph inheritance {
|
digraph inheritance {
|
||||||
rankdir=LR;
|
rankdir=LR;
|
||||||
GBoxed -> WpMetadataItem
|
|
||||||
GObject -> WpObject;
|
GObject -> WpObject;
|
||||||
WpObject -> WpProxy;
|
WpObject -> WpProxy;
|
||||||
WpProxy -> WpGlobalProxy;
|
WpProxy -> WpGlobalProxy;
|
||||||
|
|
@ -15,8 +14,6 @@ PipeWire Metadata
|
||||||
WpMetadata-> WpImplMetadata;
|
WpMetadata-> WpImplMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
.. doxygenstruct:: WpMetadataItem
|
|
||||||
|
|
||||||
.. doxygenstruct:: WpMetadata
|
.. doxygenstruct:: WpMetadata
|
||||||
|
|
||||||
.. doxygenstruct:: WpImplMetadata
|
.. doxygenstruct:: WpImplMetadata
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
.. _permission_manager_api:
|
|
||||||
|
|
||||||
WpPermissionManager
|
|
||||||
===================
|
|
||||||
.. graphviz::
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
digraph inheritance {
|
|
||||||
rankdir=LR;
|
|
||||||
GObject -> WpObject;
|
|
||||||
WpObject -> WpPermissionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
.. doxygenstruct:: WpPermissionManager
|
|
||||||
|
|
||||||
.. doxygengroup:: wppermissionmanager
|
|
||||||
:content-only:
|
|
||||||
|
|
@ -7,16 +7,10 @@ Settings
|
||||||
|
|
||||||
digraph inheritance {
|
digraph inheritance {
|
||||||
rankdir=LR;
|
rankdir=LR;
|
||||||
GBoxed -> WpSettingsSpec;
|
|
||||||
GBoxed -> WpSettingsItem;
|
|
||||||
GObject -> WpObject;
|
GObject -> WpObject;
|
||||||
WpObject -> WpSettings;
|
WpObject -> WpSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
.. doxygenstruct:: WpSettingsSpec
|
|
||||||
|
|
||||||
.. doxygenstruct:: WpSettingsItem
|
|
||||||
|
|
||||||
.. doxygenstruct:: WpSettings
|
.. doxygenstruct:: WpSettings
|
||||||
|
|
||||||
.. doxygengroup:: wpsettings
|
.. doxygengroup:: wpsettings
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ sphinx_files += files(
|
||||||
|
|
||||||
subdir('daemon')
|
subdir('daemon')
|
||||||
subdir('design')
|
subdir('design')
|
||||||
subdir('policies')
|
|
||||||
subdir('library')
|
subdir('library')
|
||||||
subdir('scripting')
|
subdir('scripting')
|
||||||
subdir('tools')
|
|
||||||
subdir('resources')
|
subdir('resources')
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
.. _policies_linking:
|
|
||||||
|
|
||||||
Linking Policy
|
|
||||||
==============
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
------------
|
|
||||||
|
|
||||||
The linking policy in WirePlumber is the logic charged to link a PipeWire stream
|
|
||||||
node with a PipeWire device node (most cases), or with another PipeWire stream
|
|
||||||
node (monitoring applications).
|
|
||||||
|
|
||||||
PipeWire stream nodes always have one of the following media classes:
|
|
||||||
|
|
||||||
- Stream/Output/Audio: For audio playback applications (Eg pw-play).
|
|
||||||
- Stream/Input/Audio: For audio capture applications (Eg pw-record).
|
|
||||||
- Stream/Input/Video: For video capture applications (Eg cheese).
|
|
||||||
|
|
||||||
And Pipewire device nodes always have one of the following media classes:
|
|
||||||
|
|
||||||
- Audio/Sink: For audio playback devices (Eg Speakers).
|
|
||||||
- Audio/Source: For audio capture devices (Eg Microphones).
|
|
||||||
- Video/Source: For video capture devices (Eg Cameras).
|
|
||||||
|
|
||||||
By default, since in most cases we want to link a stream node with a device
|
|
||||||
node, the linking policy logic when linking 2 nodes always follows the following
|
|
||||||
assignments:
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph nodes {
|
|
||||||
rankdir=LR;
|
|
||||||
APS [shape=box label=<audio playback stream<BR/>(Stream/Output/Audio)>];
|
|
||||||
APD [shape=box label=<audio playback device<BR/>(Audio/Sink)>];
|
|
||||||
ACS [shape=box label=<audio capture stream<BR/>(Stream/Input/Audio)>];
|
|
||||||
ACD [shape=box label=<audio capture device<BR/>(Audio/Source)>];
|
|
||||||
VCS [shape=box label=<video capture stream<BR/>(Stream/Input/Video)>];
|
|
||||||
VCD [shape=box label=<video capture device<BR/>(Video/Source)>];
|
|
||||||
APS -> APD;
|
|
||||||
ACD -> ACS;
|
|
||||||
VCD -> VCS;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
First, it will check if there is a default configured device node for the
|
|
||||||
selected device media class. If there is one, and the node exists, it will link
|
|
||||||
the stream node with such configured default node. Users can easily configure
|
|
||||||
default device nodes for all the 3 different device media classes using tools
|
|
||||||
such as ``pavucontrol`` or ``wpctl``. The logic is implemented in the
|
|
||||||
``linking/find-default-target.lua`` Lua script.
|
|
||||||
|
|
||||||
If there isn't any default node configured, or there is a default node
|
|
||||||
configured but the node does not exist, WirePlumber will instead select the
|
|
||||||
best device node available. The best device node is the node with highest
|
|
||||||
session priority and available routes to the physical device. The logic is
|
|
||||||
implemented in the ``linking/find-best-target.lua`` Lua script.
|
|
||||||
|
|
||||||
If the best node could not be found because the system does not have any,
|
|
||||||
WirePlumber won't link the stream and will send a "no target node available"
|
|
||||||
error to the client.
|
|
||||||
|
|
||||||
|
|
||||||
Stream node linking properties
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
The above default linking logic behavior can be changed by setting specific
|
|
||||||
properties on the nodes.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These properties must be set in the **stream** nodes (not the device nodes),
|
|
||||||
otherwise they won't have any effect.
|
|
||||||
|
|
||||||
- **target.object**:
|
|
||||||
|
|
||||||
The name of the desired node for this stream to be linked with.
|
|
||||||
If this property is present, WirePlumber will try to find such node, see if it
|
|
||||||
can be linked with the stream, and if so, will use it instead of the default
|
|
||||||
node or best node. The logic is implemented in the ``linking/find-defined-target.lua``
|
|
||||||
Lua script. Since this property is not set by default, WirePlumber will always
|
|
||||||
link stream nodes to the default or best device node found. This property can be
|
|
||||||
easily set using tools such as ``pw-play`` with the ``--target`` flag.
|
|
||||||
|
|
||||||
Note that any node name can be specified there, even if the name is not a device
|
|
||||||
node name, but another stream node name. If this is the case, WirePlumber will
|
|
||||||
link 2 stream nodes together. An example of this case is the monitoring nodes
|
|
||||||
created by ``pavucontrol`` to monitor audio of all audio devices and streams.
|
|
||||||
|
|
||||||
- **node.dont-reconnect**:
|
|
||||||
|
|
||||||
Boolean indicating whether the stream node should not be reconnected to a new
|
|
||||||
node if its current linked node (target) was destroyed or not. By default it
|
|
||||||
is set to ``false``, so if the property is not present in the stream node, WirePlumber
|
|
||||||
will always try to reconnect the stream node to a new target instead of sending
|
|
||||||
an error to the client. The logic is implemented in the ``linking/prepare-link.lua``
|
|
||||||
Lua script.
|
|
||||||
|
|
||||||
- **node.dont-move**:
|
|
||||||
|
|
||||||
Boolean indicating whether the stream node should not be movable or not at runtime
|
|
||||||
using the metadata. If a stream node is not movable, it means that users cannot
|
|
||||||
relink the stream node to a new target at runtime (using tools such as ``pavucontrol``
|
|
||||||
or ``pw-metadata``) when the stream node is already linked to a different node. By
|
|
||||||
default it is set to ``false``, so if the property is not present, WirePlumber will
|
|
||||||
always move, and therefore link the stream node to a new target if it is defined and
|
|
||||||
updated in the ``target.object`` metadata key.
|
|
||||||
|
|
||||||
- **node.dont-fallback**:
|
|
||||||
|
|
||||||
Boolean indicating whether the stream node should not fallback to a different
|
|
||||||
target if its defined target does not exist (the one defined with the ``target.object``
|
|
||||||
property) or not. Therefore, if this property is set to ``true``, WirePlumber sends
|
|
||||||
a "defined target not found" error to the client and will also destroy the stream
|
|
||||||
node. By default it is set to ``false``, so if the property is not present in the
|
|
||||||
stream node, WirePlumber will always fallback to the default or best target if
|
|
||||||
the defined target was not found.
|
|
||||||
|
|
||||||
- **node.linger**:
|
|
||||||
|
|
||||||
Boolean indicating whether the stream node should linger or not if its defined
|
|
||||||
target was not found and the ``node.dont-fallback`` is set to true. Therefore, if
|
|
||||||
this property is set to ``true``, the defined target was not found, and the
|
|
||||||
``node.dont-fallback`` is set to true, WirePlumber won't send a "defined target not found"
|
|
||||||
error to the client, and won't destroy the stream node. This is useful if we want
|
|
||||||
the stream to wait (without processing any data) until its defined target becomes
|
|
||||||
available. By default it is set to ``false``, so if the property is not present in the
|
|
||||||
stream node, WirePlumber will always destroy the node and send an error to the client
|
|
||||||
if its target was not found and ``node.dont-fallback`` was set to true.
|
|
||||||
|
|
||||||
|
|
||||||
Linking settings
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Apart from the above properties, there are also global settings for the linking
|
|
||||||
policy. See :ref:`config_settings` for more information, the linking settings
|
|
||||||
are prefixed with ``linking.``.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# you need to add here any files you add to the toc directory as well
|
|
||||||
sphinx_files += files(
|
|
||||||
'linking.rst',
|
|
||||||
'smart_filters.rst',
|
|
||||||
'software_dsp.rst',
|
|
||||||
)
|
|
||||||
|
|
@ -1,380 +0,0 @@
|
||||||
.. _policies_smart_filters:
|
|
||||||
|
|
||||||
Smart Filters
|
|
||||||
=============
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
------------
|
|
||||||
|
|
||||||
The smart filters policy allows automatically linking filters together, in a
|
|
||||||
chain, and tied to a specific target node. This is useful when we want to apply
|
|
||||||
a specific processing chain to a specific device, for example. When a stream is
|
|
||||||
about to be linked to a target node that is associated with a smart filter
|
|
||||||
chain, the policy will automatically link the stream with the first filter in
|
|
||||||
the chain, and the last filter in the chain with the target node. This is done
|
|
||||||
transparently to the client, allowing users to define a specific processing
|
|
||||||
chain for a specific device without having to create setups with virtual sinks
|
|
||||||
(or sources) that must be explicitly targeted by the clients.
|
|
||||||
|
|
||||||
Filters, in general, are nodes that are placed in the middle of the graph and
|
|
||||||
are used to modify the data that passes through them. For example, the
|
|
||||||
*echo-cancel*, the *filter-chain*, or the *loopback* nodes are filters.
|
|
||||||
Filters can be implemented either as a single node or as a pair of nodes with
|
|
||||||
opposite directions. For example, the *null-audio-sink* node can be configured
|
|
||||||
to be a single-node filter. On the other hand, the *filter-chain* is a pair of
|
|
||||||
nodes with opposite directions, where one node captures the audio from the graph
|
|
||||||
and the other node sends the modified audio back to the graph.
|
|
||||||
|
|
||||||
For the purpose of the **smart filters** policy, WirePlumber will only consider
|
|
||||||
pairs of nodes as filters, not single-node ones. More specifically, a pair of
|
|
||||||
nodes will be considered to be a filter by WirePlumber if they have the
|
|
||||||
``node.link-group`` property set to a common value. This property is always set
|
|
||||||
on pairs of nodes that are internally linked together and is a good indicator
|
|
||||||
that the nodes are implementing a filter.
|
|
||||||
|
|
||||||
That pair of nodes **must** always consist of a *stream* node and a *main* node.
|
|
||||||
The main node acts as a virtual device, where the data is sent or captured
|
|
||||||
to/from, and the stream node acts as a regular stream, where the data is sent
|
|
||||||
or received to/from the next node in the graph. This is designated by their
|
|
||||||
media class, as shown in the table below:
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:widths: 30 35 35
|
|
||||||
:header-rows: 1
|
|
||||||
:stub-columns: 1
|
|
||||||
|
|
||||||
* -
|
|
||||||
- Input filter (virtual sink)
|
|
||||||
- Output filter (virtual source)
|
|
||||||
* - Main node
|
|
||||||
- ``Audio/Sink`` (capture)
|
|
||||||
- ``Audio/Source`` (playback)
|
|
||||||
* - Stream node
|
|
||||||
- ``Stream/Output/Audio`` (playback)
|
|
||||||
- ``Stream/Input/Audio`` (capture)
|
|
||||||
|
|
||||||
For instance, if a smart filter is used between an application playback stream
|
|
||||||
and the default audio sink, the graph would look like this:
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph nodes {
|
|
||||||
rankdir=LR;
|
|
||||||
A [shape=box label=<application stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
FM [shape=box label=<filter main node<BR/>(Audio/Sink)>];
|
|
||||||
FS [shape=box label=<filter stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
D [shape=box label=<default device node<BR/>(Audio/Sink)>];
|
|
||||||
A -> FM;
|
|
||||||
FS -> D;
|
|
||||||
subgraph cluster_filter {
|
|
||||||
style="dotted";
|
|
||||||
FM; FS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The same logic is applied if the smart filter is used between an application
|
|
||||||
capture stream and the default audio source, it is just all in the opposite
|
|
||||||
direction. This is how the graph would look like in this case:
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph nodes {
|
|
||||||
rankdir=LR;
|
|
||||||
A [shape=box label=<application stream node<BR/>(Stream/Input/Audio)>];
|
|
||||||
FM [shape=box label=<filter main node<BR/>(Audio/Source)>];
|
|
||||||
FS [shape=box label=<filter stream node<BR/>(Stream/Input/Audio)>];
|
|
||||||
D [shape=box label=<default device node<BR/>(Audio/Source)>];
|
|
||||||
D -> FS;
|
|
||||||
FM -> A;
|
|
||||||
subgraph cluster_filter {
|
|
||||||
style="dotted";
|
|
||||||
FM; FS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
When multiple filters have the same direction, they can also be chained together
|
|
||||||
so that the output of one filter is sent to the input of the next filter. The
|
|
||||||
next section describes how these chains can be described with properties so that
|
|
||||||
they are automatically linked by WirePlumber in any way we want.
|
|
||||||
|
|
||||||
Filter properties
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
When a filter node is created, WirePlumber will check for the presence of the
|
|
||||||
following optional node properties on the **main** node:
|
|
||||||
|
|
||||||
- **filter.smart**
|
|
||||||
|
|
||||||
Boolean indicating whether smart policy will be used for these filter nodes or
|
|
||||||
not. This is disabled by default, therefore filter nodes will be treated as
|
|
||||||
regular nodes, without applying any kind of extra logic. On the other hand, if
|
|
||||||
this property is set to ``true``, automatic (smart) filter policy will be used
|
|
||||||
when linking them. The properties below will then also apply, providing
|
|
||||||
further instructions.
|
|
||||||
|
|
||||||
- **filter.smart.name**
|
|
||||||
|
|
||||||
The unique name of the filter. WirePlumber will use the value of the
|
|
||||||
``node.link-group`` property as the filter name if this property is not set.
|
|
||||||
|
|
||||||
- **filter.smart.disabled**
|
|
||||||
|
|
||||||
Boolean indicating whether the filter should be disabled or not. A disabled
|
|
||||||
filter will never be used under any circumstances. If the property is not set,
|
|
||||||
WirePlumber will consider the filter as enabled (i.e. disabled = false).
|
|
||||||
|
|
||||||
- **filter.smart.targetable**
|
|
||||||
|
|
||||||
Boolean indicating whether the filter can be directly linked with clients that
|
|
||||||
have it defined as a target (Eg: ``pw-play --target <filter-name>``) or not.
|
|
||||||
This can be useful when a client wants to be linked with a filter that is in
|
|
||||||
the middle of the chain in order to bypass the filters that are placed before
|
|
||||||
the selected one. If the property is not set, WirePlumber will consider the
|
|
||||||
filter not targetable by default, meaning filters will never by bypassed by
|
|
||||||
clients, and clients will always be linked with the first filter in the chain.
|
|
||||||
|
|
||||||
- **filter.smart.target**
|
|
||||||
|
|
||||||
A JSON object that defines the matching properties of the filter's target
|
|
||||||
node. A filter target can never be another filter node (WirePlumber will
|
|
||||||
ignore it), it must be a device or virtual sink (or source, depending on the
|
|
||||||
direction of the filter). If this property is not set, WirePlumber will use
|
|
||||||
the default sink/source as the target.
|
|
||||||
|
|
||||||
- **filter.smart.before**
|
|
||||||
|
|
||||||
A JSON array containing the names of the filters that are supposed to be
|
|
||||||
chained after this filter (i.e. this filter here should be chained *before*
|
|
||||||
those). If not set, WirePlumber will link the filters by order of creation.
|
|
||||||
|
|
||||||
- **filter.smart.after**
|
|
||||||
|
|
||||||
A JSON array containing the names of the filters that are supposed to be
|
|
||||||
chained before this filter (i.e. this filter here should be chained *after*
|
|
||||||
those). If not set, WirePlumber will link the filters by order of creation.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These properties must be set on the filter's **main** node, not the stream
|
|
||||||
node.
|
|
||||||
|
|
||||||
As an example, we will describe here how to create 2 loopback filters in
|
|
||||||
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
|
|
||||||
linked with the default audio device, and use loopback-2 filter as the last
|
|
||||||
filter in the chain.
|
|
||||||
|
|
||||||
The PipeWire configuration files for the 2 filters should be like this:
|
|
||||||
|
|
||||||
- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:emphasize-lines: 8-11
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
{ name = libpipewire-module-loopback
|
|
||||||
args = {
|
|
||||||
node.name = loopback-1-sink
|
|
||||||
node.description = "Loopback 1 Sink"
|
|
||||||
capture.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
media.class = Audio/Sink
|
|
||||||
filter.smart = true
|
|
||||||
filter.smart.name = loopback-1
|
|
||||||
filter.smart.before = [ loopback-2 ]
|
|
||||||
}
|
|
||||||
playback.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
node.passive = true
|
|
||||||
stream.dont-remix = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:emphasize-lines: 8-10
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
{ name = libpipewire-module-loopback
|
|
||||||
args = {
|
|
||||||
node.name = loopback-2-sink
|
|
||||||
node.description = "Loopback 2 Sink"
|
|
||||||
capture.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
media.class = Audio/Sink
|
|
||||||
filter.smart = true
|
|
||||||
filter.smart.name = loopback-2
|
|
||||||
}
|
|
||||||
playback.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
node.passive = true
|
|
||||||
stream.dont-remix = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
After restarting PipeWire to apply the configuration changes, playing a test
|
|
||||||
wave audio file with paplay to the default device should result in the following
|
|
||||||
graph:
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph nodes {
|
|
||||||
rankdir=LR;
|
|
||||||
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
|
|
||||||
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
|
|
||||||
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
|
|
||||||
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
device [shape=box label=<default device node<BR/>(Audio/Sink)>];
|
|
||||||
paplay -> L1M;
|
|
||||||
L1S -> L2M;
|
|
||||||
L2S -> device;
|
|
||||||
subgraph cluster_filter1 {
|
|
||||||
style="dotted";
|
|
||||||
L1M; L1S;
|
|
||||||
}
|
|
||||||
subgraph cluster_filter2 {
|
|
||||||
style="dotted";
|
|
||||||
L2M; L2S;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Now, if we remove the ``filter.smart.before = [ loopback-2 ]`` property from the
|
|
||||||
loopback-1 filter, and add a ``filter.smart.before = [ loopback-1 ]`` property
|
|
||||||
in the loopback-2 filter configuration file, WirePlumber should link the
|
|
||||||
loopback-1 filter as the last filter in the chain, like this:
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph nodes {
|
|
||||||
rankdir=LR;
|
|
||||||
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
|
|
||||||
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
|
|
||||||
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
|
|
||||||
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
device [shape=box label=<default device node<BR/>(Audio/Sink)>];
|
|
||||||
paplay -> L2M;
|
|
||||||
L2S -> L1M;
|
|
||||||
L1S -> device;
|
|
||||||
subgraph cluster_filter1 {
|
|
||||||
style="dotted";
|
|
||||||
L1M; L1S;
|
|
||||||
}
|
|
||||||
subgraph cluster_filter2 {
|
|
||||||
style="dotted";
|
|
||||||
L2M; L2S;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
In addition, the filters can have different targets. For example, we can define
|
|
||||||
the filters like this:
|
|
||||||
|
|
||||||
- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:emphasize-lines: 12
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
{ name = libpipewire-module-loopback
|
|
||||||
args = {
|
|
||||||
node.name = loopback-1-sink
|
|
||||||
node.description = "Loopback 1 Sink"
|
|
||||||
capture.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
media.class = Audio/Sink
|
|
||||||
filter.smart = true
|
|
||||||
filter.smart.name = loopback-1
|
|
||||||
filter.smart.after = [ loopback-2 ]
|
|
||||||
filter.smart.target = { node.name = "not-default-audio-device" }
|
|
||||||
}
|
|
||||||
playback.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
node.passive = true
|
|
||||||
stream.dont-remix = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
{ name = libpipewire-module-loopback
|
|
||||||
args = {
|
|
||||||
node.name = loopback-2-sink
|
|
||||||
node.description = "Loopback 2 Sink"
|
|
||||||
capture.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
media.class = Audio/Sink
|
|
||||||
filter.smart = true
|
|
||||||
filter.smart.name = loopback-2
|
|
||||||
}
|
|
||||||
playback.props = {
|
|
||||||
audio.position = [ FL FR ]
|
|
||||||
node.passive = true
|
|
||||||
stream.dont-remix = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
In this case, playing a test wave audio file with paplay to the
|
|
||||||
``not-default-audio-device`` device should result in the following graph:
|
|
||||||
|
|
||||||
.. graphviz::
|
|
||||||
|
|
||||||
digraph nodes {
|
|
||||||
rankdir=LR;
|
|
||||||
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
|
|
||||||
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
|
|
||||||
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
|
|
||||||
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
|
|
||||||
device [shape=box label=<not-default-audio-device node<BR/>(Audio/Sink)>];
|
|
||||||
paplay -> L2M;
|
|
||||||
L2S -> L1M;
|
|
||||||
L1S -> device;
|
|
||||||
subgraph cluster_filter1 {
|
|
||||||
style="dotted";
|
|
||||||
L1M; L1S;
|
|
||||||
}
|
|
||||||
subgraph cluster_filter2 {
|
|
||||||
style="dotted";
|
|
||||||
L2M; L2S;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
In this configuration, the loopback-1 filter will only be linked if the
|
|
||||||
application stream is targeting the device node called
|
|
||||||
"not-default-audio-device".
|
|
||||||
|
|
||||||
Filters metadata
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Similar to the default metadata, it is also possible to override the filter
|
|
||||||
properties using the "filters" metadata object. This allow users to change the
|
|
||||||
filters policy at runtime.
|
|
||||||
|
|
||||||
For example, assuming the id of the *loopback-1* main node is ``40``, we can
|
|
||||||
disable the filter by setting its ``filter.smart.disabled`` metadata key to
|
|
||||||
``true`` using the ``pw-metadata`` tool like this:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON
|
|
||||||
|
|
||||||
We can also change the target of a filter at runtime:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ pw-metadata -n filters 40 "filter.smart.target" "{ node.name = new-target-node-name }" Spa:String:JSON
|
|
||||||
|
|
||||||
Every time a key in the filters metadata changes, all filters are unlinked and
|
|
||||||
re-linked properly, following the new policy.
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
.. _policies_software_dsp:
|
|
||||||
|
|
||||||
Automatic Software DSP
|
|
||||||
======================
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
------------
|
|
||||||
|
|
||||||
WirePlumber provides a mechanism for transparently handling oddball and embedded
|
|
||||||
devices that require software DSP to be done in userspace. Devices such as smartphones,
|
|
||||||
TVs, portable speakers, and even some laptops implement an audio subsystem designed
|
|
||||||
under the assumption that the hardware sink/source will be "backed" by some sort
|
|
||||||
of transparent DSP mechanism. That is, the hardware device itself should not be
|
|
||||||
directly accessed, and expects to be sent preprocessed/pre-routed samples. Often,
|
|
||||||
especially with Android handsets, these samples are preprocessed or pre-routed
|
|
||||||
by the vendor's proprietary userspace.
|
|
||||||
|
|
||||||
WirePlumber's automatic software DSP mechanism aims to replicate this functionality in
|
|
||||||
a standardised and configurable way. The target device sink/source is hidden from
|
|
||||||
other PipeWire clients, and a virtual node is linked to it. This virtual
|
|
||||||
node is then presented to clients as *the* node, allowing implementers to specify
|
|
||||||
any custom processing or routing in a way that is transparent to users, the kernel,
|
|
||||||
and the hardware.
|
|
||||||
|
|
||||||
|
|
||||||
Activating
|
|
||||||
----------
|
|
||||||
|
|
||||||
In addition to the ``node.software-dsp.rules`` section, the ``node.software-dsp``
|
|
||||||
:ref:`feature <config_features>` must be enabled in the desired profile(s).
|
|
||||||
|
|
||||||
|
|
||||||
Matching a node
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Matching rules are specified in ``node.software-dsp.rules``. The ``create-filter``
|
|
||||||
action specifies behaviour at node insertion. All node properties can be matched
|
|
||||||
on, including any type-specific properties such as ``alsa.id``.
|
|
||||||
|
|
||||||
|
|
||||||
Configurable properties
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. describe:: filter-graph
|
|
||||||
|
|
||||||
SPA-JSON object describing the software DSP node. This is passed as-is as
|
|
||||||
an argument to ``libpipewire-module-filter-chain``. See the
|
|
||||||
`filter-chain documentation <https://docs.pipewire.org/page_module_filter_chain.html>`_
|
|
||||||
for details on what options can be set in this object.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The ``target.object`` property of the virtual node should be configured
|
|
||||||
statically to point to the node matched by the rule.
|
|
||||||
|
|
||||||
.. describe:: filter-path
|
|
||||||
|
|
||||||
Absolute path to a file on disk storing a SPA-JSON object as plain text. This will be
|
|
||||||
parsed by WirePlumber into a WpConf object with a single section called
|
|
||||||
``node.software-dsp.graph``, then passed as-is into ``libpipewire-module-filter-chain``.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
``filter-graph`` and ``filter-path`` are mutually exclusive, with the former taking
|
|
||||||
precedence if both are present in the matched rule.
|
|
||||||
|
|
||||||
.. describe:: hide-parent
|
|
||||||
|
|
||||||
Boolean indicating whether or not the matched node should be hidden from
|
|
||||||
clients. ``node/software-dsp.lua`` will set the permissions for all clients other
|
|
||||||
than WirePlumber itself to ``'-'``. This prevents use of the node by any
|
|
||||||
userspace software except for WirePlumber itself.
|
|
||||||
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
:caption: wireplumber.conf.d/99-my-dsp.conf
|
|
||||||
|
|
||||||
node.software-dsp.rules = [
|
|
||||||
{
|
|
||||||
matches = [
|
|
||||||
{ "node.name" = "alsa_output.platform-sound.HiFi__Speaker__sink" }
|
|
||||||
{ "alsa.id" = "~WeirdHardware*" } # Wildcard match
|
|
||||||
]
|
|
||||||
|
|
||||||
actions = {
|
|
||||||
create-filter = {
|
|
||||||
filter-graph = {} # Virtual node goes here
|
|
||||||
filter-path = "/path/to/spa.json"
|
|
||||||
hide-parent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
node.software-dsp = required
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
This will match any sinks with the UCM HiFi Speaker profile set or cards
|
|
||||||
containing the string "WeirdHardware" at the start of their name.
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
.. _scripting_custom_scripts:
|
|
||||||
|
|
||||||
Custom Scripts
|
|
||||||
==============
|
|
||||||
|
|
||||||
The locations where WirePlumber searches for scripts is explained in
|
|
||||||
:ref:`config_locations_scripts`.
|
|
||||||
|
|
||||||
Scripts are not loaded automatically; a component muse be defined for them, and
|
|
||||||
this component must be included in a profile. See
|
|
||||||
:ref:`config_components_and_profiles`.
|
|
||||||
|
|
||||||
Full example
|
|
||||||
------------
|
|
||||||
|
|
||||||
Let's assume that ``~/.local/share/wireplumber/scripts/90-hello-world.lua``
|
|
||||||
contains the following script:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
|
|
||||||
log = Log.open_topic("hello-world")
|
|
||||||
log.info("Hello world")
|
|
||||||
|
|
||||||
In order for it to run, we'll define a component and include it in the default
|
|
||||||
profile by including the following configuration (for example, in
|
|
||||||
``~/.config/wireplumber/wireplumber.conf.d/90-hello-world.conf``):
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
wireplumber.components = [
|
|
||||||
{
|
|
||||||
name = "90-hello-world.lua", type = script/lua
|
|
||||||
provides = hello-world
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
wireplumber.profiles = {
|
|
||||||
main = {
|
|
||||||
hello-world = required
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,33 +3,6 @@
|
||||||
Debug Logging
|
Debug Logging
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Constructors
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. function:: Log.open_topic(topic)
|
|
||||||
|
|
||||||
Opens a LogTopic with the given topic name. Well known script topics are
|
|
||||||
described in :ref:`daemon_logging`, and messages from scripts shall use
|
|
||||||
**s-***.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: lua
|
|
||||||
|
|
||||||
local obj
|
|
||||||
log = Log.open_topic ("s-linking")
|
|
||||||
log:info (obj, "an info message on obj")
|
|
||||||
log:debug ("a debug message")
|
|
||||||
|
|
||||||
Above example shows how to output debug logs.
|
|
||||||
|
|
||||||
:param string topic: The log topic to open
|
|
||||||
:returns: the log topic object
|
|
||||||
:rtype: Log (:c:struct:`WpLogTopic`)
|
|
||||||
|
|
||||||
Methods
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. function:: Log.warning(object, message)
|
.. function:: Log.warning(object, message)
|
||||||
|
|
||||||
Logs a warning message, like :c:macro:`wp_warning_object`
|
Logs a warning message, like :c:macro:`wp_warning_object`
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
sphinx_files += files(
|
sphinx_files += files(
|
||||||
'lua_api.rst',
|
'lua_api.rst',
|
||||||
'existing_scripts.rst',
|
'existing_scripts.rst',
|
||||||
'custom_scripts.rst',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
subdir('lua_api')
|
subdir('lua_api')
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
sphinx_files += files(
|
|
||||||
'wpctl.rst',
|
|
||||||
)
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
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/
|
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2020 Collabora Ltd.
|
|
||||||
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "base-dirs.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "wpversion.h"
|
|
||||||
#include "wpbuildbasedirs.h"
|
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-base-dirs")
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \defgroup wpbasedirs Base Directories File Lookup
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Returns /basedir/subdir/filename, with filename treated as a module
|
|
||||||
* if WP_BASE_DIRS_FLAG_MODULE is set.
|
|
||||||
* The basedir is assumed to be either an absolute path or NULL.
|
|
||||||
* The subdir is assumed to be a path relative to basedir or NULL.
|
|
||||||
*/
|
|
||||||
static gchar *
|
|
||||||
make_path (guint flags, const gchar *basedir, const gchar *subdir,
|
|
||||||
const gchar *filename)
|
|
||||||
{
|
|
||||||
g_autofree gchar *full_basedir = NULL;
|
|
||||||
g_autofree gchar *full_filename = NULL;
|
|
||||||
|
|
||||||
/* merge subdir into basedir, if necessary */
|
|
||||||
if (subdir) {
|
|
||||||
full_basedir = g_canonicalize_filename (subdir, basedir);
|
|
||||||
basedir = full_basedir;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags & WP_BASE_DIRS_FLAG_MODULE) {
|
|
||||||
g_autofree gchar *basename = g_path_get_basename (filename);
|
|
||||||
g_autofree gchar *dirname = g_path_get_dirname (filename);
|
|
||||||
const gchar *prefix = "";
|
|
||||||
const gchar *suffix = "";
|
|
||||||
if (!g_str_has_prefix (basename, "lib"))
|
|
||||||
prefix = "lib";
|
|
||||||
if (!g_str_has_suffix (basename, ".so"))
|
|
||||||
suffix = ".so";
|
|
||||||
full_filename = g_strconcat (dirname, G_DIR_SEPARATOR_S,
|
|
||||||
prefix, basename, suffix, NULL);
|
|
||||||
filename = full_filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
return g_canonicalize_filename (filename, basedir);
|
|
||||||
}
|
|
||||||
|
|
||||||
static GPtrArray *
|
|
||||||
lookup_dirs (guint flags, gboolean is_absolute)
|
|
||||||
{
|
|
||||||
g_autoptr(GPtrArray) dirs = g_ptr_array_new_with_free_func (g_free);
|
|
||||||
const gchar *dir;
|
|
||||||
const gchar *subdir =
|
|
||||||
(flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ? "wireplumber" : ".";
|
|
||||||
|
|
||||||
/* Compile the list of lookup directories in priority order */
|
|
||||||
if (is_absolute) {
|
|
||||||
g_ptr_array_add (dirs, NULL);
|
|
||||||
}
|
|
||||||
else if ((flags & WP_BASE_DIRS_ENV_CONFIG) &&
|
|
||||||
(dir = g_getenv ("WIREPLUMBER_CONFIG_DIR"))) {
|
|
||||||
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
|
|
||||||
for (guint i = 0; env_dirs[i]; i++) {
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((flags & WP_BASE_DIRS_ENV_DATA) &&
|
|
||||||
(dir = g_getenv ("WIREPLUMBER_DATA_DIR"))) {
|
|
||||||
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
|
|
||||||
for (guint i = 0; env_dirs[i]; i++) {
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((flags & WP_BASE_DIRS_ENV_MODULE) &&
|
|
||||||
(dir = g_getenv ("WIREPLUMBER_MODULE_DIR"))) {
|
|
||||||
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
|
|
||||||
for (guint i = 0; env_dirs[i]; i++) {
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (flags & WP_BASE_DIRS_XDG_CONFIG_HOME) {
|
|
||||||
dir = g_get_user_config_dir ();
|
|
||||||
if (G_LIKELY (g_path_is_absolute (dir)))
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
|
|
||||||
}
|
|
||||||
if (flags & WP_BASE_DIRS_XDG_DATA_HOME) {
|
|
||||||
dir = g_get_user_data_dir ();
|
|
||||||
if (G_LIKELY (g_path_is_absolute (dir)))
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
|
|
||||||
}
|
|
||||||
if (flags & WP_BASE_DIRS_XDG_CONFIG_DIRS) {
|
|
||||||
const gchar * const *xdg_dirs = g_get_system_config_dirs ();
|
|
||||||
for (guint i = 0; xdg_dirs[i]; i++) {
|
|
||||||
if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (flags & WP_BASE_DIRS_BUILD_SYSCONFDIR) {
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_SYSCONFDIR));
|
|
||||||
}
|
|
||||||
if (flags & WP_BASE_DIRS_XDG_DATA_DIRS) {
|
|
||||||
const gchar * const *xdg_dirs = g_get_system_data_dirs ();
|
|
||||||
for (guint i = 0; xdg_dirs[i]; i++) {
|
|
||||||
if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (flags & WP_BASE_DIRS_BUILD_DATADIR) {
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_DATADIR));
|
|
||||||
}
|
|
||||||
if (flags & WP_BASE_DIRS_BUILD_LIBDIR) {
|
|
||||||
subdir = (flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ?
|
|
||||||
"wireplumber-" WIREPLUMBER_API_VERSION : ".";
|
|
||||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_LIBDIR));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return g_steal_pointer (&dirs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Searches for \a filename in the hierarchy of directories specified
|
|
||||||
* by the \a flags parameter
|
|
||||||
*
|
|
||||||
* Returns the highest priority file found in the hierarchy of directories
|
|
||||||
* specified by the \a flags parameter. The \a subdir parameter is the name
|
|
||||||
* of the subdirectory to search in, inside the specified directories. If
|
|
||||||
* \a subdir is NULL, the base path of each directory is used.
|
|
||||||
*
|
|
||||||
* The \a filename parameter is the name of the file to search for. If the
|
|
||||||
* file is found, its full path is returned. If the file is not found, NULL
|
|
||||||
* is returned. The file is considered found if it is a regular file.
|
|
||||||
*
|
|
||||||
* If the \a filename is an absolute path, it is tested for existence and
|
|
||||||
* returned as is, ignoring the lookup directories in \a flags as well as
|
|
||||||
* the \a subdir parameter.
|
|
||||||
*
|
|
||||||
* \ingroup wpbasedirs
|
|
||||||
* \param flags flags to specify the directories to look into and other
|
|
||||||
* options specific to the kind of file being looked up
|
|
||||||
* \param subdir (nullable): the name of the subdirectory to search in,
|
|
||||||
* inside the specified directories
|
|
||||||
* \param filename the name of the file to search for
|
|
||||||
* \returns (transfer full) (nullable): A newly allocated string with the
|
|
||||||
* absolute, canonicalized file path, or NULL if the file was not found.
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
gchar *
|
|
||||||
wp_base_dirs_find_file (WpBaseDirsFlags flags, const gchar * subdir,
|
|
||||||
const gchar * filename)
|
|
||||||
{
|
|
||||||
gboolean is_absolute = g_path_is_absolute (filename);
|
|
||||||
g_autoptr (GPtrArray) dir_paths = lookup_dirs (flags, is_absolute);
|
|
||||||
gchar *ret = NULL;
|
|
||||||
|
|
||||||
/* ignore the subdir if filename is absolute */
|
|
||||||
if (is_absolute)
|
|
||||||
subdir = NULL;
|
|
||||||
|
|
||||||
for (guint i = 0; i < dir_paths->len; i++) {
|
|
||||||
g_autofree gchar *path = make_path (flags, g_ptr_array_index (dir_paths, i),
|
|
||||||
subdir, filename);
|
|
||||||
wp_trace ("test file: %s", path);
|
|
||||||
if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) {
|
|
||||||
ret = g_steal_pointer (&path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_debug ("lookup '%s', return: %s", filename, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct conffile_iterator_item
|
|
||||||
{
|
|
||||||
gchar *filename;
|
|
||||||
gchar *path;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
conffile_iterator_item_clear (struct conffile_iterator_item *item)
|
|
||||||
{
|
|
||||||
g_free (item->filename);
|
|
||||||
g_free (item->path);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct conffile_iterator_data
|
|
||||||
{
|
|
||||||
GArray *items;
|
|
||||||
guint idx;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
conffile_iterator_reset (WpIterator *it)
|
|
||||||
{
|
|
||||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
|
||||||
it_data->idx = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
conffile_iterator_next (WpIterator *it, GValue *item)
|
|
||||||
{
|
|
||||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
|
||||||
|
|
||||||
if (it_data->idx < it_data->items->len) {
|
|
||||||
const gchar *path = g_array_index (it_data->items,
|
|
||||||
struct conffile_iterator_item, it_data->idx).path;
|
|
||||||
it_data->idx++;
|
|
||||||
g_value_init (item, G_TYPE_STRING);
|
|
||||||
g_value_set_string (item, path);
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
conffile_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
|
||||||
gpointer data)
|
|
||||||
{
|
|
||||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
|
||||||
|
|
||||||
for (guint i = 0; i < it_data->items->len; i++) {
|
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
|
||||||
const gchar *path = g_array_index (it_data->items,
|
|
||||||
struct conffile_iterator_item, i).path;
|
|
||||||
g_value_init (&item, G_TYPE_STRING);
|
|
||||||
g_value_set_string (&item, path);
|
|
||||||
if (!func (&item, ret, data))
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
conffile_iterator_finalize (WpIterator *it)
|
|
||||||
{
|
|
||||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
|
||||||
g_clear_pointer (&it_data->items, g_array_unref);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const WpIteratorMethods conffile_iterator_methods = {
|
|
||||||
.version = WP_ITERATOR_METHODS_VERSION,
|
|
||||||
.reset = conffile_iterator_reset,
|
|
||||||
.next = conffile_iterator_next,
|
|
||||||
.fold = conffile_iterator_fold,
|
|
||||||
.finalize = conffile_iterator_finalize,
|
|
||||||
};
|
|
||||||
|
|
||||||
static gint
|
|
||||||
conffile_iterator_item_compare (const struct conffile_iterator_item *a,
|
|
||||||
const struct conffile_iterator_item *b)
|
|
||||||
{
|
|
||||||
return g_strcmp0 (a->filename, b->filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Creates an iterator to iterate over all files that match \a suffix
|
|
||||||
* within the \a subdir of the directories specified in \a flags
|
|
||||||
*
|
|
||||||
* The \a subdir parameter is the name of the subdirectory to search in,
|
|
||||||
* inside the directories specified by \a flags. If \a subdir is NULL,
|
|
||||||
* the base path of each directory is used. If \a subdir is an absolute path,
|
|
||||||
* files are only looked up in that directory and the directories in \a flags
|
|
||||||
* are ignored.
|
|
||||||
*
|
|
||||||
* The \a suffix parameter is the filename suffix to match. If \a suffix is
|
|
||||||
* NULL, all files are matched.
|
|
||||||
*
|
|
||||||
* The iterator will iterate over the absolute paths of all the files
|
|
||||||
* files found, in the order of priority of the directories, starting from
|
|
||||||
* the lowest priority directory (e.g. /usr/share/wireplumber) and ending
|
|
||||||
* with the highest priority directory (e.g. $XDG_CONFIG_HOME/wireplumber).
|
|
||||||
* Files within each directory are also sorted by filename.
|
|
||||||
*
|
|
||||||
* \ingroup wpbasedirs
|
|
||||||
* \param flags flags to specify the directories to look into and other
|
|
||||||
* options specific to the kind of file being looked up
|
|
||||||
* \param subdir (nullable): the name of the subdirectory to search in,
|
|
||||||
* inside the configuration directories
|
|
||||||
* \param suffix (nullable): The filename suffix, NULL matches all entries
|
|
||||||
* \returns (transfer full): a new iterator iterating over strings which are
|
|
||||||
* absolute & canonicalized paths to the files found
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
WpIterator *
|
|
||||||
wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
|
|
||||||
const gchar * subdir, const gchar * suffix)
|
|
||||||
{
|
|
||||||
g_autoptr (GArray) items =
|
|
||||||
g_array_new (FALSE, FALSE, sizeof (struct conffile_iterator_item));
|
|
||||||
g_autoptr (GPtrArray) dir_paths = NULL;
|
|
||||||
|
|
||||||
g_array_set_clear_func (items, (GDestroyNotify) conffile_iterator_item_clear);
|
|
||||||
|
|
||||||
if (subdir == NULL)
|
|
||||||
subdir = ".";
|
|
||||||
|
|
||||||
/* Note: this list is highest-priority first */
|
|
||||||
dir_paths = lookup_dirs (flags, g_path_is_absolute (subdir));
|
|
||||||
|
|
||||||
/* Run backwards through the list to get files in lowest-priority-first order */
|
|
||||||
for (guint i = dir_paths->len; i > 0; i--) {
|
|
||||||
g_autofree gchar *dirpath =
|
|
||||||
g_canonicalize_filename (subdir, g_ptr_array_index (dir_paths, i - 1));
|
|
||||||
g_autoptr (GDir) dir = g_dir_open (dirpath, 0, NULL);
|
|
||||||
|
|
||||||
if (dir) {
|
|
||||||
g_autoptr (GArray) dir_items = g_array_new (FALSE, FALSE,
|
|
||||||
sizeof (struct conffile_iterator_item));
|
|
||||||
|
|
||||||
wp_trace ("searching dir: %s", dirpath);
|
|
||||||
|
|
||||||
/* Store all filenames with their full path in the local array */
|
|
||||||
const gchar *filename;
|
|
||||||
while ((filename = g_dir_read_name (dir))) {
|
|
||||||
if (filename[0] == '.')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (suffix && !g_str_has_suffix (filename, suffix))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/* verify the file is regular and canonicalize the path */
|
|
||||||
g_autofree gchar *path = make_path (flags, dirpath, NULL, filename);
|
|
||||||
if (!g_file_test (path, G_FILE_TEST_IS_REGULAR))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/* remove item with the same filename from the global items array,
|
|
||||||
so that lower priority files can be shadowed */
|
|
||||||
for (guint j = 0; j < items->len; j++) {
|
|
||||||
struct conffile_iterator_item *item = &g_array_index (items,
|
|
||||||
struct conffile_iterator_item, j);
|
|
||||||
if (g_strcmp0 (item->filename, filename) == 0) {
|
|
||||||
g_array_remove_index (items, j);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* append in the local array */
|
|
||||||
g_array_append_val (dir_items, ((struct conffile_iterator_item) {
|
|
||||||
.filename = g_strdup (filename),
|
|
||||||
.path = g_steal_pointer (&path),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sort files of the current dir by filename */
|
|
||||||
g_array_sort (dir_items, (GCompareFunc) conffile_iterator_item_compare);
|
|
||||||
|
|
||||||
/* Append the sorted files to the global array */
|
|
||||||
g_array_append_vals (items, dir_items->data, dir_items->len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Construct iterator */
|
|
||||||
WpIterator *it = wp_iterator_new (&conffile_iterator_methods,
|
|
||||||
sizeof (struct conffile_iterator_data));
|
|
||||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
|
||||||
it_data->items = g_steal_pointer (&items);
|
|
||||||
it_data->idx = 0;
|
|
||||||
return g_steal_pointer (&it);
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2024 Collabora Ltd.
|
|
||||||
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __WIREPLUMBER_BASE_DIRS_H__
|
|
||||||
#define __WIREPLUMBER_BASE_DIRS_H__
|
|
||||||
|
|
||||||
#include "defs.h"
|
|
||||||
#include "iterator.h"
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Flags to specify lookup directories
|
|
||||||
* \ingroup wpbasedirs
|
|
||||||
*
|
|
||||||
* These flags can be used to specify which directories to look for a file in.
|
|
||||||
* The flags can be combined to search in multiple directories at once. Some
|
|
||||||
* flags may also used to specify the type of the file being looked up or other
|
|
||||||
* lookup parameters.
|
|
||||||
*
|
|
||||||
* Lookup is performed in the same order as the flags are listed here. Note that
|
|
||||||
* if a WirePlumber-specific environment variable is set ($WIREPLUMBER_*_DIR)
|
|
||||||
* and the equivalent WP_BASE_DIRS_ENV_* flag is specified, the lookup in other
|
|
||||||
* directories is skipped, even if the file is not found in the
|
|
||||||
* environment-specified directory.
|
|
||||||
*/
|
|
||||||
typedef enum { /*< flags >*/
|
|
||||||
WP_BASE_DIRS_ENV_CONFIG = (1 << 0), /*!< $WIREPLUMBER_CONFIG_DIR */
|
|
||||||
WP_BASE_DIRS_ENV_DATA = (1 << 1), /*!< $WIREPLUMBER_DATA_DIR */
|
|
||||||
WP_BASE_DIRS_ENV_MODULE = (1 << 2), /*!< $WIREPLUMBER_MODULE_DIR */
|
|
||||||
|
|
||||||
WP_BASE_DIRS_XDG_CONFIG_HOME = (1 << 8), /*!< XDG_CONFIG_HOME */
|
|
||||||
WP_BASE_DIRS_XDG_DATA_HOME = (1 << 9), /*!< XDG_DATA_HOME */
|
|
||||||
|
|
||||||
WP_BASE_DIRS_XDG_CONFIG_DIRS = (1 << 10), /*!< XDG_CONFIG_DIRS */
|
|
||||||
WP_BASE_DIRS_BUILD_SYSCONFDIR = (1 << 11), /*!< compile-time $sysconfdir (/etc) */
|
|
||||||
|
|
||||||
WP_BASE_DIRS_XDG_DATA_DIRS = (1 << 12), /*!< XDG_DATA_DIRS */
|
|
||||||
WP_BASE_DIRS_BUILD_DATADIR = (1 << 13), /*!< compile-time $datadir ($prefix/share) */
|
|
||||||
|
|
||||||
WP_BASE_DIRS_BUILD_LIBDIR = (1 << 14), /*!< compile-time $libdir ($prefix/lib) */
|
|
||||||
|
|
||||||
/*! the file is a loadable module; prepend "lib" and append ".so" if needed */
|
|
||||||
WP_BASE_DIRS_FLAG_MODULE = (1 << 24),
|
|
||||||
|
|
||||||
/*! append "/wireplumber" to the location, except in the case of locations
|
|
||||||
that are specified via WirePlumber-specific environment variables;
|
|
||||||
in LIBDIR, append "/wireplumber-$API_version" instead */
|
|
||||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER = (1 << 25),
|
|
||||||
|
|
||||||
WP_BASE_DIRS_CONFIGURATION =
|
|
||||||
(WP_BASE_DIRS_ENV_CONFIG |
|
|
||||||
WP_BASE_DIRS_XDG_CONFIG_HOME |
|
|
||||||
WP_BASE_DIRS_XDG_CONFIG_DIRS |
|
|
||||||
WP_BASE_DIRS_BUILD_SYSCONFDIR |
|
|
||||||
WP_BASE_DIRS_XDG_DATA_DIRS |
|
|
||||||
WP_BASE_DIRS_BUILD_DATADIR |
|
|
||||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
|
|
||||||
|
|
||||||
WP_BASE_DIRS_DATA =
|
|
||||||
(WP_BASE_DIRS_ENV_DATA |
|
|
||||||
WP_BASE_DIRS_XDG_DATA_HOME |
|
|
||||||
WP_BASE_DIRS_XDG_DATA_DIRS |
|
|
||||||
WP_BASE_DIRS_BUILD_DATADIR |
|
|
||||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
|
|
||||||
|
|
||||||
WP_BASE_DIRS_MODULE =
|
|
||||||
(WP_BASE_DIRS_ENV_MODULE |
|
|
||||||
WP_BASE_DIRS_BUILD_LIBDIR |
|
|
||||||
WP_BASE_DIRS_FLAG_MODULE |
|
|
||||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
|
|
||||||
} WpBaseDirsFlags;
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
gchar * wp_base_dirs_find_file (WpBaseDirsFlags flags,
|
|
||||||
const gchar * subdir, const gchar * filename);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpIterator * wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
|
|
||||||
const gchar * subdir, const gchar * suffix);
|
|
||||||
|
|
||||||
G_END_DECLS
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
#include "client.h"
|
#include "client.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "private/pipewire-object-mixin.h"
|
#include "private/pipewire-object-mixin.h"
|
||||||
#include "private/permission-manager.h"
|
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
|
||||||
|
|
||||||
|
|
@ -26,7 +25,6 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
|
||||||
struct _WpClient
|
struct _WpClient
|
||||||
{
|
{
|
||||||
WpGlobalProxy parent;
|
WpGlobalProxy parent;
|
||||||
GWeakRef permission_manager;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static void wp_client_pw_object_mixin_priv_interface_init (
|
static void wp_client_pw_object_mixin_priv_interface_init (
|
||||||
|
|
@ -41,7 +39,6 @@ G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
|
||||||
static void
|
static void
|
||||||
wp_client_init (WpClient * self)
|
wp_client_init (WpClient * self)
|
||||||
{
|
{
|
||||||
g_weak_ref_init (&self->permission_manager, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -79,27 +76,11 @@ wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
|
||||||
static void
|
static void
|
||||||
wp_client_pw_proxy_destroyed (WpProxy * proxy)
|
wp_client_pw_proxy_destroyed (WpProxy * proxy)
|
||||||
{
|
{
|
||||||
WpClient *self = WP_CLIENT (proxy);
|
|
||||||
|
|
||||||
wp_client_attach_permission_manager (self, NULL);
|
|
||||||
|
|
||||||
wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
|
wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
|
||||||
|
|
||||||
WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy);
|
WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
wp_impl_node_finalize (GObject * object)
|
|
||||||
{
|
|
||||||
WpClient *self = WP_CLIENT (object);
|
|
||||||
|
|
||||||
wp_client_attach_permission_manager (self, NULL);
|
|
||||||
|
|
||||||
g_weak_ref_clear (&self->permission_manager);
|
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_client_parent_class)->finalize (object);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
wp_client_class_init (WpClientClass * klass)
|
wp_client_class_init (WpClientClass * klass)
|
||||||
{
|
{
|
||||||
|
|
@ -107,7 +88,6 @@ wp_client_class_init (WpClientClass * klass)
|
||||||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
||||||
WpProxyClass *proxy_class = (WpProxyClass *) klass;
|
WpProxyClass *proxy_class = (WpProxyClass *) klass;
|
||||||
|
|
||||||
object_class->finalize = wp_impl_node_finalize;
|
|
||||||
object_class->get_property = wp_pw_object_mixin_get_property;
|
object_class->get_property = wp_pw_object_mixin_get_property;
|
||||||
|
|
||||||
wpobject_class->get_supported_features =
|
wpobject_class->get_supported_features =
|
||||||
|
|
@ -241,30 +221,3 @@ wp_client_update_properties (WpClient * self, WpProperties * updates)
|
||||||
|
|
||||||
g_warn_if_fail (client_update_properties_result >= 0);
|
g_warn_if_fail (client_update_properties_result >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Attaches a permission manager in the client to handle permissions
|
|
||||||
* automatically.
|
|
||||||
*
|
|
||||||
* \ingroup wpclient
|
|
||||||
* \param self the client
|
|
||||||
* \param pm (transfer none) (nullable): the permission manager to attach, or
|
|
||||||
* NULL to detach the current permission manager.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_client_attach_permission_manager (WpClient *self, WpPermissionManager *pm)
|
|
||||||
{
|
|
||||||
g_autoptr (WpPermissionManager) curr_pm = NULL;
|
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_CLIENT (self));
|
|
||||||
|
|
||||||
curr_pm = g_weak_ref_get (&self->permission_manager);
|
|
||||||
if (curr_pm == pm)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (curr_pm)
|
|
||||||
wp_permission_manager_remove_client (curr_pm, self);
|
|
||||||
if (pm)
|
|
||||||
wp_permission_manager_add_client (pm, self);
|
|
||||||
g_weak_ref_set (&self->permission_manager, pm);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
#define __WIREPLUMBER_CLIENT_H__
|
#define __WIREPLUMBER_CLIENT_H__
|
||||||
|
|
||||||
#include "global-proxy.h"
|
#include "global-proxy.h"
|
||||||
#include "permission-manager.h"
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
|
@ -38,10 +37,6 @@ void wp_client_update_permissions_array (WpClient * self,
|
||||||
WP_API
|
WP_API
|
||||||
void wp_client_update_properties (WpClient * self, WpProperties * updates);
|
void wp_client_update_properties (WpClient * self, WpProperties * updates);
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_client_attach_permission_manager (WpClient *self,
|
|
||||||
WpPermissionManager *pm);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -153,8 +153,8 @@ on_component_loader_load_done (WpComponentLoader * cl, GAsyncResult * res,
|
||||||
* provide if it loads successfully; this can be queried later with
|
* provide if it loads successfully; this can be queried later with
|
||||||
* wp_core_test_feature()
|
* wp_core_test_feature()
|
||||||
* \param cancellable (nullable): optional GCancellable
|
* \param cancellable (nullable): optional GCancellable
|
||||||
* \param callback (scope async)(closure data): the callback to call when the operation is done
|
* \param callback (scope async): the callback to call when the operation is done
|
||||||
* \param data data to pass to \a callback
|
* \param data (closure): data to pass to \a callback
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
wp_core_load_component (WpCore * self, const gchar * component,
|
wp_core_load_component (WpCore * self, const gchar * component,
|
||||||
|
|
|
||||||
733
lib/wp/conf.c
733
lib/wp/conf.c
|
|
@ -6,14 +6,13 @@
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "core.h"
|
||||||
#include "conf.h"
|
#include "conf.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
#include "object-interest.h"
|
||||||
#include "json-utils.h"
|
#include "json-utils.h"
|
||||||
#include "base-dirs.h"
|
|
||||||
#include "error.h"
|
|
||||||
|
|
||||||
#include <pipewire/pipewire.h>
|
#include <pipewire/pipewire.h>
|
||||||
#include <spa/utils/result.h>
|
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
|
||||||
|
|
||||||
|
|
@ -27,40 +26,19 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
|
||||||
* configuration.
|
* configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
typedef struct _WpConfSection WpConfSection;
|
|
||||||
struct _WpConfSection
|
|
||||||
{
|
|
||||||
gchar *name;
|
|
||||||
WpSpaJson *value;
|
|
||||||
gchar *location;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_conf_section_clear (WpConfSection * section)
|
|
||||||
{
|
|
||||||
g_free (section->name);
|
|
||||||
g_clear_pointer (§ion->value, wp_spa_json_unref);
|
|
||||||
g_free (section->location);
|
|
||||||
}
|
|
||||||
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpConfSection, wp_conf_section_clear)
|
|
||||||
|
|
||||||
struct _WpConf
|
struct _WpConf
|
||||||
{
|
{
|
||||||
GObject parent;
|
GObject parent;
|
||||||
|
|
||||||
/* Props */
|
/* Props */
|
||||||
gchar *name;
|
GWeakRef core;
|
||||||
WpProperties *properties;
|
|
||||||
|
|
||||||
/* Private */
|
GHashTable *sections;
|
||||||
GArray *conf_sections; /* element-type: WpConfSection */
|
|
||||||
GPtrArray *files; /* element-type: GMappedFile* */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
PROP_0,
|
PROP_0,
|
||||||
PROP_NAME,
|
PROP_CORE,
|
||||||
PROP_PROPERTIES,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
|
G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
|
||||||
|
|
@ -68,9 +46,10 @@ G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
|
||||||
static void
|
static void
|
||||||
wp_conf_init (WpConf * self)
|
wp_conf_init (WpConf * self)
|
||||||
{
|
{
|
||||||
self->conf_sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
|
g_weak_ref_init (&self->core, NULL);
|
||||||
g_array_set_clear_func (self->conf_sections, (GDestroyNotify) wp_conf_section_clear);
|
|
||||||
self->files = g_ptr_array_new_with_free_func ((GDestroyNotify) g_mapped_file_unref);
|
self->sections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
||||||
|
(GDestroyNotify) wp_spa_json_unref);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -80,11 +59,8 @@ wp_conf_set_property (GObject * object, guint property_id,
|
||||||
WpConf *self = WP_CONF (object);
|
WpConf *self = WP_CONF (object);
|
||||||
|
|
||||||
switch (property_id) {
|
switch (property_id) {
|
||||||
case PROP_NAME:
|
case PROP_CORE:
|
||||||
self->name = g_value_dup_string (value);
|
g_weak_ref_set (&self->core, g_value_get_object (value));
|
||||||
break;
|
|
||||||
case PROP_PROPERTIES:
|
|
||||||
self->properties = g_value_dup_boxed (value);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||||
|
|
@ -99,11 +75,8 @@ wp_conf_get_property (GObject * object, guint property_id,
|
||||||
WpConf *self = WP_CONF (object);
|
WpConf *self = WP_CONF (object);
|
||||||
|
|
||||||
switch (property_id) {
|
switch (property_id) {
|
||||||
case PROP_NAME:
|
case PROP_CORE:
|
||||||
g_value_set_string (value, self->name);
|
g_value_take_object (value, g_weak_ref_get (&self->core));
|
||||||
break;
|
|
||||||
case PROP_PROPERTIES:
|
|
||||||
g_value_take_boxed (value, wp_properties_copy (self->properties));
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||||
|
|
@ -116,11 +89,8 @@ wp_conf_finalize (GObject * object)
|
||||||
{
|
{
|
||||||
WpConf *self = WP_CONF (object);
|
WpConf *self = WP_CONF (object);
|
||||||
|
|
||||||
wp_conf_close (self);
|
g_clear_pointer (&self->sections, g_hash_table_unref);
|
||||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
g_weak_ref_clear (&self->core);
|
||||||
g_clear_pointer (&self->conf_sections, g_array_unref);
|
|
||||||
g_clear_pointer (&self->files, g_ptr_array_unref);
|
|
||||||
g_clear_pointer (&self->name, g_free);
|
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object);
|
G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object);
|
||||||
}
|
}
|
||||||
|
|
@ -134,469 +104,346 @@ wp_conf_class_init (WpConfClass * klass)
|
||||||
object_class->set_property = wp_conf_set_property;
|
object_class->set_property = wp_conf_set_property;
|
||||||
object_class->get_property = wp_conf_get_property;
|
object_class->get_property = wp_conf_get_property;
|
||||||
|
|
||||||
g_object_class_install_property(object_class, PROP_NAME,
|
g_object_class_install_property (object_class, PROP_CORE,
|
||||||
g_param_spec_string ("name", "name", "The name of the configuration file",
|
g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE,
|
||||||
NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||||
|
|
||||||
g_object_class_install_property(object_class, PROP_PROPERTIES,
|
|
||||||
g_param_spec_boxed ("properties", "properties", "WpProperties",
|
|
||||||
WP_TYPE_PROPERTIES, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Creates a new WpConf object
|
* \brief Returns the WpConf instance that is associated with the
|
||||||
|
* given core.
|
||||||
*
|
*
|
||||||
* This does not open the files, it only creates the object. For most use cases,
|
* This method will also create the instance and register it with the core
|
||||||
* you should use wp_conf_new_open() instead.
|
* if it had not been created before.
|
||||||
*
|
*
|
||||||
* \ingroup wpconf
|
* \ingroup wpconf
|
||||||
* \param name the name of the configuration file
|
* \param core the core
|
||||||
* \param properties (transfer full) (nullable): a WpProperties with keys
|
* \returns (transfer full): the WpConf instance
|
||||||
* specifying how to load the WpConf object
|
|
||||||
* \returns (transfer full): a new WpConf object
|
|
||||||
*/
|
*/
|
||||||
WpConf *
|
WpConf *
|
||||||
wp_conf_new (const gchar * name, WpProperties * properties)
|
wp_conf_get_instance (WpCore *core)
|
||||||
{
|
{
|
||||||
g_return_val_if_fail (name, NULL);
|
WpConf *conf = wp_core_find_object (core,
|
||||||
g_autoptr (WpProperties) props = properties;
|
(GEqualFunc) WP_IS_CONF, NULL);
|
||||||
return g_object_new (WP_TYPE_CONF, "name", name,
|
|
||||||
"properties", props,
|
|
||||||
NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
if (G_UNLIKELY (!conf)) {
|
||||||
* \brief Creates a new WpConf object and opens the configuration file and its
|
conf = g_object_new (WP_TYPE_CONF,
|
||||||
* fragments, keeping them mapped in memory for further access.
|
"core", core,
|
||||||
*
|
NULL);
|
||||||
* \ingroup wpconf
|
|
||||||
* \param name the name of the configuration file
|
|
||||||
* \param properties (transfer full) (nullable): a WpProperties with keys
|
|
||||||
* specifying how to load the WpConf object
|
|
||||||
* \param error (out) (nullable): return location for a GError, or NULL
|
|
||||||
* \returns (transfer full) (nullable): a new WpConf object, or NULL
|
|
||||||
* if an error occurred
|
|
||||||
*/
|
|
||||||
WpConf *
|
|
||||||
wp_conf_new_open (const gchar * name, WpProperties * properties, GError ** error)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (name, NULL);
|
|
||||||
|
|
||||||
g_autoptr (WpConf) self = wp_conf_new (name, properties);
|
wp_core_register_object (core, g_object_ref (conf));
|
||||||
if (!wp_conf_open (self, error))
|
|
||||||
return NULL;
|
|
||||||
return g_steal_pointer (&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
wp_info_object (conf, "created wpconf object");
|
||||||
detect_old_conf_format (WpConf * self, GMappedFile *file)
|
|
||||||
{
|
|
||||||
const gchar *data = g_mapped_file_get_contents (file);
|
|
||||||
gsize size = g_mapped_file_get_length (file);
|
|
||||||
|
|
||||||
/* wireplumber 0.4 used to have components of type = config/lua */
|
|
||||||
return g_strrstr_len (data, size, "config/lua") ? TRUE : FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
open_and_load_sections (WpConf * self, const gchar *path, GError ** error)
|
|
||||||
{
|
|
||||||
const gchar *as_section = NULL;
|
|
||||||
|
|
||||||
if (self->properties) {
|
|
||||||
/* as-section="some.name" means that the entire file will be stored as a
|
|
||||||
single JSON value that will be accessible through wp_conf_get_section()
|
|
||||||
using "some.name" - no parsing is done; the value is expected to be a
|
|
||||||
container */
|
|
||||||
as_section = wp_properties_get (self->properties, "as-section");
|
|
||||||
wp_debug_object (self, "Reading config file as single section: %s", as_section);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g_autoptr (GMappedFile) file = g_mapped_file_new (path, FALSE, error);
|
return conf;
|
||||||
if (!file)
|
}
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
if (!g_mapped_file_get_contents (file) || g_mapped_file_get_length (file) == 0) {
|
static gint
|
||||||
wp_notice_object (self, "Ignoring empty configuration file at '%s'", path);
|
merge_section_cb (void *data, const char *location, const char *section,
|
||||||
return TRUE;
|
const char *str, size_t len)
|
||||||
|
{
|
||||||
|
WpSpaJson **res_section = (WpSpaJson **)data;
|
||||||
|
g_autoptr (WpSpaJson) json = NULL;
|
||||||
|
gboolean override;
|
||||||
|
|
||||||
|
g_return_val_if_fail (res_section, -EINVAL);
|
||||||
|
|
||||||
|
override = g_str_has_prefix (section, OVERRIDE_SECTION_PREFIX);
|
||||||
|
if (override)
|
||||||
|
section += strlen (OVERRIDE_SECTION_PREFIX);
|
||||||
|
|
||||||
|
wp_debug ("loading section %s (override=%d) from %s", section, override,
|
||||||
|
location);
|
||||||
|
|
||||||
|
/* Only allow sections to be objects or arrays */
|
||||||
|
json = wp_spa_json_new_wrap_stringn (str, len);
|
||||||
|
if (!wp_spa_json_is_container (json)) {
|
||||||
|
wp_warning (
|
||||||
|
"skipping section %s from %s as it is not JSON object or array",
|
||||||
|
section, location);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* test if the file is a relic from 0.4 */
|
/* Merge section if it was defined previously and the 'override.' prefix is
|
||||||
if (detect_old_conf_format (self, file)) {
|
* not used */
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
if (!override && *res_section) {
|
||||||
"The configuration file at '%s' is likely an old WirePlumber 0.4 config "
|
g_autoptr (WpSpaJson) merged =
|
||||||
"and is not supported anymore. Try removing it.", path);
|
wp_json_utils_merge_containers (*res_section, json);
|
||||||
return FALSE;
|
if (!merged) {
|
||||||
}
|
wp_warning (
|
||||||
|
"skipping merge of %s from %s as JSON values are not compatible",
|
||||||
g_autoptr (GArray) sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
|
section, location);
|
||||||
g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear);
|
return 0;
|
||||||
|
|
||||||
g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (
|
|
||||||
g_mapped_file_get_contents (file), g_mapped_file_get_length (file));
|
|
||||||
|
|
||||||
if (as_section) {
|
|
||||||
WpConfSection section = { 0, };
|
|
||||||
|
|
||||||
if (!wp_spa_json_is_container (json)) {
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"invalid single-section config file start (expected an object or array): %.*s",
|
|
||||||
(int) wp_spa_json_get_size (json), wp_spa_json_get_data (json));
|
|
||||||
return FALSE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section.name = g_strdup (as_section);
|
g_clear_pointer (res_section, wp_spa_json_unref);
|
||||||
section.value = g_steal_pointer (&json);
|
*res_section = g_steal_pointer (&merged);
|
||||||
section.location = g_strdup (path);
|
wp_debug ("section %s from %s loaded", location, section);
|
||||||
g_array_append_val (sections, section);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Otherwise always replace */
|
||||||
else {
|
else {
|
||||||
g_auto (WpConfSection) section = { 0, };
|
g_clear_pointer (res_section, wp_spa_json_unref);
|
||||||
g_autoptr (WpSpaJson) tmp = NULL, toplevel = NULL;
|
*res_section = g_steal_pointer (&json);
|
||||||
g_autoptr (WpSpaJsonParser) parser = wp_spa_json_parser_new_undefined (json);
|
wp_debug ("section %s from %s loaded", location, section);
|
||||||
|
|
||||||
/* get the very first token */
|
|
||||||
tmp = wp_spa_json_parser_get_json (parser);
|
|
||||||
|
|
||||||
/* if the top-level token is an object, parse that instead */
|
|
||||||
if (tmp && wp_spa_json_is_object (tmp)) {
|
|
||||||
g_clear_pointer (&parser, wp_spa_json_parser_unref);
|
|
||||||
toplevel = g_steal_pointer (&tmp);
|
|
||||||
parser = wp_spa_json_parser_new_object (toplevel);
|
|
||||||
tmp = wp_spa_json_parser_get_json (parser);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (TRUE) {
|
|
||||||
if (!tmp)
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* if !is_string, but we want to support strings without quotes */
|
|
||||||
if (wp_spa_json_is_container (tmp) ||
|
|
||||||
wp_spa_json_is_int (tmp) ||
|
|
||||||
wp_spa_json_is_float (tmp) ||
|
|
||||||
wp_spa_json_is_boolean (tmp) ||
|
|
||||||
wp_spa_json_is_null (tmp))
|
|
||||||
{
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"invalid section name (not a string): %.*s",
|
|
||||||
(int) wp_spa_json_get_size (tmp), wp_spa_json_get_data (tmp));
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.name = wp_spa_json_parse_string (tmp);
|
|
||||||
g_clear_pointer (&tmp, wp_spa_json_unref);
|
|
||||||
|
|
||||||
/* parse the section contents */
|
|
||||||
tmp = wp_spa_json_parser_get_json (parser);
|
|
||||||
if (!tmp) {
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"section '%s' has no value", section.name);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.value = g_steal_pointer (&tmp);
|
|
||||||
section.location = g_strdup (path);
|
|
||||||
g_array_append_val (sections, section);
|
|
||||||
memset (§ion, 0, sizeof (section));
|
|
||||||
|
|
||||||
/* parse the next section name */
|
|
||||||
tmp = wp_spa_json_parser_get_json (parser);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* store the mapped file and the sections; note that the stored WpSpaJson
|
return 0;
|
||||||
still point to the data in the GMappedFile, so this is why we keep the
|
|
||||||
GMappedFile alive */
|
|
||||||
g_ptr_array_add (self->files, g_steal_pointer (&file));
|
|
||||||
g_array_append_vals (self->conf_sections, sections->data, sections->len);
|
|
||||||
g_array_set_clear_func (sections, NULL);
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
static void
|
||||||
* \brief Opens the configuration file and its fragments and keeps them
|
ensure_section_loaded (WpConf *self, const gchar *section)
|
||||||
* mapped in memory for further access.
|
|
||||||
*
|
|
||||||
* \ingroup wpconf
|
|
||||||
* \param self the configuration
|
|
||||||
* \param error (out)(nullable): return location for a GError, or NULL
|
|
||||||
* \returns TRUE on success, FALSE on error
|
|
||||||
*/
|
|
||||||
gboolean
|
|
||||||
wp_conf_open (WpConf * self, GError ** error)
|
|
||||||
{
|
{
|
||||||
const gchar *no_frags = NULL;
|
g_autoptr (WpCore) core = NULL;
|
||||||
|
struct pw_context *pw_ctx = NULL;
|
||||||
|
g_autoptr (WpSpaJson) json_section = NULL;
|
||||||
|
g_autofree gchar *override_section = NULL;
|
||||||
|
|
||||||
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
if (g_hash_table_contains (self->sections, section))
|
||||||
|
return;
|
||||||
|
|
||||||
g_autofree gchar *path = NULL;
|
core = g_weak_ref_get (&self->core);
|
||||||
g_autoptr (WpIterator) iterator = NULL;
|
g_return_if_fail (core);
|
||||||
g_auto (GValue) value = G_VALUE_INIT;
|
pw_ctx = wp_core_get_pw_context (core);
|
||||||
|
g_return_if_fail (pw_ctx);
|
||||||
|
|
||||||
if (self->properties) {
|
pw_context_conf_section_for_each (pw_ctx, section, merge_section_cb,
|
||||||
no_frags = wp_properties_get (self->properties, "no-fragments");
|
&json_section);
|
||||||
}
|
override_section = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", section);
|
||||||
|
pw_context_conf_section_for_each (pw_ctx, override_section, merge_section_cb,
|
||||||
|
&json_section);
|
||||||
|
|
||||||
/*
|
if (json_section)
|
||||||
* open the config file - if the path supplied is absolute,
|
g_hash_table_insert (self->sections, g_strdup (section),
|
||||||
* wp_base_dirs_find_file will ignore WP_BASE_DIRS_CONFIGURATION
|
g_steal_pointer (&json_section));
|
||||||
*/
|
|
||||||
path = wp_base_dirs_find_file (WP_BASE_DIRS_CONFIGURATION, NULL, self->name);
|
|
||||||
if (path) {
|
|
||||||
wp_info_object (self, "opening config file: %s", path);
|
|
||||||
if (!open_and_load_sections (self, path, error))
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
g_clear_pointer (&path, g_free);
|
|
||||||
|
|
||||||
/* open the .conf.d/ fragments */
|
|
||||||
if (!no_frags) {
|
|
||||||
path = g_strdup_printf ("%s.d", self->name);
|
|
||||||
iterator = wp_base_dirs_new_files_iterator (WP_BASE_DIRS_CONFIGURATION, path,
|
|
||||||
".conf");
|
|
||||||
|
|
||||||
for (; wp_iterator_next (iterator, &value); g_value_unset (&value)) {
|
|
||||||
const gchar *filename = g_value_get_string (&value);
|
|
||||||
|
|
||||||
wp_info_object (self, "opening fragment file: %s", filename);
|
|
||||||
|
|
||||||
g_autoptr (GError) e = NULL;
|
|
||||||
if (!open_and_load_sections (self, filename, &e)) {
|
|
||||||
wp_warning_object (self, "failed to open '%s': %s", filename, e->message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self->files->len == 0) {
|
|
||||||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
|
||||||
"Could not locate configuration file '%s'", self->name);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Closes the configuration file and its fragments
|
|
||||||
*
|
|
||||||
* \ingroup wpconf
|
|
||||||
* \param self the configuration
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_conf_close (WpConf * self)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_CONF (self));
|
|
||||||
|
|
||||||
g_array_set_size (self->conf_sections, 0);
|
|
||||||
g_ptr_array_set_size (self->files, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Tests if the configuration files are open
|
|
||||||
*
|
|
||||||
* \ingroup wpconf
|
|
||||||
* \param self the configuration
|
|
||||||
* \returns TRUE if the configuration files are open, FALSE otherwise
|
|
||||||
*/
|
|
||||||
gboolean
|
|
||||||
wp_conf_is_open (WpConf * self)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
|
||||||
return self->files->len > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the name of the configuration file
|
|
||||||
*
|
|
||||||
* \ingroup wpconf
|
|
||||||
* \param self the configuration
|
|
||||||
* \returns the name of the configuration file
|
|
||||||
*/
|
|
||||||
const gchar *
|
|
||||||
wp_conf_get_name (WpConf * self)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
|
||||||
return self->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
static WpSpaJson *
|
|
||||||
ensure_merged_section (WpConf * self, const gchar *section)
|
|
||||||
{
|
|
||||||
g_autoptr (WpSpaJson) merged = NULL;
|
|
||||||
WpConfSection *merged_section = NULL;
|
|
||||||
|
|
||||||
/* check if the section is already merged */
|
|
||||||
for (guint i = 0; i < self->conf_sections->len; i++) {
|
|
||||||
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
|
|
||||||
if (g_str_equal (s->name, section)) {
|
|
||||||
if (!s->location) {
|
|
||||||
wp_debug_object (self, "section %s is already merged", section);
|
|
||||||
return wp_spa_json_ref (s->value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Iterate over the sections and merge them */
|
|
||||||
for (guint i = 0; i < self->conf_sections->len; i++) {
|
|
||||||
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
|
|
||||||
const gchar *s_name = s->name;
|
|
||||||
|
|
||||||
/* skip the "override." prefix and take a note */
|
|
||||||
gboolean override = g_str_has_prefix (s_name, OVERRIDE_SECTION_PREFIX);
|
|
||||||
if (override)
|
|
||||||
s_name += strlen (OVERRIDE_SECTION_PREFIX);
|
|
||||||
|
|
||||||
if (g_str_equal (s_name, section)) {
|
|
||||||
/* Merge sections if a previous value exists and
|
|
||||||
the 'override.' prefix is not present */
|
|
||||||
if (!override && merged) {
|
|
||||||
g_autoptr (WpSpaJson) new_merged =
|
|
||||||
wp_json_utils_merge_containers (merged, s->value);
|
|
||||||
if (!merged) {
|
|
||||||
wp_warning_object (self,
|
|
||||||
"skipping merge of '%s' from '%s' as JSON containers are not compatible",
|
|
||||||
section, s->location);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_clear_pointer (&merged, wp_spa_json_unref);
|
|
||||||
merged = g_steal_pointer (&new_merged);
|
|
||||||
merged_section = NULL;
|
|
||||||
}
|
|
||||||
/* Otherwise always replace */
|
|
||||||
else {
|
|
||||||
g_clear_pointer (&merged, wp_spa_json_unref);
|
|
||||||
merged = wp_spa_json_ref (s->value);
|
|
||||||
merged_section = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cache the result */
|
|
||||||
if (merged_section) {
|
|
||||||
/* if the merged json came from a single location, just clear
|
|
||||||
the location from that WpConfSection to mark it as the result */
|
|
||||||
wp_info_object (self, "section '%s' is used as-is from '%s'", section,
|
|
||||||
merged_section->location);
|
|
||||||
g_clear_pointer (&merged_section->location, g_free);
|
|
||||||
} else if (merged) {
|
|
||||||
/* if the merged json came from multiple locations, create a new
|
|
||||||
WpConfSection to store it */
|
|
||||||
WpConfSection s = { g_strdup (section), wp_spa_json_ref (merged), NULL };
|
|
||||||
g_array_append_val (self->conf_sections, s);
|
|
||||||
wp_info_object (self, "section '%s' is merged from multiple locations",
|
|
||||||
section);
|
|
||||||
} else {
|
|
||||||
wp_info_object (self, "section '%s' is not defined", section);
|
|
||||||
}
|
|
||||||
|
|
||||||
return g_steal_pointer (&merged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* This method will get the JSON value of a specific section from the
|
* This method will get the JSON value of a specific section from the
|
||||||
* configuration. If the same section is defined in multiple locations, the
|
* configuration. If the same section is defined in multiple locations, the
|
||||||
* sections with the same name will be either merged in case of arrays and
|
* sections with the same name will be either merged in case of arrays and
|
||||||
* objects, or overridden in case of boolean, int, double and strings.
|
* objects, or overridden in case of boolean, int, double and strings. The
|
||||||
|
* passed fallback value will be returned if the section does not exist.
|
||||||
*
|
*
|
||||||
* \ingroup wpconf
|
* \ingroup wpconf
|
||||||
* \param self the configuration
|
* \param self the configuration
|
||||||
* \param section the section name
|
* \param section the section name
|
||||||
* \returns (transfer full) (nullable): the JSON value of the section or NULL
|
* \param fallback (transfer full)(nullable): the fallback value
|
||||||
* if the section does not exist
|
* \returns (transfer full): the JSON value of the section
|
||||||
*/
|
*/
|
||||||
WpSpaJson *
|
WpSpaJson *
|
||||||
wp_conf_get_section (WpConf *self, const gchar *section)
|
wp_conf_get_section (WpConf *self, const gchar *section, WpSpaJson *fallback)
|
||||||
{
|
{
|
||||||
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
WpSpaJson *s;
|
||||||
g_return_val_if_fail (section, NULL);
|
g_autoptr (WpSpaJson) fb = fallback;
|
||||||
|
|
||||||
return ensure_merged_section (self, section);
|
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||||
|
|
||||||
|
ensure_section_loaded (self, section);
|
||||||
|
|
||||||
|
s = g_hash_table_lookup (self->sections, section);
|
||||||
|
if (!s)
|
||||||
|
return fb ? g_steal_pointer (&fb) : NULL;
|
||||||
|
|
||||||
|
return wp_spa_json_ref (s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Updates the given properties with the values of a specific section
|
* This is a convenient function to access a JSON value from an object
|
||||||
* from the configuration.
|
* section in the configuration. If the section is an array, or the key does
|
||||||
|
* not exist in the object section, it will return the passed fallback value.
|
||||||
*
|
*
|
||||||
* \ingroup wpconf
|
* \ingroup wpconf
|
||||||
* \param self the configuration
|
* \param self the configuration
|
||||||
* \param section the section name
|
* \param section the section name
|
||||||
* \param props the properties to update
|
* \param key the key name
|
||||||
* \returns the number of properties updated
|
* \param fallback (transfer full)(nullable): the fallback value
|
||||||
|
* \returns (transfer full): the JSON value of the section's key if it exists,
|
||||||
|
* or the passed fallback value otherwise
|
||||||
*/
|
*/
|
||||||
gint
|
WpSpaJson *
|
||||||
wp_conf_section_update_props (WpConf *self, const gchar *section,
|
wp_conf_get_value (WpConf *self, const gchar *section, const gchar *key,
|
||||||
WpProperties *props)
|
WpSpaJson *fallback)
|
||||||
{
|
{
|
||||||
g_autoptr (WpSpaJson) json = NULL;
|
g_autoptr (WpSpaJson) s = NULL;
|
||||||
|
g_autoptr (WpSpaJson) fb = fallback;
|
||||||
|
WpSpaJson *v;
|
||||||
|
|
||||||
g_return_val_if_fail (WP_IS_CONF (self), -1);
|
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||||
g_return_val_if_fail (section, -1);
|
g_return_val_if_fail (section, NULL);
|
||||||
g_return_val_if_fail (props, -1);
|
g_return_val_if_fail (key, NULL);
|
||||||
|
|
||||||
json = wp_conf_get_section (self, section);
|
s = wp_conf_get_section (self, section, NULL);
|
||||||
if (!json)
|
if (!s)
|
||||||
return 0;
|
goto return_fallback;
|
||||||
return wp_properties_update_from_json (props, json);
|
|
||||||
|
if (!wp_spa_json_is_object (s)) {
|
||||||
|
wp_warning_object (self,
|
||||||
|
"Cannot get JSON key %s from %s as section is not an JSON object",
|
||||||
|
key, section);
|
||||||
|
goto return_fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wp_spa_json_object_get (s, key, "J", &v, NULL))
|
||||||
|
return v;
|
||||||
|
|
||||||
|
return_fallback:
|
||||||
|
return fb ? g_steal_pointer (&fb) : NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "private/parse-conf-section.c"
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Parses standard pw_context sections from \a conf
|
* This is a convenient function to access a boolean value from an object
|
||||||
|
* section in the configuration. If the section is an array, or the key does
|
||||||
|
* not exist in the object section, it will return the passed fallback value.
|
||||||
*
|
*
|
||||||
* \ingroup wpconf
|
* \ingroup wpconf
|
||||||
* \param self the configuration
|
* \param self the configuration
|
||||||
* \param context the associated pw_context
|
* \param section the section name
|
||||||
|
* \param key the key name
|
||||||
|
* \param fallback the fallback value
|
||||||
|
* \returns the boolean value of the section's key if it exists and could be
|
||||||
|
* parsed, or the passed fallback value otherwise
|
||||||
*/
|
*/
|
||||||
void
|
gboolean
|
||||||
wp_conf_parse_pw_context_sections (WpConf * self, struct pw_context * context)
|
wp_conf_get_value_boolean (WpConf *self, const gchar *section,
|
||||||
|
const gchar *key, gboolean fallback)
|
||||||
{
|
{
|
||||||
gint res;
|
g_autoptr (WpSpaJson) s = NULL;
|
||||||
WpProperties *conf_wp;
|
gboolean v;
|
||||||
struct pw_properties *conf_pw;
|
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_CONF (self));
|
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
||||||
g_return_if_fail (context);
|
g_return_val_if_fail (section, FALSE);
|
||||||
|
g_return_val_if_fail (key, FALSE);
|
||||||
|
|
||||||
/* convert needed sections into a pipewire-style conf dictionary */
|
s = wp_conf_get_section (self, section, NULL);
|
||||||
conf_wp = wp_properties_new ("config.path", "wpconf", NULL);
|
if (!s)
|
||||||
{
|
return fallback;
|
||||||
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.spa-libs");
|
|
||||||
if (j) {
|
if (!wp_spa_json_is_object (s)) {
|
||||||
g_autofree gchar *js = wp_spa_json_parse_string (j);
|
wp_warning_object (self,
|
||||||
wp_properties_set (conf_wp, "context.spa-libs", js);
|
"Cannot get boolean key %s from %s as section is not an JSON object",
|
||||||
}
|
key, section);
|
||||||
|
return fallback;
|
||||||
}
|
}
|
||||||
{
|
|
||||||
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.modules");
|
|
||||||
if (j) {
|
|
||||||
g_autofree gchar *js = wp_spa_json_parse_string (j);
|
|
||||||
wp_properties_set (conf_wp, "context.modules", js);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conf_pw = wp_properties_unref_and_take_pw_properties (conf_wp);
|
|
||||||
|
|
||||||
/* parse sections */
|
return wp_spa_json_object_get (s, key, "b", &v, NULL) ? v : fallback;
|
||||||
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.spa-libs")) < 0)
|
}
|
||||||
goto error;
|
|
||||||
wp_info_object (self, "parsed %d context.spa-libs items", res);
|
/*!
|
||||||
|
* This is a convenient function to access a int value from an object
|
||||||
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.modules")) < 0)
|
* section in the configuration. If the section is an array, or the key does
|
||||||
goto error;
|
* not exist in the object section, it will return the passed fallback value.
|
||||||
if (res > 0)
|
*
|
||||||
wp_info_object (self, "parsed %d context.modules items", res);
|
* \ingroup wpconf
|
||||||
else
|
* \param self the configuration
|
||||||
wp_warning_object (self, "no modules loaded from context.modules");
|
* \param section the section name
|
||||||
|
* \param key the key name
|
||||||
out:
|
* \param fallback the fallback value
|
||||||
pw_properties_free (conf_pw);
|
* \returns the int value of the section's key if it exists and could be
|
||||||
return;
|
* parsed, or the passed fallback value otherwise
|
||||||
|
*/
|
||||||
error:
|
gint
|
||||||
wp_critical_object (self, "failed to parse pw_context sections: %s",
|
wp_conf_get_value_int (WpConf *self, const gchar *section,
|
||||||
spa_strerror (res));
|
const gchar *key, gint fallback)
|
||||||
goto out;
|
{
|
||||||
|
g_autoptr (WpSpaJson) s = NULL;
|
||||||
|
gint v;
|
||||||
|
|
||||||
|
g_return_val_if_fail (WP_IS_CONF (self), 0);
|
||||||
|
g_return_val_if_fail (section, 0);
|
||||||
|
g_return_val_if_fail (key, 0);
|
||||||
|
|
||||||
|
s = wp_conf_get_section (self, section, NULL);
|
||||||
|
if (!s)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
if (!wp_spa_json_is_object (s)) {
|
||||||
|
wp_warning_object (self,
|
||||||
|
"Cannot get int key %s from %s as section is not an JSON object",
|
||||||
|
key, section);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wp_spa_json_object_get (s, key, "i", &v, NULL) ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* This is a convenient function to access a float value from an object
|
||||||
|
* section in the configuration. If the section is an array, or the key does
|
||||||
|
* not exist in the object section, it will return the passed fallback value.
|
||||||
|
*
|
||||||
|
* \ingroup wpconf
|
||||||
|
* \param self the configuration
|
||||||
|
* \param section the section name
|
||||||
|
* \param key the key name
|
||||||
|
* \param fallback the fallback value
|
||||||
|
* \returns the float value of the section's key if it exists and could be
|
||||||
|
* parsed, or the passed fallback value otherwise
|
||||||
|
*/
|
||||||
|
float
|
||||||
|
wp_conf_get_value_float (WpConf *self, const gchar *section,
|
||||||
|
const gchar *key, float fallback)
|
||||||
|
{
|
||||||
|
g_autoptr (WpSpaJson) s = NULL;
|
||||||
|
float v;
|
||||||
|
|
||||||
|
g_return_val_if_fail (WP_IS_CONF (self), 0);
|
||||||
|
g_return_val_if_fail (section, 0);
|
||||||
|
g_return_val_if_fail (key, 0);
|
||||||
|
|
||||||
|
s = wp_conf_get_section (self, section, NULL);
|
||||||
|
if (!s)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
if (!wp_spa_json_is_object (s)) {
|
||||||
|
wp_warning_object (self,
|
||||||
|
"Cannot get float key %s from %s as section is not an JSON object",
|
||||||
|
key, section);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wp_spa_json_object_get (s, key, "f", &v, NULL) ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* This is a convenient function to access a string value from an object
|
||||||
|
* section in the configuration. If the section is an array, or the key does
|
||||||
|
* not exist in the object section, it will return the passed fallback value.
|
||||||
|
*
|
||||||
|
* \ingroup wpconf
|
||||||
|
* \param self the configuration
|
||||||
|
* \param section the section name
|
||||||
|
* \param key the key name
|
||||||
|
* \param fallback (nullable): the fallback value
|
||||||
|
* \returns (transfer full): the string value of the section's key if it exists
|
||||||
|
* and could be parsed, or the passed fallback value otherwise
|
||||||
|
*/
|
||||||
|
gchar *
|
||||||
|
wp_conf_get_value_string (WpConf *self, const gchar *section,
|
||||||
|
const gchar *key, const gchar *fallback)
|
||||||
|
{
|
||||||
|
g_autoptr (WpSpaJson) s = NULL;
|
||||||
|
gchar *v;
|
||||||
|
|
||||||
|
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||||
|
g_return_val_if_fail (section, NULL);
|
||||||
|
g_return_val_if_fail (key, NULL);
|
||||||
|
|
||||||
|
s = wp_conf_get_section (self, section, NULL);
|
||||||
|
if (!s)
|
||||||
|
goto return_fallback;
|
||||||
|
|
||||||
|
if (!wp_spa_json_is_object (s)) {
|
||||||
|
wp_warning_object (self,
|
||||||
|
"Cannot get string key %s from %s as section is not an JSON object",
|
||||||
|
key, section);
|
||||||
|
goto return_fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wp_spa_json_object_get (s, key, "s", &v, NULL))
|
||||||
|
return v;
|
||||||
|
|
||||||
|
return_fallback:
|
||||||
|
return fallback ? g_strdup (fallback) : NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
struct pw_context;
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief The WpConf GType
|
* \brief The WpConf GType
|
||||||
* \ingroup wpconf
|
* \ingroup wpconf
|
||||||
|
|
@ -26,34 +24,31 @@ WP_API
|
||||||
G_DECLARE_FINAL_TYPE (WpConf, wp_conf, WP, CONF, GObject)
|
G_DECLARE_FINAL_TYPE (WpConf, wp_conf, WP, CONF, GObject)
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpConf * wp_conf_new (const gchar * name, WpProperties * properties);
|
WpConf * wp_conf_get_instance (WpCore * core);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpConf * wp_conf_new_open (const gchar * name, WpProperties * properties,
|
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section,
|
||||||
GError ** error);
|
WpSpaJson *fallback);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_conf_open (WpConf * self, GError ** error);
|
WpSpaJson *wp_conf_get_value (WpConf *self,
|
||||||
|
const gchar *section, const gchar *key, WpSpaJson *fallback);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
void wp_conf_close (WpConf * self);
|
gboolean wp_conf_get_value_boolean (WpConf *self,
|
||||||
|
const gchar *section, const gchar *key, gboolean fallback);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_conf_is_open (WpConf * self);
|
gint wp_conf_get_value_int (WpConf *self,
|
||||||
|
const gchar *section, const gchar *key, gint fallback);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
const gchar * wp_conf_get_name (WpConf * self);
|
float wp_conf_get_value_float (WpConf *self,
|
||||||
|
const gchar *section, const gchar *key, float fallback);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section);
|
gchar *wp_conf_get_value_string (WpConf *self,
|
||||||
|
const gchar *section, const gchar *key, const gchar *fallback);
|
||||||
WP_API
|
|
||||||
gint wp_conf_section_update_props (WpConf * self, const gchar * section,
|
|
||||||
WpProperties * props);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_conf_parse_pw_context_sections (WpConf * self,
|
|
||||||
struct pw_context * context);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
|
|
|
||||||
252
lib/wp/core.c
252
lib/wp/core.c
|
|
@ -30,23 +30,20 @@ struct _WpLoopSource
|
||||||
{
|
{
|
||||||
GSource parent;
|
GSource parent;
|
||||||
struct pw_loop *loop;
|
struct pw_loop *loop;
|
||||||
gboolean entered;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
{
|
{
|
||||||
WpLoopSource *ls = WP_LOOP_SOURCE (s);
|
|
||||||
int result;
|
int result;
|
||||||
|
|
||||||
if (!ls->entered) {
|
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
|
||||||
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
|
|
||||||
pw_loop_enter (ls->loop);
|
|
||||||
ls->entered = TRUE;
|
|
||||||
g_source_set_ready_time (s, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = pw_loop_iterate (ls->loop, 0);
|
pw_loop_enter (WP_LOOP_SOURCE(s)->loop);
|
||||||
|
result = pw_loop_iterate (WP_LOOP_SOURCE(s)->loop, 0);
|
||||||
|
pw_loop_leave (WP_LOOP_SOURCE(s)->loop);
|
||||||
|
|
||||||
|
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
|
||||||
|
|
||||||
if (G_UNLIKELY (result < 0))
|
if (G_UNLIKELY (result < 0))
|
||||||
wp_warning_boxed (G_TYPE_SOURCE, s,
|
wp_warning_boxed (G_TYPE_SOURCE, s,
|
||||||
|
|
@ -58,21 +55,7 @@ wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
static void
|
static void
|
||||||
wp_loop_source_finalize (GSource * s)
|
wp_loop_source_finalize (GSource * s)
|
||||||
{
|
{
|
||||||
WpLoopSource *ls = WP_LOOP_SOURCE (s);
|
pw_loop_destroy (WP_LOOP_SOURCE(s)->loop);
|
||||||
|
|
||||||
wp_trace_boxed (G_TYPE_SOURCE, s, "finalize loop source");
|
|
||||||
|
|
||||||
/* Source should be left from the thread it was entered from.
|
|
||||||
*
|
|
||||||
* This puts additional restrictions to upper layers on how WpLoopSource (and
|
|
||||||
* WpCore) can be used: they must be finalized from the GMainContext thread.
|
|
||||||
*/
|
|
||||||
if (ls->entered) {
|
|
||||||
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
|
|
||||||
pw_loop_leave (ls->loop);
|
|
||||||
}
|
|
||||||
|
|
||||||
pw_loop_destroy (ls->loop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static GSourceFuncs source_funcs = {
|
static GSourceFuncs source_funcs = {
|
||||||
|
|
@ -92,9 +75,6 @@ wp_loop_source_new (void)
|
||||||
pw_loop_get_fd (WP_LOOP_SOURCE(s)->loop),
|
pw_loop_get_fd (WP_LOOP_SOURCE(s)->loop),
|
||||||
G_IO_IN | G_IO_ERR | G_IO_HUP);
|
G_IO_IN | G_IO_ERR | G_IO_HUP);
|
||||||
|
|
||||||
/* dispatch immediately to enter the loop */
|
|
||||||
g_source_set_ready_time (s, 0);
|
|
||||||
|
|
||||||
return (GSource *) s;
|
return (GSource *) s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,25 +96,6 @@ wp_loop_source_new (void)
|
||||||
* objects that appear in the registry, making them accessible through
|
* objects that appear in the registry, making them accessible through
|
||||||
* the WpObjectManager API.
|
* the WpObjectManager API.
|
||||||
*
|
*
|
||||||
* The core is also responsible for loading components, which are defined in
|
|
||||||
* the main configuration file. Components are loaded when
|
|
||||||
* WP_CORE_FEATURE_COMPONENTS is activated.
|
|
||||||
*
|
|
||||||
* \b Configuration
|
|
||||||
*
|
|
||||||
* The main configuration file needs to be created and opened before the core
|
|
||||||
* is created, using the WpConf API. It is then passed to the core as an
|
|
||||||
* argument in the constructor.
|
|
||||||
*
|
|
||||||
* If a configuration file is not provided, the core will let the underlying
|
|
||||||
* `pw_context` load its own configuration, based on the rules that apply to
|
|
||||||
* all pipewire clients (e.g. it respects the `PIPEWIRE_CONFIG_NAME` environment
|
|
||||||
* variable and loads "client.conf" as a last resort).
|
|
||||||
*
|
|
||||||
* If a configuration file is provided, the core does not let the underlying
|
|
||||||
* `pw_context` load any configuration and instead uses the provided WpConf
|
|
||||||
* object.
|
|
||||||
*
|
|
||||||
* \gproperties
|
* \gproperties
|
||||||
*
|
*
|
||||||
* \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
|
* \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
|
||||||
|
|
@ -149,9 +110,6 @@ wp_loop_source_new (void)
|
||||||
* \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE,
|
* \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE,
|
||||||
* The pipewire core}
|
* The pipewire core}
|
||||||
*
|
*
|
||||||
* \gproperty{conf, WpConf *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
|
|
||||||
* The main configuration file}
|
|
||||||
*
|
|
||||||
* \gsignals
|
* \gsignals
|
||||||
*
|
*
|
||||||
* \par connected
|
* \par connected
|
||||||
|
|
@ -193,25 +151,16 @@ struct _WpCore
|
||||||
struct spa_hook core_listener;
|
struct spa_hook core_listener;
|
||||||
struct spa_hook proxy_core_listener;
|
struct spa_hook proxy_core_listener;
|
||||||
|
|
||||||
/* the main configuration file */
|
|
||||||
WpConf *conf;
|
|
||||||
|
|
||||||
WpRegistry registry;
|
WpRegistry registry;
|
||||||
GHashTable *async_tasks; // <int seq, GTask*>
|
GHashTable *async_tasks; // <int seq, GTask*>
|
||||||
};
|
};
|
||||||
|
|
||||||
struct context_data {
|
|
||||||
grefcount rc;
|
|
||||||
GSource *loop_source;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
PROP_0,
|
PROP_0,
|
||||||
PROP_G_MAIN_CONTEXT,
|
PROP_G_MAIN_CONTEXT,
|
||||||
PROP_PROPERTIES,
|
PROP_PROPERTIES,
|
||||||
PROP_PW_CONTEXT,
|
PROP_PW_CONTEXT,
|
||||||
PROP_PW_CORE,
|
PROP_PW_CORE,
|
||||||
PROP_CONF,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
|
|
@ -327,32 +276,16 @@ static void
|
||||||
wp_core_constructed (GObject *object)
|
wp_core_constructed (GObject *object)
|
||||||
{
|
{
|
||||||
WpCore *self = WP_CORE (object);
|
WpCore *self = WP_CORE (object);
|
||||||
|
g_autoptr (GSource) source = NULL;
|
||||||
|
|
||||||
|
/* loop */
|
||||||
|
source = wp_loop_source_new ();
|
||||||
|
g_source_attach (source, self->g_main_context);
|
||||||
|
|
||||||
/* context */
|
/* context */
|
||||||
if (!self->pw_context) {
|
if (!self->pw_context) {
|
||||||
struct pw_properties *p = NULL;
|
struct pw_properties *p = NULL;
|
||||||
const gchar *str = NULL;
|
const gchar *str = NULL;
|
||||||
g_autoptr (GSource) source = wp_loop_source_new ();
|
|
||||||
|
|
||||||
/* use our own configuration file, if specified */
|
|
||||||
if (self->conf) {
|
|
||||||
wp_info_object (self, "using configuration file: %s",
|
|
||||||
wp_conf_get_name (self->conf));
|
|
||||||
|
|
||||||
/* ensure we have our very own properties set,
|
|
||||||
since we are going to modify it */
|
|
||||||
self->properties = self->properties ?
|
|
||||||
wp_properties_ensure_unique_owner (self->properties) :
|
|
||||||
wp_properties_new_empty ();
|
|
||||||
|
|
||||||
/* load context.properties */
|
|
||||||
wp_conf_section_update_props (self->conf, "context.properties",
|
|
||||||
self->properties);
|
|
||||||
|
|
||||||
/* disable loading of a configuration file in pw_context */
|
|
||||||
wp_properties_set (self->properties, PW_KEY_CONFIG_NAME, "null");
|
|
||||||
wp_properties_set (self->properties, "context.modules.allow-empty", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* properties are fully stored in the pw_context, no need to keep a copy */
|
/* properties are fully stored in the pw_context, no need to keep a copy */
|
||||||
p = self->properties ?
|
p = self->properties ?
|
||||||
|
|
@ -360,34 +293,26 @@ wp_core_constructed (GObject *object)
|
||||||
self->properties = NULL;
|
self->properties = NULL;
|
||||||
|
|
||||||
self->pw_context = pw_context_new (WP_LOOP_SOURCE(source)->loop, p,
|
self->pw_context = pw_context_new (WP_LOOP_SOURCE(source)->loop, p,
|
||||||
sizeof (struct context_data));
|
sizeof (grefcount));
|
||||||
g_return_if_fail (self->pw_context);
|
g_return_if_fail (self->pw_context);
|
||||||
|
|
||||||
/* use the same config option as pipewire to set the log level */
|
/* use the same config option as pipewire to set the log level */
|
||||||
p = (struct pw_properties *) pw_context_get_properties (self->pw_context);
|
p = (struct pw_properties *) pw_context_get_properties (self->pw_context);
|
||||||
if (!g_getenv("WIREPLUMBER_DEBUG") &&
|
if (!g_getenv("WIREPLUMBER_DEBUG") &&
|
||||||
(str = pw_properties_get(p, "log.level")) != NULL) {
|
(str = pw_properties_get(p, "log.level")) != NULL) {
|
||||||
if (!wp_log_set_level (str))
|
if (!wp_log_set_global_level (str))
|
||||||
wp_warning ("ignoring invalid log.level in config file: %s", str);
|
wp_warning ("ignoring invalid log.level in config file: %s", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* parse pw_context specific configuration sections */
|
|
||||||
if (self->conf)
|
|
||||||
wp_conf_parse_pw_context_sections (self->conf, self->pw_context);
|
|
||||||
|
|
||||||
/* Init refcount */
|
/* Init refcount */
|
||||||
struct context_data *cd = pw_context_get_user_data (self->pw_context);
|
grefcount *rc = pw_context_get_user_data (self->pw_context);
|
||||||
g_return_if_fail (cd);
|
g_return_if_fail (rc);
|
||||||
g_ref_count_init (&cd->rc);
|
g_ref_count_init (rc);
|
||||||
cd->loop_source = g_source_ref (source);
|
|
||||||
|
|
||||||
/* Start source */
|
|
||||||
g_source_attach (source, self->g_main_context);
|
|
||||||
} else {
|
} else {
|
||||||
/* Increase refcount */
|
/* Increase refcount */
|
||||||
struct context_data *cd = pw_context_get_user_data (self->pw_context);
|
grefcount *rc = pw_context_get_user_data (self->pw_context);
|
||||||
g_return_if_fail (cd);
|
g_return_if_fail (rc);
|
||||||
g_ref_count_inc (&cd->rc);
|
g_ref_count_inc (rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_core_parent_class)->constructed (object);
|
G_OBJECT_CLASS (wp_core_parent_class)->constructed (object);
|
||||||
|
|
@ -408,24 +333,18 @@ static void
|
||||||
wp_core_finalize (GObject * obj)
|
wp_core_finalize (GObject * obj)
|
||||||
{
|
{
|
||||||
WpCore *self = WP_CORE (obj);
|
WpCore *self = WP_CORE (obj);
|
||||||
struct context_data *cd = pw_context_get_user_data (self->pw_context);
|
grefcount *rc = pw_context_get_user_data (self->pw_context);
|
||||||
g_return_if_fail (cd);
|
g_return_if_fail (rc);
|
||||||
|
|
||||||
wp_core_disconnect (self);
|
wp_core_disconnect (self);
|
||||||
|
|
||||||
/* Clear pw-context if refcount reaches 0 */
|
/* Clear pw-context if refcount reaches 0 */
|
||||||
if (g_ref_count_dec (&cd->rc)) {
|
if (g_ref_count_dec (rc))
|
||||||
GSource *source = cd->loop_source;
|
|
||||||
|
|
||||||
g_clear_pointer (&self->pw_context, pw_context_destroy);
|
g_clear_pointer (&self->pw_context, pw_context_destroy);
|
||||||
g_source_destroy (source);
|
|
||||||
g_source_unref (source);
|
|
||||||
}
|
|
||||||
|
|
||||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||||
g_clear_pointer (&self->g_main_context, g_main_context_unref);
|
g_clear_pointer (&self->g_main_context, g_main_context_unref);
|
||||||
g_clear_pointer (&self->async_tasks, g_hash_table_unref);
|
g_clear_pointer (&self->async_tasks, g_hash_table_unref);
|
||||||
g_clear_object (&self->conf);
|
|
||||||
|
|
||||||
wp_debug_object (self, "WpCore destroyed");
|
wp_debug_object (self, "WpCore destroyed");
|
||||||
|
|
||||||
|
|
@ -451,9 +370,6 @@ wp_core_get_property (GObject * object, guint property_id,
|
||||||
case PROP_PW_CORE:
|
case PROP_PW_CORE:
|
||||||
g_value_set_pointer (value, self->pw_core);
|
g_value_set_pointer (value, self->pw_core);
|
||||||
break;
|
break;
|
||||||
case PROP_CONF:
|
|
||||||
g_value_set_object (value, self->conf);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||||
break;
|
break;
|
||||||
|
|
@ -476,9 +392,6 @@ wp_core_set_property (GObject * object, guint property_id,
|
||||||
case PROP_PW_CONTEXT:
|
case PROP_PW_CONTEXT:
|
||||||
self->pw_context = g_value_get_pointer (value);
|
self->pw_context = g_value_get_pointer (value);
|
||||||
break;
|
break;
|
||||||
case PROP_CONF:
|
|
||||||
self->conf = g_value_dup_object (value);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||||
break;
|
break;
|
||||||
|
|
@ -534,11 +447,6 @@ on_components_loaded (WpCore * self, GAsyncResult *res,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->conf) {
|
|
||||||
wp_info_object (self, "done loading components, closing conf file...");
|
|
||||||
wp_conf_close (self->conf);
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
|
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,11 +541,6 @@ wp_core_class_init (WpCoreClass * klass)
|
||||||
g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core",
|
g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core",
|
||||||
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
||||||
|
|
||||||
g_object_class_install_property (object_class, PROP_CONF,
|
|
||||||
g_param_spec_object ("conf", "conf", "The main configuration file",
|
|
||||||
WP_TYPE_CONF,
|
|
||||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
||||||
|
|
||||||
signals[SIGNAL_CONNECTED] = g_signal_new ("connected",
|
signals[SIGNAL_CONNECTED] = g_signal_new ("connected",
|
||||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||||
G_TYPE_NONE, 0);
|
G_TYPE_NONE, 0);
|
||||||
|
|
@ -652,20 +555,16 @@ wp_core_class_init (WpCoreClass * klass)
|
||||||
*
|
*
|
||||||
* \ingroup wpcore
|
* \ingroup wpcore
|
||||||
* \param context (transfer none) (nullable): the GMainContext to use for events
|
* \param context (transfer none) (nullable): the GMainContext to use for events
|
||||||
* \param conf (transfer full) (nullable): the main configuration file
|
* \param properties (transfer full) (nullable): additional properties, which are
|
||||||
* \param properties (transfer full) (nullable): additional properties, which
|
* passed to pw_context_new() and pw_context_connect()
|
||||||
* are also passed to pw_context_new() and pw_context_connect()
|
|
||||||
* \returns (transfer full): a new WpCore
|
* \returns (transfer full): a new WpCore
|
||||||
*/
|
*/
|
||||||
WpCore *
|
WpCore *
|
||||||
wp_core_new (GMainContext * context, WpConf * conf, WpProperties * properties)
|
wp_core_new (GMainContext *context, WpProperties * properties)
|
||||||
{
|
{
|
||||||
g_autoptr (WpConf) c = conf;
|
|
||||||
g_autoptr (WpProperties) props = properties;
|
g_autoptr (WpProperties) props = properties;
|
||||||
|
|
||||||
return g_object_new (WP_TYPE_CORE,
|
return g_object_new (WP_TYPE_CORE,
|
||||||
"g-main-context", context,
|
"g-main-context", context,
|
||||||
"conf", conf,
|
|
||||||
"properties", properties,
|
"properties", properties,
|
||||||
"pw-context", NULL,
|
"pw-context", NULL,
|
||||||
NULL);
|
NULL);
|
||||||
|
|
@ -684,7 +583,6 @@ wp_core_clone (WpCore * self)
|
||||||
return g_object_new (WP_TYPE_CORE,
|
return g_object_new (WP_TYPE_CORE,
|
||||||
"core", self,
|
"core", self,
|
||||||
"g-main-context", self->g_main_context,
|
"g-main-context", self->g_main_context,
|
||||||
"conf", self->conf,
|
|
||||||
"properties", self->properties,
|
"properties", self->properties,
|
||||||
"pw-context", self->pw_context,
|
"pw-context", self->pw_context,
|
||||||
NULL);
|
NULL);
|
||||||
|
|
@ -720,20 +618,6 @@ wp_core_get_export_core (WpCore * self)
|
||||||
return wp_core_find_object (self, find_export_core, NULL);
|
return wp_core_find_object (self, find_export_core, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the main configuration file of the core
|
|
||||||
*
|
|
||||||
* \ingroup wpcore
|
|
||||||
* \param self the core
|
|
||||||
* \returns (transfer full) (nullable): the main configuration file
|
|
||||||
*/
|
|
||||||
WpConf *
|
|
||||||
wp_core_get_conf (WpCore * self)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (WP_IS_CORE (self), NULL);
|
|
||||||
return self->conf ? g_object_ref (self->conf) : NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Gets the GMainContext of the core
|
* \brief Gets the GMainContext of the core
|
||||||
*
|
*
|
||||||
|
|
@ -851,37 +735,6 @@ wp_core_get_vm_type (WpCore *self)
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
wp_core_connect_internal (WpCore *self, int fd)
|
|
||||||
{
|
|
||||||
struct pw_properties *p = NULL;
|
|
||||||
|
|
||||||
/* Don't do anything if core is already connected */
|
|
||||||
if (self->pw_core)
|
|
||||||
return TRUE;
|
|
||||||
|
|
||||||
/* Connect */
|
|
||||||
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
|
|
||||||
|
|
||||||
if (fd == -1)
|
|
||||||
self->pw_core = pw_context_connect (self->pw_context, p, 0);
|
|
||||||
else
|
|
||||||
self->pw_core = pw_context_connect_fd (self->pw_context, fd, p, 0);
|
|
||||||
|
|
||||||
if (!self->pw_core)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
/* Add the core listeners */
|
|
||||||
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
|
|
||||||
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
|
|
||||||
&self->proxy_core_listener, &proxy_core_events, self);
|
|
||||||
|
|
||||||
/* Add the registry listener */
|
|
||||||
wp_registry_attach (&self->registry, self->pw_core);
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Connects this core to the PipeWire server.
|
* \brief Connects this core to the PipeWire server.
|
||||||
*
|
*
|
||||||
|
|
@ -895,31 +748,29 @@ wp_core_connect_internal (WpCore *self, int fd)
|
||||||
gboolean
|
gboolean
|
||||||
wp_core_connect (WpCore *self)
|
wp_core_connect (WpCore *self)
|
||||||
{
|
{
|
||||||
|
struct pw_properties *p = NULL;
|
||||||
|
|
||||||
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
|
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
|
||||||
|
|
||||||
return wp_core_connect_internal (self, -1);
|
/* Don't do anything if core is already connected */
|
||||||
}
|
if (self->pw_core)
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
/*!
|
/* Connect */
|
||||||
* \brief Connects this core to the PipeWire server on the given socket.
|
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
|
||||||
*
|
self->pw_core = pw_context_connect (self->pw_context, p, 0);
|
||||||
* When connection succeeds, the WpCore \c "connected" signal is emitted.
|
if (!self->pw_core)
|
||||||
*
|
return FALSE;
|
||||||
* \ingroup wpcore
|
|
||||||
* \param self the core
|
|
||||||
* \param fd the connected socket to use, the socket will be closed
|
|
||||||
* automatically on disconnect or error
|
|
||||||
* \returns TRUE if the core is effectively connected or FALSE if
|
|
||||||
* connection failed
|
|
||||||
* \since 0.5.6
|
|
||||||
*/
|
|
||||||
gboolean
|
|
||||||
wp_core_connect_fd (WpCore *self, int fd)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
|
|
||||||
g_return_val_if_fail (fd > -1, FALSE);
|
|
||||||
|
|
||||||
return wp_core_connect_internal (self, fd);
|
/* Add the core listeners */
|
||||||
|
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
|
||||||
|
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
|
||||||
|
&self->proxy_core_listener, &proxy_core_events, self);
|
||||||
|
|
||||||
|
/* Add the registry listener */
|
||||||
|
wp_registry_attach (&self->registry, self->pw_core);
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
@ -1138,8 +989,8 @@ wp_core_update_properties (WpCore * self, WpProperties * updates)
|
||||||
* \ingroup wpcore
|
* \ingroup wpcore
|
||||||
* \param self the core
|
* \param self the core
|
||||||
* \param source (out) (optional): the source
|
* \param source (out) (optional): the source
|
||||||
* \param function (scope notified)(closure data)(destroy destroy): the function to call
|
* \param function (scope notified): the function to call
|
||||||
* \param data data to pass to \a function
|
* \param data (closure): data to pass to \a function
|
||||||
* \param destroy (nullable): a function to destroy \a data
|
* \param destroy (nullable): a function to destroy \a data
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
|
|
@ -1203,8 +1054,8 @@ wp_core_idle_add_closure (WpCore * self, GSource **source, GClosure * closure)
|
||||||
* \param self the core
|
* \param self the core
|
||||||
* \param source (out) (optional): the source
|
* \param source (out) (optional): the source
|
||||||
* \param timeout_ms the timeout in milliseconds
|
* \param timeout_ms the timeout in milliseconds
|
||||||
* \param function (scope notified)(closure data)(destroy destroy): the function to call
|
* \param function (scope notified): the function to call
|
||||||
* \param data data to pass to \a function
|
* \param data (closure): data to pass to \a function
|
||||||
* \param destroy (nullable): a function to destroy \a data
|
* \param destroy (nullable): a function to destroy \a data
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
|
|
@ -1267,8 +1118,8 @@ wp_core_timeout_add_closure (WpCore * self, GSource **source, guint timeout_ms,
|
||||||
* \ingroup wpcore
|
* \ingroup wpcore
|
||||||
* \param self the core
|
* \param self the core
|
||||||
* \param cancellable (nullable): a GCancellable to cancel the operation
|
* \param cancellable (nullable): a GCancellable to cancel the operation
|
||||||
* \param callback (scope async)(closure user_data): a function to call when the operation is done
|
* \param callback (scope async): a function to call when the operation is done
|
||||||
* \param user_data data to pass to \a callback
|
* \param user_data (closure): data to pass to \a callback
|
||||||
* \returns TRUE if the sync operation was started, FALSE if an error
|
* \returns TRUE if the sync operation was started, FALSE if an error
|
||||||
* occurred before returning from this function
|
* occurred before returning from this function
|
||||||
*/
|
*/
|
||||||
|
|
@ -1378,7 +1229,6 @@ wp_core_sync_finish (WpCore * self, GAsyncResult * res, GError ** error)
|
||||||
/*!
|
/*!
|
||||||
* \brief Finds a registered object
|
* \brief Finds a registered object
|
||||||
*
|
*
|
||||||
* \ingroup wpcore
|
|
||||||
* \param self the core
|
* \param self the core
|
||||||
* \param func (scope call): a function that takes the object being searched
|
* \param func (scope call): a function that takes the object being searched
|
||||||
* as the first argument and \a data as the second. it should return TRUE if
|
* as the first argument and \a data as the second. it should return TRUE if
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
#include "object.h"
|
#include "object.h"
|
||||||
#include "properties.h"
|
#include "properties.h"
|
||||||
#include "spa-json.h"
|
#include "spa-json.h"
|
||||||
#include "conf.h"
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
|
@ -42,8 +41,7 @@ G_DECLARE_FINAL_TYPE (WpCore, wp_core, WP, CORE, WpObject)
|
||||||
/* Basic */
|
/* Basic */
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpCore * wp_core_new (GMainContext * context, WpConf * conf,
|
WpCore * wp_core_new (GMainContext *context, WpProperties * properties);
|
||||||
WpProperties * properties);
|
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpCore * wp_core_clone (WpCore * self);
|
WpCore * wp_core_clone (WpCore * self);
|
||||||
|
|
@ -51,9 +49,6 @@ WpCore * wp_core_clone (WpCore * self);
|
||||||
WP_API
|
WP_API
|
||||||
WpCore * wp_core_get_export_core (WpCore * self);
|
WpCore * wp_core_get_export_core (WpCore * self);
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpConf * wp_core_get_conf (WpCore * self);
|
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
GMainContext * wp_core_get_g_main_context (WpCore * self);
|
GMainContext * wp_core_get_g_main_context (WpCore * self);
|
||||||
|
|
||||||
|
|
@ -71,9 +66,6 @@ gchar *wp_core_get_vm_type (WpCore *self);
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_core_connect (WpCore *self);
|
gboolean wp_core_connect (WpCore *self);
|
||||||
|
|
||||||
WP_API
|
|
||||||
gboolean wp_core_connect_fd (WpCore *self, int fd);
|
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
void wp_core_disconnect (WpCore *self);
|
void wp_core_disconnect (WpCore *self);
|
||||||
|
|
||||||
|
|
|
||||||
141
lib/wp/device.c
141
lib/wp/device.c
|
|
@ -199,7 +199,6 @@ struct _WpSpaDevice
|
||||||
struct spa_hook listener;
|
struct spa_hook listener;
|
||||||
WpProperties *properties;
|
WpProperties *properties;
|
||||||
GPtrArray *managed_objs;
|
GPtrArray *managed_objs;
|
||||||
GPtrArray *pending_obj_config;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
|
|
@ -226,19 +225,11 @@ object_unref_safe (gpointer object)
|
||||||
g_object_unref (object);
|
g_object_unref (object);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
pod_unref_safe (gpointer object)
|
|
||||||
{
|
|
||||||
if (object)
|
|
||||||
wp_spa_pod_unref (object);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
wp_spa_device_init (WpSpaDevice * self)
|
wp_spa_device_init (WpSpaDevice * self)
|
||||||
{
|
{
|
||||||
self->properties = wp_properties_new_empty ();
|
self->properties = wp_properties_new_empty ();
|
||||||
self->managed_objs = g_ptr_array_new_with_free_func (object_unref_safe);
|
self->managed_objs = g_ptr_array_new_with_free_func (object_unref_safe);
|
||||||
self->pending_obj_config = g_ptr_array_new_with_free_func (pod_unref_safe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -271,7 +262,6 @@ wp_spa_device_finalize (GObject * object)
|
||||||
g_clear_pointer (&self->handle, pw_unload_spa_handle);
|
g_clear_pointer (&self->handle, pw_unload_spa_handle);
|
||||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||||
g_clear_pointer (&self->managed_objs, g_ptr_array_unref);
|
g_clear_pointer (&self->managed_objs, g_ptr_array_unref);
|
||||||
g_clear_pointer (&self->pending_obj_config, g_ptr_array_unref);
|
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_spa_device_parent_class)->finalize (object);
|
G_OBJECT_CLASS (wp_spa_device_parent_class)->finalize (object);
|
||||||
}
|
}
|
||||||
|
|
@ -323,8 +313,8 @@ spa_device_event_info (void *data, const struct spa_device_info *info)
|
||||||
WpSpaDevice *self = WP_SPA_DEVICE (data);
|
WpSpaDevice *self = WP_SPA_DEVICE (data);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This is emitted synchronously at the time we add the listener and
|
* This is emited syncrhonously at the time we add the listener and
|
||||||
* before object_info is emitted. It gives us additional properties
|
* before object_info is emited. It gives us additional properties
|
||||||
* about the device, like the "api.alsa.card.*" ones that are not
|
* about the device, like the "api.alsa.card.*" ones that are not
|
||||||
* set by the monitor
|
* set by the monitor
|
||||||
*/
|
*/
|
||||||
|
|
@ -332,67 +322,6 @@ spa_device_event_info (void *data, const struct spa_device_info *info)
|
||||||
wp_properties_update_from_dict (self->properties, info->props);
|
wp_properties_update_from_dict (self->properties, info->props);
|
||||||
}
|
}
|
||||||
|
|
||||||
static WpSpaPod *
|
|
||||||
pending_obj_config_pop (WpSpaDevice *self, guint32 id)
|
|
||||||
{
|
|
||||||
if (id < self->pending_obj_config->len)
|
|
||||||
return g_steal_pointer (&g_ptr_array_index (self->pending_obj_config, id));
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
pending_obj_config_set (WpSpaDevice *self, guint32 id, WpSpaPod *props)
|
|
||||||
{
|
|
||||||
if (id >= self->pending_obj_config->len)
|
|
||||||
g_ptr_array_set_size (self->pending_obj_config, id + 1);
|
|
||||||
|
|
||||||
gpointer *ptr = &g_ptr_array_index (self->pending_obj_config, id);
|
|
||||||
pod_unref_safe (*ptr);
|
|
||||||
*ptr = props;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
append_props (WpSpaPodBuilder *b, WpSpaPod *props, GHashTable *used)
|
|
||||||
{
|
|
||||||
g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (props);
|
|
||||||
GValue next = G_VALUE_INIT;
|
|
||||||
|
|
||||||
for (; wp_iterator_next (it, &next); g_value_unset (&next)) {
|
|
||||||
WpSpaPod *p = g_value_get_boxed (&next);
|
|
||||||
const char *key;
|
|
||||||
g_autoptr (WpSpaPod) value = NULL;
|
|
||||||
|
|
||||||
if (!wp_spa_pod_get_property (p, &key, &value))
|
|
||||||
continue;
|
|
||||||
if (g_hash_table_contains(used, key))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
wp_spa_pod_builder_add_property (b, key);
|
|
||||||
wp_spa_pod_builder_add_pod (b, value);
|
|
||||||
|
|
||||||
g_hash_table_add (used, (gpointer) g_strdup (key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static WpSpaPod *
|
|
||||||
merge_props (WpSpaPod *old_props, WpSpaPod *new_props)
|
|
||||||
{
|
|
||||||
g_autoptr (GHashTable) used = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
|
||||||
g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_object (
|
|
||||||
"Spa:Pod:Object:Param:Props", "Props");
|
|
||||||
|
|
||||||
if (new_props) {
|
|
||||||
append_props (b, new_props, used);
|
|
||||||
wp_spa_pod_unref (new_props);
|
|
||||||
}
|
|
||||||
if (old_props) {
|
|
||||||
append_props (b, old_props, used);
|
|
||||||
wp_spa_pod_unref (old_props);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wp_spa_pod_builder_end (b);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
spa_device_event_event (void *data, const struct spa_event *event)
|
spa_device_event_event (void *data, const struct spa_event *event)
|
||||||
{
|
{
|
||||||
|
|
@ -412,18 +341,10 @@ spa_device_event_event (void *data, const struct spa_event *event)
|
||||||
NULL))
|
NULL))
|
||||||
child = wp_spa_device_get_managed_object (self, id);
|
child = wp_spa_device_get_managed_object (self, id);
|
||||||
|
|
||||||
if (!g_strcmp0 (type, "ObjectConfig") && props) {
|
if (child && !g_strcmp0 (type, "ObjectConfig") &&
|
||||||
if (child && WP_IS_PIPEWIRE_OBJECT (child)) {
|
WP_IS_PIPEWIRE_OBJECT (child) && props) {
|
||||||
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
|
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
|
||||||
g_steal_pointer (&props));
|
g_steal_pointer (&props));
|
||||||
} else if (!child) {
|
|
||||||
/* Save Props set on ids pending for a managed object */
|
|
||||||
WpSpaPod *pending_props = pending_obj_config_pop (self, id);
|
|
||||||
if (pending_props) {
|
|
||||||
pending_props = merge_props (pending_props, g_steal_pointer(&props));
|
|
||||||
pending_obj_config_set (self, id, pending_props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -438,23 +359,12 @@ spa_device_event_object_info (void *data, uint32_t id,
|
||||||
g_autoptr (WpProperties) props = NULL;
|
g_autoptr (WpProperties) props = NULL;
|
||||||
|
|
||||||
type = spa_debug_type_short_name (info->type);
|
type = spa_debug_type_short_name (info->type);
|
||||||
props = wp_properties_new_copy_dict (info->props);
|
props = wp_properties_new_wrap_dict (info->props);
|
||||||
|
|
||||||
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
|
|
||||||
id, type, info->factory_name);
|
|
||||||
|
|
||||||
if (id < self->managed_objs->len &&
|
|
||||||
g_ptr_array_index (self->managed_objs, id) != NULL) {
|
|
||||||
wp_debug_object (self, "object already exists, removing");
|
|
||||||
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
|
|
||||||
wp_spa_device_store_managed_object (self, id, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
g_signal_emit (self, spa_device_signals[SIGNAL_CREATE_OBJECT], 0,
|
g_signal_emit (self, spa_device_signals[SIGNAL_CREATE_OBJECT], 0,
|
||||||
id, type, info->factory_name, props);
|
id, type, info->factory_name, props);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
wp_debug_object (self, "object removed: id:%u", id);
|
|
||||||
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
|
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
|
||||||
wp_spa_device_store_managed_object (self, id, NULL);
|
wp_spa_device_store_managed_object (self, id, NULL);
|
||||||
}
|
}
|
||||||
|
|
@ -537,7 +447,6 @@ wp_spa_device_deactivate (WpObject * object, WpObjectFeatures features)
|
||||||
WpSpaDevice *self = WP_SPA_DEVICE (object);
|
WpSpaDevice *self = WP_SPA_DEVICE (object);
|
||||||
spa_hook_remove (&self->listener);
|
spa_hook_remove (&self->listener);
|
||||||
g_ptr_array_set_size (self->managed_objs, 0);
|
g_ptr_array_set_size (self->managed_objs, 0);
|
||||||
g_ptr_array_set_size (self->pending_obj_config, 0);
|
|
||||||
wp_object_update_features (object, 0, WP_SPA_DEVICE_FEATURE_ENABLED);
|
wp_object_update_features (object, 0, WP_SPA_DEVICE_FEATURE_ENABLED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -788,40 +697,4 @@ wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
|
||||||
if (*ptr)
|
if (*ptr)
|
||||||
g_object_unref (*ptr);
|
g_object_unref (*ptr);
|
||||||
*ptr = object;
|
*ptr = object;
|
||||||
|
|
||||||
/* Clear pending status, and set pending props if any */
|
|
||||||
g_autoptr(WpSpaPod) props = pending_obj_config_pop (self, id);
|
|
||||||
|
|
||||||
if (props && object && WP_IS_PIPEWIRE_OBJECT (object)) {
|
|
||||||
wp_trace_boxed (WP_TYPE_SPA_POD, props, "pending ObjectConfig, object %d", id);
|
|
||||||
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (object), "Props", 0,
|
|
||||||
g_steal_pointer (&props));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Marks a managed object id pending.
|
|
||||||
*
|
|
||||||
* When an object id is pending, Props from received ObjectConfig events
|
|
||||||
* for the id are saved. When \ref wp_spa_device_store_managed_object later sets
|
|
||||||
* an object for the id, the saved Props are immediately set on the object and
|
|
||||||
* pending status is cleared.
|
|
||||||
*
|
|
||||||
* If an object is already set for the id, this has no effect.
|
|
||||||
*
|
|
||||||
* \ingroup wpspadevice
|
|
||||||
* \param self the spa device
|
|
||||||
* \param id the (device-internal) id of the object
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_spa_device_set_managed_pending (WpSpaDevice * self, guint id)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_SPA_DEVICE (self));
|
|
||||||
|
|
||||||
g_autoptr (GObject) obj = wp_spa_device_get_managed_object (self, id);
|
|
||||||
if (obj)
|
|
||||||
return;
|
|
||||||
|
|
||||||
pending_obj_config_set (self, id,
|
|
||||||
wp_spa_pod_new_object ("Spa:Pod:Object:Param:Props", "Props", NULL));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,6 @@ WP_API
|
||||||
void wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
|
void wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
|
||||||
GObject * object);
|
GObject * object);
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_spa_device_set_managed_pending (WpSpaDevice * self, guint id);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -15,161 +15,6 @@
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
|
||||||
|
|
||||||
typedef struct _HookData HookData;
|
|
||||||
struct _HookData
|
|
||||||
{
|
|
||||||
struct spa_list link;
|
|
||||||
WpEventHook *hook;
|
|
||||||
GPtrArray *dependencies;
|
|
||||||
};
|
|
||||||
|
|
||||||
static inline HookData *
|
|
||||||
hook_data_new (WpEventHook * hook)
|
|
||||||
{
|
|
||||||
HookData *hook_data = g_new0 (HookData, 1);
|
|
||||||
spa_list_init (&hook_data->link);
|
|
||||||
hook_data->hook = g_object_ref (hook);
|
|
||||||
hook_data->dependencies = g_ptr_array_new ();
|
|
||||||
return hook_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
hook_data_free (HookData *self)
|
|
||||||
{
|
|
||||||
g_clear_object (&self->hook);
|
|
||||||
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
|
||||||
g_free (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
record_dependency (struct spa_list *list, const gchar *target,
|
|
||||||
const gchar *dependency)
|
|
||||||
{
|
|
||||||
HookData *hook_data;
|
|
||||||
spa_list_for_each (hook_data, list, link) {
|
|
||||||
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
|
||||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline gboolean
|
|
||||||
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
|
||||||
{
|
|
||||||
HookData *hook_data;
|
|
||||||
if (!spa_list_is_empty (list)) {
|
|
||||||
spa_list_for_each (hook_data, list, link) {
|
|
||||||
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
sort_hooks (GPtrArray *hooks)
|
|
||||||
{
|
|
||||||
struct spa_list collected, result, remaining;
|
|
||||||
HookData *sorted_hook_data = NULL;
|
|
||||||
|
|
||||||
spa_list_init (&collected);
|
|
||||||
spa_list_init (&result);
|
|
||||||
spa_list_init (&remaining);
|
|
||||||
|
|
||||||
for (guint i = 0; i < hooks->len; i++) {
|
|
||||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
|
||||||
HookData *hook_data = hook_data_new (hook);
|
|
||||||
|
|
||||||
/* record "after" dependencies directly */
|
|
||||||
const gchar * const * strv =
|
|
||||||
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
|
||||||
while (strv && *strv) {
|
|
||||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
|
||||||
strv++;
|
|
||||||
}
|
|
||||||
|
|
||||||
spa_list_append (&collected, &hook_data->link);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!spa_list_is_empty (&collected)) {
|
|
||||||
HookData *hook_data;
|
|
||||||
|
|
||||||
/* convert "before" dependencies into "after" dependencies */
|
|
||||||
spa_list_for_each (hook_data, &collected, link) {
|
|
||||||
const gchar * const * strv =
|
|
||||||
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
|
||||||
while (strv && *strv) {
|
|
||||||
/* record hook_data->hook as a dependency of the *strv hook */
|
|
||||||
record_dependency (&collected, *strv,
|
|
||||||
wp_event_hook_get_name (hook_data->hook));
|
|
||||||
strv++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sort */
|
|
||||||
while (!spa_list_is_empty (&collected)) {
|
|
||||||
gboolean made_progress = FALSE;
|
|
||||||
|
|
||||||
/* examine each hook to see if its dependencies are satisfied in the
|
|
||||||
result list; if yes, then append it to the result too */
|
|
||||||
spa_list_consume (hook_data, &collected, link) {
|
|
||||||
guint deps_satisfied = 0;
|
|
||||||
|
|
||||||
spa_list_remove (&hook_data->link);
|
|
||||||
|
|
||||||
for (guint i = 0; i < hook_data->dependencies->len; i++) {
|
|
||||||
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
|
|
||||||
/* if the dependency is already in the sorted result list or if
|
|
||||||
it doesn't exist at all, we consider it satisfied */
|
|
||||||
if (hook_exists_in (dep, &result) ||
|
|
||||||
!(hook_exists_in (dep, &collected) ||
|
|
||||||
hook_exists_in (dep, &remaining))) {
|
|
||||||
deps_satisfied++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deps_satisfied == hook_data->dependencies->len) {
|
|
||||||
spa_list_append (&result, &hook_data->link);
|
|
||||||
made_progress = TRUE;
|
|
||||||
} else {
|
|
||||||
spa_list_append (&remaining, &hook_data->link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (made_progress) {
|
|
||||||
/* run again with the remaining hooks */
|
|
||||||
spa_list_insert_list (&collected, &remaining);
|
|
||||||
spa_list_init (&remaining);
|
|
||||||
}
|
|
||||||
else if (!spa_list_is_empty (&remaining)) {
|
|
||||||
/* if we did not make any progress towards growing the result list,
|
|
||||||
it means the dependencies cannot be satisfied because of circles */
|
|
||||||
spa_list_consume (hook_data, &result, link) {
|
|
||||||
spa_list_remove (&hook_data->link);
|
|
||||||
hook_data_free (hook_data);
|
|
||||||
}
|
|
||||||
spa_list_consume (hook_data, &remaining, link) {
|
|
||||||
spa_list_remove (&hook_data->link);
|
|
||||||
hook_data_free (hook_data);
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* clear hooks and add the sorted ones */
|
|
||||||
g_ptr_array_set_size (hooks, 0);
|
|
||||||
spa_list_consume (sorted_hook_data, &result, link) {
|
|
||||||
spa_list_remove (&sorted_hook_data->link);
|
|
||||||
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
|
|
||||||
hook_data_free (sorted_hook_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct _EventData EventData;
|
typedef struct _EventData EventData;
|
||||||
struct _EventData
|
struct _EventData
|
||||||
{
|
{
|
||||||
|
|
@ -204,8 +49,7 @@ struct _WpEventDispatcher
|
||||||
GObject parent;
|
GObject parent;
|
||||||
|
|
||||||
GWeakRef core;
|
GWeakRef core;
|
||||||
GHashTable *defined_hooks; /* registered hooks for defined events */
|
GPtrArray *hooks; /* registered hooks */
|
||||||
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
|
|
||||||
GSource *source; /* the event loop source */
|
GSource *source; /* the event loop source */
|
||||||
GList *events; /* the events stack */
|
GList *events; /* the events stack */
|
||||||
struct spa_system *system;
|
struct spa_system *system;
|
||||||
|
|
@ -260,7 +104,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
|
|
||||||
/* get the highest priority event */
|
/* get the highest priority event */
|
||||||
GList *levent = g_list_first (d->events);
|
GList *levent = g_list_first (d->events);
|
||||||
if (levent) {
|
while (levent) {
|
||||||
EventData *event_data = (EventData *) (levent->data);
|
EventData *event_data = (EventData *) (levent->data);
|
||||||
WpEvent *event = event_data->event;
|
WpEvent *event = event_data->event;
|
||||||
GCancellable *cancellable = wp_event_get_cancellable (event);
|
GCancellable *cancellable = wp_event_get_cancellable (event);
|
||||||
|
|
@ -300,8 +144,6 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||||
|
|
||||||
/* get the next event */
|
/* get the next event */
|
||||||
levent = g_list_first (d->events);
|
levent = g_list_first (d->events);
|
||||||
if (levent && !((EventData *) levent->data)->current_hook_in_async)
|
|
||||||
spa_system_eventfd_write (d->system, d->eventfd, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
|
|
@ -318,9 +160,7 @@ static void
|
||||||
wp_event_dispatcher_init (WpEventDispatcher * self)
|
wp_event_dispatcher_init (WpEventDispatcher * self)
|
||||||
{
|
{
|
||||||
g_weak_ref_init (&self->core, NULL);
|
g_weak_ref_init (&self->core, NULL);
|
||||||
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||||
(GDestroyNotify)g_ptr_array_unref);
|
|
||||||
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
|
||||||
|
|
||||||
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
|
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
|
||||||
((WpEventSource *) self->source)->dispatcher = self;
|
((WpEventSource *) self->source)->dispatcher = self;
|
||||||
|
|
@ -344,8 +184,7 @@ wp_event_dispatcher_finalize (GObject * object)
|
||||||
|
|
||||||
close (self->eventfd);
|
close (self->eventfd);
|
||||||
|
|
||||||
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
|
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
||||||
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
|
|
||||||
g_weak_ref_clear (&self->core);
|
g_weak_ref_clear (&self->core);
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
|
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
|
||||||
|
|
@ -425,7 +264,7 @@ wp_event_dispatcher_push_event (WpEventDispatcher * self, WpEvent * event)
|
||||||
|
|
||||||
self->events = g_list_insert_sorted (self->events, event_data,
|
self->events = g_list_insert_sorted (self->events, event_data,
|
||||||
(GCompareFunc) event_cmp_func);
|
(GCompareFunc) event_cmp_func);
|
||||||
wp_debug_object (self, "pushed event (%s)", wp_event_get_name (event));
|
wp_trace_object (self, "pushed event (%s)", wp_event_get_name (event));
|
||||||
|
|
||||||
/* wakeup the GSource */
|
/* wakeup the GSource */
|
||||||
spa_system_eventfd_write (self->system, self->eventfd, 1);
|
spa_system_eventfd_write (self->system, self->eventfd, 1);
|
||||||
|
|
@ -445,10 +284,6 @@ void
|
||||||
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
||||||
WpEventHook * hook)
|
WpEventHook * hook)
|
||||||
{
|
{
|
||||||
g_autoptr (GPtrArray) event_types = NULL;
|
|
||||||
gboolean is_defined = FALSE;
|
|
||||||
const gchar *hook_name;
|
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||||
|
|
||||||
|
|
@ -457,79 +292,12 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
||||||
g_return_if_fail (already_registered_dispatcher == NULL);
|
g_return_if_fail (already_registered_dispatcher == NULL);
|
||||||
|
|
||||||
wp_event_hook_set_dispatcher (hook, self);
|
wp_event_hook_set_dispatcher (hook, self);
|
||||||
|
g_ptr_array_add (self->hooks, g_object_ref (hook));
|
||||||
/* Register the event hook in the defined hooks table if it is defined */
|
|
||||||
hook_name = wp_event_hook_get_name (hook);
|
|
||||||
event_types = wp_event_hook_get_matching_event_types (hook);
|
|
||||||
if (event_types) {
|
|
||||||
for (guint i = 0; i < event_types->len; i++) {
|
|
||||||
const gchar *event_type = g_ptr_array_index (event_types, i);
|
|
||||||
GPtrArray *hooks;
|
|
||||||
|
|
||||||
wp_debug_object (self, "Registering hook %s for defined event type %s",
|
|
||||||
hook_name, event_type);
|
|
||||||
|
|
||||||
/* Check if the event type was registered in the hash table */
|
|
||||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
|
||||||
if (hooks) {
|
|
||||||
g_ptr_array_add (hooks, g_object_ref (hook));
|
|
||||||
if (!sort_hooks (hooks))
|
|
||||||
goto sort_error;
|
|
||||||
} else {
|
|
||||||
GPtrArray *new_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
|
||||||
/* Add undefined hooks */
|
|
||||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
|
||||||
WpEventHook *uh = g_ptr_array_index (self->undefined_hooks, i);
|
|
||||||
g_ptr_array_add (new_hooks, g_object_ref (uh));
|
|
||||||
}
|
|
||||||
/* Add current hook */
|
|
||||||
g_ptr_array_add (new_hooks, g_object_ref (hook));
|
|
||||||
g_hash_table_insert (self->defined_hooks, g_strdup (event_type),
|
|
||||||
new_hooks);
|
|
||||||
if (!sort_hooks (new_hooks))
|
|
||||||
goto sort_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_defined = TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Otherwise just register it as undefined hook */
|
|
||||||
if (!is_defined) {
|
|
||||||
GHashTableIter iter;
|
|
||||||
gpointer value;
|
|
||||||
|
|
||||||
wp_debug_object (self, "Registering hook %s for undefined event types",
|
|
||||||
hook_name);
|
|
||||||
|
|
||||||
/* Add it to the defined hooks table */
|
|
||||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
|
||||||
GPtrArray *defined_hooks = value;
|
|
||||||
g_ptr_array_add (defined_hooks, g_object_ref (hook));
|
|
||||||
if (!sort_hooks (defined_hooks))
|
|
||||||
goto sort_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add it to the undefined hooks */
|
|
||||||
g_ptr_array_add (self->undefined_hooks, g_object_ref (hook));
|
|
||||||
if (!sort_hooks (self->undefined_hooks))
|
|
||||||
goto sort_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_info_object (self, "Registered hook %s successfully", hook_name);
|
|
||||||
return;
|
|
||||||
|
|
||||||
sort_error:
|
|
||||||
/* Unregister hook */
|
|
||||||
wp_event_dispatcher_unregister_hook (self, hook);
|
|
||||||
wp_warning_object (self,
|
|
||||||
"Could not register hook %s because of circular dependencies", hook_name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Unregisters an event hook
|
* \brief Unregisters an event hook
|
||||||
* \ingroup wpeventdispatcher
|
* \ingroup wpeventdispacher
|
||||||
*
|
*
|
||||||
* \param self the event dispatcher
|
* \param self the event dispatcher
|
||||||
* \param hook (transfer none): the hook to unregister
|
* \param hook (transfer none): the hook to unregister
|
||||||
|
|
@ -538,9 +306,6 @@ void
|
||||||
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||||
WpEventHook * hook)
|
WpEventHook * hook)
|
||||||
{
|
{
|
||||||
GHashTableIter iter;
|
|
||||||
gpointer value;
|
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||||
|
|
||||||
|
|
@ -549,29 +314,11 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||||
g_return_if_fail (already_registered_dispatcher == self);
|
g_return_if_fail (already_registered_dispatcher == self);
|
||||||
|
|
||||||
wp_event_hook_set_dispatcher (hook, NULL);
|
wp_event_hook_set_dispatcher (hook, NULL);
|
||||||
|
g_ptr_array_remove_fast (self->hooks, hook);
|
||||||
/* Remove hook from defined table and undefined list */
|
|
||||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
|
||||||
GPtrArray *defined_hooks = value;
|
|
||||||
g_ptr_array_remove (defined_hooks, hook);
|
|
||||||
}
|
|
||||||
g_ptr_array_remove (self->undefined_hooks, hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_unique (GPtrArray *array, WpEventHook * hook)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < array->len; i++)
|
|
||||||
if (g_ptr_array_index (array, i) == hook)
|
|
||||||
return;
|
|
||||||
g_ptr_array_add (array, g_object_ref (hook));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns an iterator to iterate over all the registered hooks
|
* \brief Returns an iterator to iterate over all the registered hooks
|
||||||
* \deprecated Use \ref wp_event_dispatcher_new_hooks_for_event_type_iterator
|
|
||||||
* instead.
|
|
||||||
* \ingroup wpeventdispatcher
|
* \ingroup wpeventdispatcher
|
||||||
*
|
*
|
||||||
* \param self the event dispatcher
|
* \param self the event dispatcher
|
||||||
|
|
@ -580,56 +327,7 @@ add_unique (GPtrArray *array, WpEventHook * hook)
|
||||||
WpIterator *
|
WpIterator *
|
||||||
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
||||||
{
|
{
|
||||||
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
|
GPtrArray *items =
|
||||||
GHashTableIter iter;
|
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
|
||||||
gpointer value;
|
|
||||||
|
|
||||||
/* Add all defined hooks */
|
|
||||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
|
||||||
GPtrArray *hooks = value;
|
|
||||||
for (guint i = 0; i < hooks->len; i++) {
|
|
||||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
|
||||||
add_unique (items, hook);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add all undefined hooks */
|
|
||||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
|
||||||
WpEventHook *hook = g_ptr_array_index (self->undefined_hooks, i);
|
|
||||||
add_unique (items, hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Returns an iterator to iterate over the registered hooks for a
|
|
||||||
* particular event type.
|
|
||||||
* \ingroup wpeventdispatcher
|
|
||||||
*
|
|
||||||
* \param self the event dispatcher
|
|
||||||
* \param event_type the event type
|
|
||||||
* \return (transfer full): a new iterator
|
|
||||||
* \since 0.5.13
|
|
||||||
*/
|
|
||||||
WpIterator *
|
|
||||||
wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
|
||||||
WpEventDispatcher * self, const gchar *event_type)
|
|
||||||
{
|
|
||||||
GPtrArray *items;
|
|
||||||
GPtrArray *hooks;
|
|
||||||
|
|
||||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
|
||||||
if (hooks) {
|
|
||||||
wp_debug_object (self, "Using %d defined hooks for event type %s",
|
|
||||||
hooks->len, event_type);
|
|
||||||
} else {
|
|
||||||
hooks = self->undefined_hooks;
|
|
||||||
wp_debug_object (self, "Using %d undefined hooks for event type %s",
|
|
||||||
hooks->len, event_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
items = g_ptr_array_copy (hooks, (GCopyFunc) g_object_ref, NULL);
|
|
||||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,7 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||||
WpEventHook * hook);
|
WpEventHook * hook);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
|
||||||
G_GNUC_DEPRECATED_FOR (wp_event_dispatcher_new_hooks_for_event_type_iterator);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpIterator * wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
|
||||||
WpEventDispatcher * self, const gchar *event_type);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,6 @@ wp_event_hook_get_runs_after_hooks (WpEventHook * self)
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns the associated event dispatcher
|
* \brief Returns the associated event dispatcher
|
||||||
*
|
*
|
||||||
* \private
|
|
||||||
* \ingroup wpeventhook
|
* \ingroup wpeventhook
|
||||||
* \param self the event hook
|
* \param self the event hook
|
||||||
* \return (transfer full)(nullable): the event dispatcher on which this hook
|
* \return (transfer full)(nullable): the event dispatcher on which this hook
|
||||||
|
|
@ -198,15 +197,6 @@ wp_event_hook_get_dispatcher (WpEventHook * self)
|
||||||
return g_weak_ref_get (&priv->dispatcher);
|
return g_weak_ref_get (&priv->dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Sets the associated event dispatcher
|
|
||||||
*
|
|
||||||
* \private
|
|
||||||
* \ingroup wpeventhook
|
|
||||||
* \param self the event hook
|
|
||||||
* \param dispatcher (transfer none): the event dispatcher on which
|
|
||||||
* this hook is registered
|
|
||||||
*/
|
|
||||||
void
|
void
|
||||||
wp_event_hook_set_dispatcher (WpEventHook * self, WpEventDispatcher * dispatcher)
|
wp_event_hook_set_dispatcher (WpEventHook * self, WpEventDispatcher * dispatcher)
|
||||||
{
|
{
|
||||||
|
|
@ -239,9 +229,9 @@ wp_event_hook_runs_for_event (WpEventHook * self, WpEvent * event)
|
||||||
* \param self the event hook
|
* \param self the event hook
|
||||||
* \param event the event that triggered the hook
|
* \param event the event that triggered the hook
|
||||||
* \param cancellable (nullable): a GCancellable to cancel the async operation
|
* \param cancellable (nullable): a GCancellable to cancel the async operation
|
||||||
* \param callback (scope async)(closure callback_data): a callback to fire after execution of the hook
|
* \param callback (scope async): a callback to fire after execution of the hook
|
||||||
* has completed
|
* has completed
|
||||||
* \param callback_data data for the callback
|
* \param callback_data (closure): data for the callback
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
wp_event_hook_run (WpEventHook * self,
|
wp_event_hook_run (WpEventHook * self,
|
||||||
|
|
@ -254,24 +244,6 @@ wp_event_hook_run (WpEventHook * self,
|
||||||
callback_data);
|
callback_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets all the matching event types for this hook if any.
|
|
||||||
*
|
|
||||||
* \ingroup wpeventhook
|
|
||||||
* \param self the event hook
|
|
||||||
* \returns (element-type gchar*) (transfer full) (nullable): the matching
|
|
||||||
* event types for this hook if any.
|
|
||||||
* \since 0.5.13
|
|
||||||
*/
|
|
||||||
GPtrArray *
|
|
||||||
wp_event_hook_get_matching_event_types (WpEventHook * self)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
|
|
||||||
g_return_val_if_fail (
|
|
||||||
WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
|
|
||||||
return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Finishes the async operation that was started by wp_event_hook_run()
|
* \brief Finishes the async operation that was started by wp_event_hook_run()
|
||||||
*
|
*
|
||||||
|
|
@ -339,61 +311,34 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
|
||||||
wp_interest_event_hook_get_instance_private (self);
|
wp_interest_event_hook_get_instance_private (self);
|
||||||
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
|
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
|
||||||
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
||||||
|
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
|
||||||
guint i;
|
guint i;
|
||||||
WpObjectInterest *interest = NULL;
|
WpObjectInterest *interest = NULL;
|
||||||
|
WpInterestMatch match;
|
||||||
|
|
||||||
|
const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
|
||||||
|
WP_INTEREST_MATCH_PW_PROPERTIES |
|
||||||
|
WP_INTEREST_MATCH_G_PROPERTIES);
|
||||||
|
|
||||||
for (i = 0; i < priv->interests->len; i++) {
|
for (i = 0; i < priv->interests->len; i++) {
|
||||||
interest = g_ptr_array_index (priv->interests, i);
|
interest = g_ptr_array_index (priv->interests, i);
|
||||||
if (wp_object_interest_matches_full (interest,
|
match = wp_object_interest_matches_full (interest,
|
||||||
WP_INTEREST_MATCH_FLAGS_NONE,
|
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
|
||||||
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
|
gtype, subject, properties, properties);
|
||||||
|
|
||||||
|
/* the interest may have a GType that matches the GType of the subject
|
||||||
|
or it may have WP_TYPE_EVENT as its GType, in which case it will
|
||||||
|
match any type of subject */
|
||||||
|
if (match == WP_INTEREST_MATCH_ALL)
|
||||||
|
return TRUE;
|
||||||
|
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
|
||||||
|
match = wp_object_interest_matches_full (interest, 0,
|
||||||
|
WP_TYPE_EVENT, NULL, NULL, NULL);
|
||||||
|
if (match & WP_INTEREST_MATCH_GTYPE)
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_unique (GPtrArray *array, const gchar * lookup)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < array->len; i++)
|
|
||||||
if (g_str_equal (g_ptr_array_index (array, i), lookup))
|
|
||||||
return;
|
|
||||||
g_ptr_array_add (array, g_strdup (lookup));
|
|
||||||
}
|
|
||||||
|
|
||||||
static GPtrArray *
|
|
||||||
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
|
|
||||||
{
|
|
||||||
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
|
|
||||||
WpInterestEventHookPrivate *priv =
|
|
||||||
wp_interest_event_hook_get_instance_private (self);
|
|
||||||
g_autoptr (GPtrArray) res = g_ptr_array_new_with_free_func (g_free);
|
|
||||||
guint i;
|
|
||||||
|
|
||||||
for (i = 0; i < priv->interests->len; i++) {
|
|
||||||
WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
|
|
||||||
if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
|
|
||||||
WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
|
|
||||||
g_autoptr (GPtrArray) values =
|
|
||||||
wp_object_interest_find_defined_constraint_values (interest,
|
|
||||||
WP_CONSTRAINT_TYPE_NONE, "event.type");
|
|
||||||
if (!values || values->len == 0) {
|
|
||||||
/* We always consider the hook undefined if it has at least one interest
|
|
||||||
* without a defined 'event.type' constraint */
|
|
||||||
return NULL;
|
|
||||||
} else {
|
|
||||||
for (guint j = 0; j < values->len; j++) {
|
|
||||||
GVariant *v = g_ptr_array_index (values, j);
|
|
||||||
if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
|
|
||||||
const gchar *v_str = g_variant_get_string (v, NULL);
|
|
||||||
add_unique (res, v_str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return FALSE;
|
||||||
return g_steal_pointer (&res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -404,8 +349,6 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
|
||||||
|
|
||||||
object_class->finalize = wp_interest_event_hook_finalize;
|
object_class->finalize = wp_interest_event_hook_finalize;
|
||||||
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
|
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
|
||||||
hook_class->get_matching_event_types =
|
|
||||||
wp_interest_event_hook_get_matching_event_types;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,8 @@ struct _WpEventHookClass
|
||||||
|
|
||||||
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
|
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
|
||||||
|
|
||||||
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
|
|
||||||
|
|
||||||
/*< private >*/
|
/*< private >*/
|
||||||
WP_PADDING(4)
|
WP_PADDING(5)
|
||||||
};
|
};
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
|
|
@ -69,9 +67,6 @@ void wp_event_hook_run (WpEventHook * self,
|
||||||
WpEvent * event, GCancellable * cancellable,
|
WpEvent * event, GCancellable * cancellable,
|
||||||
GAsyncReadyCallback callback, gpointer callback_data);
|
GAsyncReadyCallback callback, gpointer callback_data);
|
||||||
|
|
||||||
WP_API
|
|
||||||
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
|
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
|
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
|
||||||
GError ** error);
|
GError ** error);
|
||||||
|
|
|
||||||
253
lib/wp/event.c
253
lib/wp/event.c
|
|
@ -17,11 +17,37 @@
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
|
||||||
|
|
||||||
|
typedef struct _HookData HookData;
|
||||||
|
struct _HookData
|
||||||
|
{
|
||||||
|
struct spa_list link;
|
||||||
|
WpEventHook *hook;
|
||||||
|
GPtrArray *dependencies;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline HookData *
|
||||||
|
hook_data_new (WpEventHook * hook)
|
||||||
|
{
|
||||||
|
HookData *hook_data = g_new0 (HookData, 1);
|
||||||
|
spa_list_init (&hook_data->link);
|
||||||
|
hook_data->hook = g_object_ref (hook);
|
||||||
|
hook_data->dependencies = g_ptr_array_new ();
|
||||||
|
return hook_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
hook_data_free (HookData *self)
|
||||||
|
{
|
||||||
|
g_clear_object (&self->hook);
|
||||||
|
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
||||||
|
g_free (self);
|
||||||
|
}
|
||||||
|
|
||||||
struct _WpEvent
|
struct _WpEvent
|
||||||
{
|
{
|
||||||
grefcount ref;
|
grefcount ref;
|
||||||
GData *datalist;
|
GData *datalist;
|
||||||
GPtrArray *hooks;
|
struct spa_list hooks;
|
||||||
|
|
||||||
/* immutable fields */
|
/* immutable fields */
|
||||||
gint priority;
|
gint priority;
|
||||||
|
|
@ -70,7 +96,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
|
||||||
WpEvent * self = g_new0 (WpEvent, 1);
|
WpEvent * self = g_new0 (WpEvent, 1);
|
||||||
g_ref_count_init (&self->ref);
|
g_ref_count_init (&self->ref);
|
||||||
g_datalist_init (&self->datalist);
|
g_datalist_init (&self->datalist);
|
||||||
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
spa_list_init (&self->hooks);
|
||||||
|
|
||||||
self->priority = priority;
|
self->priority = priority;
|
||||||
self->properties = properties ?
|
self->properties = properties ?
|
||||||
|
|
@ -129,7 +155,11 @@ wp_event_get_name(WpEvent *self)
|
||||||
static void
|
static void
|
||||||
wp_event_free (WpEvent * self)
|
wp_event_free (WpEvent * self)
|
||||||
{
|
{
|
||||||
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
HookData *hook_data;
|
||||||
|
spa_list_consume (hook_data, &self->hooks, link) {
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
hook_data_free (hook_data);
|
||||||
|
}
|
||||||
g_datalist_clear (&self->datalist);
|
g_datalist_clear (&self->datalist);
|
||||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||||
g_clear_object (&self->source);
|
g_clear_object (&self->source);
|
||||||
|
|
@ -286,6 +316,33 @@ wp_event_get_data (WpEvent * self, const gchar * key)
|
||||||
return g_datalist_get_data (&self->datalist, key);
|
return g_datalist_get_data (&self->datalist, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
record_dependency (struct spa_list *list, const gchar *target,
|
||||||
|
const gchar *dependency)
|
||||||
|
{
|
||||||
|
HookData *hook_data;
|
||||||
|
spa_list_for_each (hook_data, list, link) {
|
||||||
|
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
||||||
|
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline gboolean
|
||||||
|
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
||||||
|
{
|
||||||
|
HookData *hook_data;
|
||||||
|
if (!spa_list_is_empty (list)) {
|
||||||
|
spa_list_for_each (hook_data, list, link) {
|
||||||
|
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Collects all the hooks registered in the \a dispatcher that run for
|
* \brief Collects all the hooks registered in the \a dispatcher that run for
|
||||||
* this \a event
|
* this \a event
|
||||||
|
|
@ -298,37 +355,188 @@ wp_event_get_data (WpEvent * self, const gchar * key)
|
||||||
gboolean
|
gboolean
|
||||||
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
||||||
{
|
{
|
||||||
|
struct spa_list collected, result, remaining;
|
||||||
g_autoptr (WpIterator) all_hooks = NULL;
|
g_autoptr (WpIterator) all_hooks = NULL;
|
||||||
g_auto (GValue) value = G_VALUE_INIT;
|
g_auto (GValue) value = G_VALUE_INIT;
|
||||||
const gchar *event_type = NULL;
|
|
||||||
|
|
||||||
g_return_val_if_fail (event != NULL, FALSE);
|
g_return_val_if_fail (event != NULL, FALSE);
|
||||||
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
|
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
|
||||||
|
|
||||||
/* Clear all current hooks */
|
/* hooks already collected */
|
||||||
g_ptr_array_set_size (event->hooks, 0);
|
if (!spa_list_is_empty (&event->hooks))
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
/* Get the event type */
|
spa_list_init (&collected);
|
||||||
event_type = wp_properties_get (event->properties, "event.type");
|
spa_list_init (&result);
|
||||||
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
|
spa_list_init (&remaining);
|
||||||
event->name, event_type);
|
|
||||||
|
|
||||||
/* Collect hooks that run for this event */
|
/* collect hooks that run for this event */
|
||||||
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
|
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
|
||||||
event_type);
|
|
||||||
while (wp_iterator_next (all_hooks, &value)) {
|
while (wp_iterator_next (all_hooks, &value)) {
|
||||||
WpEventHook *hook = g_value_get_object (&value);
|
WpEventHook *hook = g_value_get_object (&value);
|
||||||
|
|
||||||
if (wp_event_hook_runs_for_event (hook, event)) {
|
if (wp_event_hook_runs_for_event (hook, event)) {
|
||||||
g_ptr_array_add (event->hooks, g_object_ref (hook));
|
HookData *hook_data = hook_data_new (hook);
|
||||||
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
|
||||||
|
/* record "after" dependencies directly */
|
||||||
|
const gchar * const * strv =
|
||||||
|
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
||||||
|
while (strv && *strv) {
|
||||||
|
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
||||||
|
strv++;
|
||||||
|
}
|
||||||
|
|
||||||
|
spa_list_append (&collected, &hook_data->link);
|
||||||
|
|
||||||
|
wp_trace_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
||||||
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
|
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
|
||||||
}
|
}
|
||||||
|
|
||||||
g_value_unset (&value);
|
g_value_unset (&value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return event->hooks->len > 0;
|
if (!spa_list_is_empty (&collected)) {
|
||||||
|
HookData *hook_data;
|
||||||
|
|
||||||
|
/* convert "before" dependencies into "after" dependencies */
|
||||||
|
spa_list_for_each (hook_data, &collected, link) {
|
||||||
|
const gchar * const * strv =
|
||||||
|
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
||||||
|
while (strv && *strv) {
|
||||||
|
/* record hook_data->hook as a dependency of the *strv hook */
|
||||||
|
record_dependency (&collected, *strv,
|
||||||
|
wp_event_hook_get_name (hook_data->hook));
|
||||||
|
strv++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sort */
|
||||||
|
while (!spa_list_is_empty (&collected)) {
|
||||||
|
gboolean made_progress = FALSE;
|
||||||
|
|
||||||
|
/* examine each hook to see if its dependencies are satisfied in the
|
||||||
|
result list; if yes, then append it to the result too */
|
||||||
|
spa_list_consume (hook_data, &collected, link) {
|
||||||
|
guint deps_satisfied = 0;
|
||||||
|
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
|
||||||
|
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 */
|
||||||
|
wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
|
||||||
|
"dependencies in the collected hooks!");
|
||||||
|
|
||||||
|
/* clean up */
|
||||||
|
spa_list_consume (hook_data, &result, link) {
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
hook_data_free (hook_data);
|
||||||
|
}
|
||||||
|
spa_list_consume (hook_data, &remaining, link) {
|
||||||
|
spa_list_remove (&hook_data->link);
|
||||||
|
hook_data_free (hook_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spa_list_insert_list (&event->hooks, &result);
|
||||||
|
return !spa_list_is_empty (&event->hooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct event_hooks_iterator_data
|
||||||
|
{
|
||||||
|
WpEvent *event;
|
||||||
|
HookData *cur;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_hooks_iterator_reset (WpIterator *it)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
struct spa_list *list = &it_data->event->hooks;
|
||||||
|
|
||||||
|
if (!spa_list_is_empty (list))
|
||||||
|
it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
event_hooks_iterator_next (WpIterator *it, GValue *item)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
struct spa_list *list = &it_data->event->hooks;
|
||||||
|
|
||||||
|
if (!spa_list_is_empty (list) &&
|
||||||
|
!spa_list_is_end (it_data->cur, list, link)) {
|
||||||
|
g_value_init (item, WP_TYPE_EVENT_HOOK);
|
||||||
|
g_value_set_object (item, it_data->cur->hook);
|
||||||
|
it_data->cur = spa_list_next (it_data->cur, link);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
struct spa_list *list = &it_data->event->hooks;
|
||||||
|
HookData *hook_data;
|
||||||
|
|
||||||
|
if (!spa_list_is_empty (list)) {
|
||||||
|
spa_list_for_each (hook_data, list, link) {
|
||||||
|
g_auto (GValue) item = G_VALUE_INIT;
|
||||||
|
g_value_init (&item, WP_TYPE_EVENT_HOOK);
|
||||||
|
g_value_set_object (&item, hook_data->hook);
|
||||||
|
if (!func (&item, ret, data))
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_hooks_iterator_finalize (WpIterator *it)
|
||||||
|
{
|
||||||
|
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||||
|
wp_event_unref (it_data->event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const WpIteratorMethods event_hooks_iterator_methods = {
|
||||||
|
.version = WP_ITERATOR_METHODS_VERSION,
|
||||||
|
.reset = event_hooks_iterator_reset,
|
||||||
|
.next = event_hooks_iterator_next,
|
||||||
|
.fold = event_hooks_iterator_fold,
|
||||||
|
.finalize = event_hooks_iterator_finalize,
|
||||||
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns an iterator that iterates over all the hooks that were
|
* \brief Returns an iterator that iterates over all the hooks that were
|
||||||
* collected by wp_event_collect_hooks()
|
* collected by wp_event_collect_hooks()
|
||||||
|
|
@ -339,8 +547,15 @@ wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
||||||
WpIterator *
|
WpIterator *
|
||||||
wp_event_new_hooks_iterator (WpEvent * event)
|
wp_event_new_hooks_iterator (WpEvent * event)
|
||||||
{
|
{
|
||||||
GPtrArray *hooks;
|
WpIterator *it = NULL;
|
||||||
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL);
|
struct event_hooks_iterator_data *it_data;
|
||||||
return wp_iterator_new_ptr_array (hooks, WP_TYPE_EVENT_HOOK);
|
|
||||||
|
|
||||||
|
g_return_val_if_fail (event != NULL, NULL);
|
||||||
|
|
||||||
|
it = wp_iterator_new (&event_hooks_iterator_methods,
|
||||||
|
sizeof (struct event_hooks_iterator_data));
|
||||||
|
it_data = wp_iterator_get_user_data (it);
|
||||||
|
it_data->event = wp_event_ref (event);
|
||||||
|
event_hooks_iterator_reset (it);
|
||||||
|
return it;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,20 +250,6 @@ wp_global_proxy_destroyed (WpProxy * proxy)
|
||||||
WpGlobalProxyPrivate *priv =
|
WpGlobalProxyPrivate *priv =
|
||||||
wp_global_proxy_get_instance_private (self);
|
wp_global_proxy_get_instance_private (self);
|
||||||
|
|
||||||
if (priv->global && priv->global->proxy &&
|
|
||||||
(priv->global->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY)) {
|
|
||||||
/* We can end up here as a result of _request_destroy() followed by
|
|
||||||
* _deactivate(FEATURE_BOUND), where the latter triggers this callback
|
|
||||||
* before _remove_global is processed.
|
|
||||||
* If proxy is owned, it is gone now, so not much owned left.
|
|
||||||
* self might be cleaned up soon, so this is a good time to remove
|
|
||||||
* the non-refcounted backreference in global. If not done now, _dispose()
|
|
||||||
* does not have a chance to cleanup (as the reference to global is gone).
|
|
||||||
* If remove_global then comes in later, there is no more real work to
|
|
||||||
* do when WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY is removed
|
|
||||||
*/
|
|
||||||
wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
|
||||||
}
|
|
||||||
g_clear_pointer (&priv->global, wp_global_unref);
|
g_clear_pointer (&priv->global, wp_global_unref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,9 +178,9 @@ wp_iterator_next (WpIterator *self, GValue *item)
|
||||||
*
|
*
|
||||||
* \ingroup wpiterator
|
* \ingroup wpiterator
|
||||||
* \param self the iterator
|
* \param self the iterator
|
||||||
* \param func (scope call)(closure data): the fold function
|
* \param func (scope call): the fold function
|
||||||
* \param ret (inout): the accumulator data
|
* \param ret (inout): the accumulator data
|
||||||
* \param data the user data
|
* \param data (closure): the user data
|
||||||
* \returns TRUE if all the items were processed, FALSE otherwise.
|
* \returns TRUE if all the items were processed, FALSE otherwise.
|
||||||
*/
|
*/
|
||||||
gboolean
|
gboolean
|
||||||
|
|
@ -200,8 +200,8 @@ wp_iterator_fold (WpIterator *self, WpIteratorFoldFunc func, GValue *ret,
|
||||||
*
|
*
|
||||||
* \ingroup wpiterator
|
* \ingroup wpiterator
|
||||||
* \param self the iterator
|
* \param self the iterator
|
||||||
* \param func (scope call)(closure data): the foreach function
|
* \param func (scope call): the foreach function
|
||||||
* \param data the user data
|
* \param data (closure): the user data
|
||||||
* \returns TRUE if all the items were processed, FALSE otherwise.
|
* \returns TRUE if all the items were processed, FALSE otherwise.
|
||||||
*/
|
*/
|
||||||
gboolean
|
gboolean
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ match_rules_cb (void *data, const char *location, const char *action,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Matches the given properties against a set of rules described in JSON
|
* \brief Matches the given properties against a set of rules descriped in JSON
|
||||||
* and calls the given callback to perform actions on a successful match.
|
* and calls the given callback to perform actions on a successful match.
|
||||||
*
|
*
|
||||||
* The given JSON should be an array of objects, where each object has a
|
* The given JSON should be an array of objects, where each object has a
|
||||||
|
|
@ -53,7 +53,7 @@ match_rules_cb (void *data, const char *location, const char *action,
|
||||||
* {
|
* {
|
||||||
* matches = [
|
* matches = [
|
||||||
* # any of the items in matches needs to match, if one does,
|
* # any of the items in matches needs to match, if one does,
|
||||||
* # actions are emitted.
|
* # actions are emited.
|
||||||
* {
|
* {
|
||||||
* # all keys must match the value. ! negates. ~ starts regex.
|
* # all keys must match the value. ! negates. ~ starts regex.
|
||||||
* <key> = <value>
|
* <key> = <value>
|
||||||
|
|
@ -72,8 +72,8 @@ match_rules_cb (void *data, const char *location, const char *action,
|
||||||
* \ingroup wpjsonutils
|
* \ingroup wpjsonutils
|
||||||
* \param json a JSON array containing rules in the described format
|
* \param json a JSON array containing rules in the described format
|
||||||
* \param match_props (transfer none): the properties to match against the rules
|
* \param match_props (transfer none): the properties to match against the rules
|
||||||
* \param callback (scope call)(closure data): a function to call for each action on a successful match
|
* \param callback (scope call): a function to call for each action on a successful match
|
||||||
* \param data data to be passed to \a callback
|
* \param data (closure callback): data to be passed to \a callback
|
||||||
* \param error (out)(optional): the error that occurred, if any
|
* \param error (out)(optional): the error that occurred, if any
|
||||||
* \returns FALSE if an error occurred, TRUE otherwise
|
* \returns FALSE if an error occurred, TRUE otherwise
|
||||||
*/
|
*/
|
||||||
|
|
@ -117,7 +117,7 @@ update_props_cb (gpointer data, const gchar * action, WpSpaJson * value,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Matches the given properties against a set of rules described in JSON
|
* \brief Matches the given properties against a set of rules descriped in JSON
|
||||||
* and updates the properties if the rule actions include the "update-props"
|
* and updates the properties if the rule actions include the "update-props"
|
||||||
* action.
|
* action.
|
||||||
*
|
*
|
||||||
|
|
@ -211,12 +211,12 @@ merge_json_objects (WpSpaJson *a, WpSpaJson *b)
|
||||||
g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
|
g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
|
||||||
val = g_value_dup_boxed (&item);
|
val = g_value_dup_boxed (&item);
|
||||||
|
|
||||||
if (!override && wp_spa_json_is_container (val) &&
|
if (!override &&
|
||||||
(wp_spa_json_object_get (a, key_str, "J", &j, NULL) ||
|
(wp_spa_json_object_get (a, key_str, "J", &j, NULL) ||
|
||||||
wp_spa_json_object_get (a, override_key_str, "J", &j, NULL))) {
|
wp_spa_json_object_get (a, override_key_str, "J", &j, NULL))) {
|
||||||
g_autoptr (WpSpaJson) merged = wp_json_utils_merge_containers (j, val);
|
g_autoptr (WpSpaJson) merged = wp_json_utils_merge_containers (j, val);
|
||||||
if (!merged) {
|
if (!merged) {
|
||||||
wp_warning ("skipping merge of %s as JSON values are not compatible containers",
|
wp_warning ("skipping merge of %s as JSON values are not compatible",
|
||||||
key_str);
|
key_str);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-link")
|
||||||
* \code
|
* \code
|
||||||
* void
|
* void
|
||||||
* state_changed_callback (WpLink * self,
|
* state_changed_callback (WpLink * self,
|
||||||
* WpLinkState old_state,
|
* WpLinkState * old_state,
|
||||||
* WpLinkState new_state,
|
* WpLinkState * new_state,
|
||||||
* gpointer user_data)
|
* gpointer user_data)
|
||||||
* \endcode
|
* \endcode
|
||||||
*
|
*
|
||||||
|
|
|
||||||
533
lib/wp/log.c
533
lib/wp/log.c
|
|
@ -18,39 +18,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-log")
|
||||||
* \{
|
* \{
|
||||||
*/
|
*/
|
||||||
/*!
|
/*!
|
||||||
* \def WP_DEFINE_LOCAL_LOG_TOPIC(name)
|
* \def WP_LOG_LEVEL_TRACE
|
||||||
* \brief Defines a static \em WpLogTopic* variable called \em WP_LOCAL_LOG_TOPIC
|
* \brief A custom GLib log level for trace messages (see GLogLevelFlags)
|
||||||
*
|
|
||||||
* The log topic is automatically initialized to the given topic \a name when
|
|
||||||
* it is first used. The default logging macros expect this variable to be
|
|
||||||
* defined, so it is a good coding practice in the WirePlumber codebase to
|
|
||||||
* start all files at the top with:
|
|
||||||
* \code
|
|
||||||
* WP_DEFINE_LOCAL_LOG_TOPIC ("some-topic")
|
|
||||||
* \endcode
|
|
||||||
*
|
|
||||||
* \param name The name of the log topic
|
|
||||||
*/
|
|
||||||
/*!
|
|
||||||
* \def WP_LOG_TOPIC_STATIC(var, name)
|
|
||||||
* \brief Defines a static \em WpLogTopic* variable called \a var with the given
|
|
||||||
* topic \a name
|
|
||||||
* \param var The name of the variable to define
|
|
||||||
* \param name The name of the log topic
|
|
||||||
*/
|
|
||||||
/*!
|
|
||||||
* \def WP_LOG_TOPIC(var, name)
|
|
||||||
* \brief Defines a \em WpLogTopic* variable called \a var with the given
|
|
||||||
* topic \a name. Unlike WP_LOG_TOPIC_STATIC(), the variable defined here is
|
|
||||||
* not static, so it can be linked to by other object files.
|
|
||||||
* \param var The name of the variable to define
|
|
||||||
* \param name The name of the log topic
|
|
||||||
*/
|
|
||||||
/*!
|
|
||||||
* \def WP_LOG_TOPIC_EXTERN(var)
|
|
||||||
* \brief Declares an extern \em WpLogTopic* variable called \a var.
|
|
||||||
* This variable is meant to be defined in a .c file with WP_LOG_TOPIC()
|
|
||||||
* \param var The name of the variable to declare
|
|
||||||
*/
|
*/
|
||||||
/*!
|
/*!
|
||||||
* \def WP_OBJECT_FORMAT
|
* \def WP_OBJECT_FORMAT
|
||||||
|
|
@ -207,33 +176,24 @@ static GString *spa_dbg_str = NULL;
|
||||||
|
|
||||||
#include <spa/debug/pod.h>
|
#include <spa/debug/pod.h>
|
||||||
|
|
||||||
#define DEFAULT_LOG_LEVEL 4 /* MESSAGE */
|
|
||||||
#define DEFAULT_LOG_LEVEL_FLAGS (G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR)
|
|
||||||
|
|
||||||
struct log_topic_pattern
|
struct log_topic_pattern
|
||||||
{
|
{
|
||||||
GPatternSpec *spec;
|
GPatternSpec *spec;
|
||||||
gchar *spec_str;
|
|
||||||
gint log_level;
|
gint log_level;
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct {
|
static struct {
|
||||||
gboolean use_color;
|
gboolean use_color;
|
||||||
gboolean output_is_journal;
|
gboolean output_is_journal;
|
||||||
gboolean set_pw_log;
|
|
||||||
gint global_log_level;
|
gint global_log_level;
|
||||||
GLogLevelFlags global_log_level_flags;
|
GLogLevelFlags global_log_level_flags;
|
||||||
struct log_topic_pattern *patterns;
|
struct log_topic_pattern *patterns;
|
||||||
GPtrArray *log_topics;
|
|
||||||
GMutex log_topics_lock;
|
|
||||||
} log_state = {
|
} log_state = {
|
||||||
.use_color = FALSE,
|
.use_color = FALSE,
|
||||||
.output_is_journal = FALSE,
|
.output_is_journal = FALSE,
|
||||||
.set_pw_log = FALSE,
|
.global_log_level = 4 /* MESSAGE */,
|
||||||
.global_log_level = DEFAULT_LOG_LEVEL,
|
.global_log_level_flags = G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR,
|
||||||
.global_log_level_flags = DEFAULT_LOG_LEVEL_FLAGS,
|
|
||||||
.patterns = NULL,
|
.patterns = NULL,
|
||||||
.log_topics = NULL,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* reference: https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit */
|
/* reference: https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit */
|
||||||
|
|
@ -300,8 +260,6 @@ level_index_from_flags (GLogLevelFlags log_level)
|
||||||
static G_GNUC_CONST inline GLogLevelFlags
|
static G_GNUC_CONST inline GLogLevelFlags
|
||||||
level_index_to_flag (gint lvl_index)
|
level_index_to_flag (gint lvl_index)
|
||||||
{
|
{
|
||||||
if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
|
|
||||||
return 0;
|
|
||||||
return log_level_info [lvl_index].log_level_flags;
|
return log_level_info [lvl_index].log_level_flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,8 +300,6 @@ level_index_from_spa (gint spa_lvl, gboolean warn_to_notice)
|
||||||
static G_GNUC_CONST inline gint
|
static G_GNUC_CONST inline gint
|
||||||
level_index_to_spa (gint lvl_index)
|
level_index_to_spa (gint lvl_index)
|
||||||
{
|
{
|
||||||
if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
|
|
||||||
return 0;
|
|
||||||
return log_level_info [lvl_index].spa_level;
|
return log_level_info [lvl_index].spa_level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,6 +325,106 @@ level_index_from_string (const char *str, gint *lvl)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* private, called from wp_init() */
|
||||||
|
void
|
||||||
|
wp_log_init (gint flags)
|
||||||
|
{
|
||||||
|
const gchar *level_str;
|
||||||
|
gint global_log_level = log_state.global_log_level;
|
||||||
|
struct log_topic_pattern *patterns = NULL, *pttrn;
|
||||||
|
gint n_tokens = 0;
|
||||||
|
gchar **tokens = NULL;
|
||||||
|
|
||||||
|
level_str = g_getenv ("WIREPLUMBER_DEBUG");
|
||||||
|
|
||||||
|
log_state.use_color = g_log_writer_supports_color (fileno (stderr));
|
||||||
|
log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
|
||||||
|
|
||||||
|
if (level_str && level_str[0] != '\0') {
|
||||||
|
/* [<glob>:]<level>,..., */
|
||||||
|
tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* allocate enough space to hold all pattern specs */
|
||||||
|
patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
|
||||||
|
pttrn = patterns;
|
||||||
|
if (!patterns)
|
||||||
|
g_error ("unable to allocate space for %d log patterns", n_tokens + 2);
|
||||||
|
|
||||||
|
for (gint i = 0; i < n_tokens; i++) {
|
||||||
|
gint n_tok;
|
||||||
|
gchar **tok;
|
||||||
|
gint lvl;
|
||||||
|
|
||||||
|
tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
|
||||||
|
if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
|
||||||
|
pttrn->spec = g_pattern_spec_new (tok[0]);
|
||||||
|
pttrn->log_level = lvl;
|
||||||
|
pttrn++;
|
||||||
|
} else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
|
||||||
|
global_log_level = lvl;
|
||||||
|
} else {
|
||||||
|
/* note that this is going to initialize the wp-log topic here */
|
||||||
|
wp_warning ("Ignoring invalid format in WIREPLUMBER_DEBUG: '%s'",
|
||||||
|
tokens[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_free_strv (tok);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disable pipewire connection trace by default */
|
||||||
|
pttrn->spec = g_pattern_spec_new ("conn.*");
|
||||||
|
pttrn->log_level = 0;
|
||||||
|
pttrn++;
|
||||||
|
|
||||||
|
/* terminate with NULL */
|
||||||
|
pttrn->spec = NULL;
|
||||||
|
pttrn->log_level = 0;
|
||||||
|
|
||||||
|
pw_free_strv (tokens);
|
||||||
|
|
||||||
|
log_state.patterns = patterns;
|
||||||
|
log_state.global_log_level = global_log_level;
|
||||||
|
log_state.global_log_level_flags =
|
||||||
|
level_index_to_full_flags (global_log_level);
|
||||||
|
|
||||||
|
/* set the log level also on the spa_log */
|
||||||
|
wp_spa_log_get_instance()->level = level_index_to_spa (global_log_level);
|
||||||
|
|
||||||
|
if (flags & WP_INIT_SET_GLIB_LOG)
|
||||||
|
g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
|
||||||
|
|
||||||
|
/* set PIPEWIRE_DEBUG and the spa_log interface that pipewire will use */
|
||||||
|
if (flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG")) {
|
||||||
|
/* always set PIPEWIRE_DEBUG for 2 reasons:
|
||||||
|
* 1. to overwrite it from the environment, in case the user has set it
|
||||||
|
* 2. to prevent pw_context from parsing "log.level" from the config file;
|
||||||
|
* we do this ourselves here and allows us to have more control over
|
||||||
|
* the whole process.
|
||||||
|
*/
|
||||||
|
gchar lvl_str[2];
|
||||||
|
g_snprintf (lvl_str, 2, "%d", wp_spa_log_get_instance ()->level);
|
||||||
|
g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
|
||||||
|
pw_log_set_level (wp_spa_log_get_instance ()->level);
|
||||||
|
pw_log_set (wp_spa_log_get_instance ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
wp_log_set_global_level (const gchar *log_level)
|
||||||
|
{
|
||||||
|
gint level;
|
||||||
|
if (level_index_from_string (log_level, &level)) {
|
||||||
|
log_state.global_log_level = level;
|
||||||
|
log_state.global_log_level_flags = level_index_to_full_flags (level);
|
||||||
|
wp_spa_log_get_instance()->level = level_index_to_spa (level);
|
||||||
|
pw_log_set_level (level_index_to_spa (level));
|
||||||
|
return TRUE;
|
||||||
|
} else {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static gint
|
static gint
|
||||||
find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
|
find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
|
||||||
{
|
{
|
||||||
|
|
@ -396,255 +452,6 @@ find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
|
||||||
return log_level;
|
return log_level;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
log_topic_update_level (WpLogTopic *topic)
|
|
||||||
{
|
|
||||||
gint log_level = find_topic_log_level (topic->topic_name, NULL);
|
|
||||||
gint flags = topic->flags & ~WP_LOG_TOPIC_LEVEL_MASK;
|
|
||||||
|
|
||||||
flags |= level_index_to_full_flags (log_level);
|
|
||||||
|
|
||||||
topic->flags = flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
update_log_topic_levels (void)
|
|
||||||
{
|
|
||||||
guint i;
|
|
||||||
|
|
||||||
g_mutex_lock (&log_state.log_topics_lock);
|
|
||||||
|
|
||||||
if (log_state.log_topics)
|
|
||||||
for (i = 0; i < log_state.log_topics->len; ++i)
|
|
||||||
log_topic_update_level (g_ptr_array_index (log_state.log_topics, i));
|
|
||||||
|
|
||||||
g_mutex_unlock (&log_state.log_topics_lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
free_patterns (struct log_topic_pattern *patterns)
|
|
||||||
{
|
|
||||||
struct log_topic_pattern *p = patterns;
|
|
||||||
|
|
||||||
while (p && p->spec) {
|
|
||||||
g_clear_pointer (&p->spec, g_pattern_spec_free);
|
|
||||||
g_clear_pointer (&p->spec_str, g_free);
|
|
||||||
++p;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_free (patterns);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parse value to log level and patterns. If no global level in string,
|
|
||||||
global_log_level is not modified. */
|
|
||||||
static gboolean
|
|
||||||
parse_log_level (const gchar *level_str, struct log_topic_pattern **global_patterns, gint *global_log_level)
|
|
||||||
{
|
|
||||||
struct log_topic_pattern *patterns = NULL, *pttrn;
|
|
||||||
gint n_tokens = 0;
|
|
||||||
gchar **tokens = NULL;
|
|
||||||
int level = *global_log_level;
|
|
||||||
|
|
||||||
*global_patterns = NULL;
|
|
||||||
|
|
||||||
if (level_str && level_str[0] != '\0') {
|
|
||||||
/* [<glob>:]<level>,..., */
|
|
||||||
tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* allocate enough space to hold all pattern specs */
|
|
||||||
patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
|
|
||||||
pttrn = patterns;
|
|
||||||
if (!patterns)
|
|
||||||
g_error ("unable to allocate space for %d log patterns", n_tokens + 2);
|
|
||||||
|
|
||||||
for (gint i = 0; i < n_tokens; i++) {
|
|
||||||
gint n_tok;
|
|
||||||
gchar **tok;
|
|
||||||
gint lvl;
|
|
||||||
|
|
||||||
tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
|
|
||||||
if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
|
|
||||||
pttrn->spec = g_pattern_spec_new (tok[0]);
|
|
||||||
pttrn->spec_str = g_strdup (tok[0]);
|
|
||||||
pttrn->log_level = lvl;
|
|
||||||
pttrn++;
|
|
||||||
} else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
|
|
||||||
level = lvl;
|
|
||||||
} else {
|
|
||||||
pttrn->spec = NULL;
|
|
||||||
pw_free_strv (tok);
|
|
||||||
free_patterns (patterns);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
pw_free_strv (tok);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* disable pipewire connection trace by default */
|
|
||||||
pttrn->spec = g_pattern_spec_new ("conn.*");
|
|
||||||
pttrn->spec_str = g_strdup ("conn.*");
|
|
||||||
pttrn->log_level = 0;
|
|
||||||
pttrn++;
|
|
||||||
|
|
||||||
/* terminate with NULL */
|
|
||||||
pttrn->spec = NULL;
|
|
||||||
pttrn->spec_str = NULL;
|
|
||||||
pttrn->log_level = 0;
|
|
||||||
|
|
||||||
pw_free_strv (tokens);
|
|
||||||
|
|
||||||
*global_patterns = patterns;
|
|
||||||
*global_log_level = level;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gchar *
|
|
||||||
format_pw_log_level_string (gint level, const struct log_topic_pattern *patterns)
|
|
||||||
{
|
|
||||||
GString *str = g_string_new (NULL);
|
|
||||||
const struct log_topic_pattern *p;
|
|
||||||
|
|
||||||
g_string_printf (str, "%d", level_index_to_spa (level));
|
|
||||||
|
|
||||||
for (p = patterns; p && p->spec; ++p)
|
|
||||||
g_string_append_printf (str, ",%s:%d", p->spec_str, level_index_to_spa (p->log_level));
|
|
||||||
|
|
||||||
return g_string_free (str, FALSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
wp_log_set_level (const gchar *level_str)
|
|
||||||
{
|
|
||||||
gint level;
|
|
||||||
GLogLevelFlags flags;
|
|
||||||
struct log_topic_pattern *patterns;
|
|
||||||
|
|
||||||
level = DEFAULT_LOG_LEVEL;
|
|
||||||
if (!parse_log_level (level_str, &patterns, &level))
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
flags = level_index_to_full_flags (level);
|
|
||||||
|
|
||||||
g_mutex_lock (&log_state.log_topics_lock);
|
|
||||||
log_state.global_log_level = level;
|
|
||||||
log_state.global_log_level_flags = flags;
|
|
||||||
SPA_SWAP (log_state.patterns, patterns);
|
|
||||||
g_mutex_unlock (&log_state.log_topics_lock);
|
|
||||||
|
|
||||||
free_patterns (patterns);
|
|
||||||
|
|
||||||
update_log_topic_levels ();
|
|
||||||
|
|
||||||
wp_spa_log_get_instance()->level = level_index_to_spa (level);
|
|
||||||
|
|
||||||
if (log_state.set_pw_log) {
|
|
||||||
#if PW_CHECK_VERSION(1,1,0)
|
|
||||||
g_autofree gchar *pw_pattern = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
|
|
||||||
pw_log_set_level_string (pw_pattern);
|
|
||||||
#else
|
|
||||||
pw_log_set_level (level_index_to_spa (level));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief private, called from wp_init()
|
|
||||||
* \ingroup wplog
|
|
||||||
* \private
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_log_init (gint flags)
|
|
||||||
{
|
|
||||||
log_state.use_color = g_log_writer_supports_color (fileno (stderr));
|
|
||||||
log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
|
|
||||||
log_state.set_pw_log = flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG");
|
|
||||||
|
|
||||||
if (flags & WP_INIT_SET_GLIB_LOG)
|
|
||||||
g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
|
|
||||||
|
|
||||||
/* set the spa_log interface that pipewire will use */
|
|
||||||
if (log_state.set_pw_log)
|
|
||||||
pw_log_set (wp_spa_log_get_instance ());
|
|
||||||
|
|
||||||
if (!wp_log_set_level (g_getenv ("WIREPLUMBER_DEBUG"))) {
|
|
||||||
wp_warning ("Ignoring invalid value in WIREPLUMBER_DEBUG");
|
|
||||||
wp_log_set_level (NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log_state.set_pw_log) {
|
|
||||||
/* always set PIPEWIRE_DEBUG for 2 reasons:
|
|
||||||
* 1. to overwrite it from the environment, in case the user has set it
|
|
||||||
* 2. to prevent pw_context from parsing "log.level" from the config file;
|
|
||||||
* we do this ourselves here and allows us to have more control over
|
|
||||||
* the whole process.
|
|
||||||
*/
|
|
||||||
g_autofree gchar *lvl_str = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
|
|
||||||
g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
log_topic_register (WpLogTopic *topic)
|
|
||||||
{
|
|
||||||
if (!log_state.log_topics)
|
|
||||||
log_state.log_topics = g_ptr_array_new ();
|
|
||||||
|
|
||||||
g_ptr_array_add (log_state.log_topics, topic);
|
|
||||||
|
|
||||||
log_topic_update_level (topic);
|
|
||||||
topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
log_topic_unregister (WpLogTopic *topic)
|
|
||||||
{
|
|
||||||
if (!log_state.log_topics)
|
|
||||||
return;
|
|
||||||
|
|
||||||
g_ptr_array_remove_fast (log_state.log_topics, topic);
|
|
||||||
|
|
||||||
if (log_state.log_topics->len == 0) {
|
|
||||||
g_ptr_array_free (log_state.log_topics, TRUE);
|
|
||||||
log_state.log_topics = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Registers a log topic.
|
|
||||||
*
|
|
||||||
* The log topic must be unregistered using \ref wp_log_topic_unregister
|
|
||||||
* before its lifetime ends.
|
|
||||||
*
|
|
||||||
* This function is threadsafe.
|
|
||||||
*
|
|
||||||
* \ingroup wplog
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_log_topic_register (WpLogTopic *topic)
|
|
||||||
{
|
|
||||||
g_mutex_lock (&log_state.log_topics_lock);
|
|
||||||
log_topic_register (topic);
|
|
||||||
g_mutex_unlock (&log_state.log_topics_lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Unregisters a log topic.
|
|
||||||
*
|
|
||||||
* This function is threadsafe.
|
|
||||||
*
|
|
||||||
* \ingroup wplog
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_log_topic_unregister (WpLogTopic *topic)
|
|
||||||
{
|
|
||||||
g_mutex_lock (&log_state.log_topics_lock);
|
|
||||||
log_topic_unregister (topic);
|
|
||||||
g_mutex_unlock (&log_state.log_topics_lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Initializes a log topic. Internal function, don't use it directly
|
* \brief Initializes a log topic. Internal function, don't use it directly
|
||||||
* \ingroup wplog
|
* \ingroup wplog
|
||||||
|
|
@ -652,17 +459,21 @@ wp_log_topic_unregister (WpLogTopic *topic)
|
||||||
void
|
void
|
||||||
wp_log_topic_init (WpLogTopic *topic)
|
wp_log_topic_init (WpLogTopic *topic)
|
||||||
{
|
{
|
||||||
g_mutex_lock (&log_state.log_topics_lock);
|
g_bit_lock (&topic->flags, 30);
|
||||||
if ((topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) == 0) {
|
if ((topic->flags & (1u << 31)) == 0) {
|
||||||
if (topic->flags & WP_LOG_TOPIC_FLAG_STATIC) {
|
bool has_custom_level;
|
||||||
/* Auto-register log topics that have infinite lifetime */
|
gint log_level = find_topic_log_level (topic->topic_name, &has_custom_level);
|
||||||
log_topic_register (topic);
|
|
||||||
} else {
|
gint flags = topic->flags;
|
||||||
log_topic_update_level (topic);
|
flags |= level_index_to_full_flags (log_level);
|
||||||
topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
|
flags |= (1u << 31); /* initialized = true */
|
||||||
}
|
if (has_custom_level)
|
||||||
|
flags |= (1u << 29); /* has_custom_level = true */
|
||||||
|
|
||||||
|
topic->global_flags = &log_state.global_log_level_flags;
|
||||||
|
topic->flags = flags;
|
||||||
}
|
}
|
||||||
g_mutex_unlock (&log_state.log_topics_lock);
|
g_bit_unlock (&topic->flags, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct _WpLogFields WpLogFields;
|
typedef struct _WpLogFields WpLogFields;
|
||||||
|
|
@ -674,7 +485,6 @@ struct _WpLogFields
|
||||||
const gchar *func;
|
const gchar *func;
|
||||||
const gchar *message;
|
const gchar *message;
|
||||||
gint log_level;
|
gint log_level;
|
||||||
gboolean debug;
|
|
||||||
GType object_type;
|
GType object_type;
|
||||||
gconstpointer object;
|
gconstpointer object;
|
||||||
};
|
};
|
||||||
|
|
@ -683,7 +493,6 @@ static void
|
||||||
wp_log_fields_init (WpLogFields *lf,
|
wp_log_fields_init (WpLogFields *lf,
|
||||||
const gchar *log_topic,
|
const gchar *log_topic,
|
||||||
gint log_level,
|
gint log_level,
|
||||||
gboolean debug,
|
|
||||||
const gchar *file,
|
const gchar *file,
|
||||||
const gchar *line,
|
const gchar *line,
|
||||||
const gchar *func,
|
const gchar *func,
|
||||||
|
|
@ -693,7 +502,6 @@ wp_log_fields_init (WpLogFields *lf,
|
||||||
{
|
{
|
||||||
lf->log_topic = log_topic ? log_topic : "default";
|
lf->log_topic = log_topic ? log_topic : "default";
|
||||||
lf->log_level = log_level;
|
lf->log_level = log_level;
|
||||||
lf->debug = debug;
|
|
||||||
lf->file = file;
|
lf->file = file;
|
||||||
lf->line = line;
|
lf->line = line;
|
||||||
lf->func = func;
|
lf->func = func;
|
||||||
|
|
@ -702,18 +510,11 @@ wp_log_fields_init (WpLogFields *lf,
|
||||||
lf->message = message ? message : "(null)";
|
lf->message = message ? message : "(null)";
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
wp_want_debug_log (const struct spa_log_topic *topic)
|
|
||||||
{
|
|
||||||
return spa_log_level_topic_enabled (wp_spa_log_get_instance(), topic, SPA_LOG_LEVEL_DEBUG);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
wp_log_fields_init_from_glib (WpLogFields *lf, GLogLevelFlags log_level_flags,
|
wp_log_fields_init_from_glib (WpLogFields *lf, GLogLevelFlags log_level_flags,
|
||||||
const GLogField *fields, gsize n_fields)
|
const GLogField *fields, gsize n_fields)
|
||||||
{
|
{
|
||||||
wp_log_fields_init (lf, NULL, level_index_from_flags (log_level_flags),
|
wp_log_fields_init (lf, NULL, level_index_from_flags (log_level_flags),
|
||||||
wp_want_debug_log (NULL),
|
|
||||||
NULL, NULL, NULL, 0, NULL, NULL);
|
NULL, NULL, NULL, 0, NULL, NULL);
|
||||||
|
|
||||||
for (guint i = 0; i < n_fields; i++) {
|
for (guint i = 0; i < n_fields; i++) {
|
||||||
|
|
@ -772,52 +573,15 @@ wp_log_fields_write_to_stream (WpLogFields *lf, FILE *s)
|
||||||
static gboolean
|
static gboolean
|
||||||
wp_log_fields_write_to_journal (WpLogFields *lf)
|
wp_log_fields_write_to_journal (WpLogFields *lf)
|
||||||
{
|
{
|
||||||
GLogField fields[10];
|
gsize n_fields = 6;
|
||||||
gsize n_fields = 0;
|
GLogField fields[6] = {
|
||||||
g_autofree gchar *full_message = NULL;
|
{ "PRIORITY", log_level_info[lf->log_level].priority, -1 },
|
||||||
const gchar *message = lf->message ? lf->message : "";
|
{ "CODE_FILE", lf->file ? lf->file : "", -1 },
|
||||||
g_autofree gchar *pid = g_strdup_printf("%d", getpid());
|
{ "CODE_LINE", lf->line ? lf->line : "", -1 },
|
||||||
g_autofree gchar *tid = g_strdup_printf("%d", gettid());
|
{ "CODE_FUNC", lf->func ? lf->func : "", -1 },
|
||||||
#ifdef HAS_SHORT_NAME
|
{ "TOPIC", lf->log_topic ? lf->log_topic : "", -1 },
|
||||||
const gchar *syslog_identifier = program_invocation_short_name;
|
{ "MESSAGE", lf->message ? lf->message : "", -1 },
|
||||||
#else
|
};
|
||||||
const gchar *syslog_identifier = g_get_prgname();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (lf->debug) {
|
|
||||||
if (lf->file && lf->line && lf->func) {
|
|
||||||
g_autofree gchar *file = g_path_get_basename(lf->file);
|
|
||||||
|
|
||||||
message = full_message = g_strdup_printf("%c %s%s[%s:%s:%s]: %s",
|
|
||||||
log_level_info[lf->log_level].name,
|
|
||||||
lf->log_topic ? lf->log_topic : "",
|
|
||||||
lf->log_topic ? " " : "",
|
|
||||||
file, lf->line, lf->func, message);
|
|
||||||
} else {
|
|
||||||
message = full_message = g_strdup_printf("%c %s%s%s",
|
|
||||||
log_level_info[lf->log_level].name,
|
|
||||||
lf->log_topic ? lf->log_topic : "",
|
|
||||||
lf->log_topic ? ": " : "",
|
|
||||||
message);
|
|
||||||
}
|
|
||||||
} else if (lf->log_topic) {
|
|
||||||
message = full_message = g_strdup_printf("%s: %s", lf->log_topic, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
fields[n_fields++] = (GLogField) { "SYSLOG_PID", pid, -1 };
|
|
||||||
fields[n_fields++] = (GLogField) { "TID", tid, -1 };
|
|
||||||
fields[n_fields++] = (GLogField) { "SYSLOG_IDENTIFIER", syslog_identifier, -1 };
|
|
||||||
fields[n_fields++] = (GLogField) { "SYSLOG_FACILITY", "3", -1 };
|
|
||||||
fields[n_fields++] = (GLogField) { "PRIORITY", log_level_info[lf->log_level].priority, -1 };
|
|
||||||
if (lf->file)
|
|
||||||
fields[n_fields++] = (GLogField) { "CODE_FILE", lf->file, -1 };
|
|
||||||
if (lf->line)
|
|
||||||
fields[n_fields++] = (GLogField) { "CODE_LINE", lf->line, -1 };
|
|
||||||
if (lf->func)
|
|
||||||
fields[n_fields++] = (GLogField) { "CODE_FUNC", lf->func, -1 };
|
|
||||||
if (lf->log_topic)
|
|
||||||
fields[n_fields++] = (GLogField) { "TOPIC", lf->log_topic, -1 };
|
|
||||||
fields[n_fields++] = (GLogField) { "MESSAGE", message, -1 };
|
|
||||||
|
|
||||||
/* the log level flags are not used in this function, so we can pass 0 */
|
/* the log level flags are not used in this function, so we can pass 0 */
|
||||||
return (g_log_writer_journald (0, fields, n_fields, NULL) == G_LOG_WRITER_HANDLED);
|
return (g_log_writer_journald (0, fields, n_fields, NULL) == G_LOG_WRITER_HANDLED);
|
||||||
|
|
@ -907,8 +671,6 @@ wp_log_writer_default (GLogLevelFlags log_level_flags,
|
||||||
/*!
|
/*!
|
||||||
* \brief Used internally by the debug logging macros. Avoid using it directly.
|
* \brief Used internally by the debug logging macros. Avoid using it directly.
|
||||||
*
|
*
|
||||||
* \deprecated Use \ref wp_logt_checked instead.
|
|
||||||
*
|
|
||||||
* This assumes that the arguments are correct and that the log_topic is
|
* This assumes that the arguments are correct and that the log_topic is
|
||||||
* enabled for the given log_level. No additional checks are performed.
|
* enabled for the given log_level. No additional checks are performed.
|
||||||
* \ingroup wplog
|
* \ingroup wplog
|
||||||
|
|
@ -934,46 +696,6 @@ wp_log_checked (
|
||||||
va_end (args);
|
va_end (args);
|
||||||
|
|
||||||
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags),
|
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags),
|
||||||
wp_want_debug_log (NULL),
|
|
||||||
file, line, func, object_type, object, message);
|
|
||||||
wp_log_fields_log (&lf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Used internally by the debug logging macros. Avoid using it directly.
|
|
||||||
*
|
|
||||||
* This assumes that the arguments are correct and that the log_topic is
|
|
||||||
* enabled for the given log_level. No additional checks are performed.
|
|
||||||
* \ingroup wplog
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_logt_checked (
|
|
||||||
const WpLogTopic *topic,
|
|
||||||
GLogLevelFlags log_level_flags,
|
|
||||||
const gchar *file,
|
|
||||||
const gchar *line,
|
|
||||||
const gchar *func,
|
|
||||||
GType object_type,
|
|
||||||
gconstpointer object,
|
|
||||||
const gchar *message_format,
|
|
||||||
...)
|
|
||||||
{
|
|
||||||
WpLogFields lf = {0};
|
|
||||||
g_autofree gchar *message = NULL;
|
|
||||||
va_list args;
|
|
||||||
const gchar *log_topic = topic ? topic->topic_name : NULL;
|
|
||||||
gboolean debug;
|
|
||||||
|
|
||||||
if (topic)
|
|
||||||
debug = (topic->flags & WP_LOG_TOPIC_LEVEL_MASK & G_LOG_LEVEL_DEBUG);
|
|
||||||
else
|
|
||||||
debug = wp_want_debug_log (NULL);
|
|
||||||
|
|
||||||
va_start (args, message_format);
|
|
||||||
message = g_strdup_vprintf (message_format, args);
|
|
||||||
va_end (args);
|
|
||||||
|
|
||||||
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags), debug,
|
|
||||||
file, line, func, object_type, object, message);
|
file, line, func, object_type, object, message);
|
||||||
wp_log_fields_log (&lf);
|
wp_log_fields_log (&lf);
|
||||||
}
|
}
|
||||||
|
|
@ -997,7 +719,6 @@ wp_spa_log_logtv (void *object,
|
||||||
message = g_strdup_vprintf (fmt, args);
|
message = g_strdup_vprintf (fmt, args);
|
||||||
|
|
||||||
wp_log_fields_init (&lf, topic ? topic->topic : NULL, log_level,
|
wp_log_fields_init (&lf, topic ? topic->topic : NULL, log_level,
|
||||||
wp_want_debug_log (topic),
|
|
||||||
file, line_str, func, 0, NULL, message);
|
file, line_str, func, 0, NULL, message);
|
||||||
wp_log_fields_log (&lf);
|
wp_log_fields_log (&lf);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
98
lib/wp/log.h
98
lib/wp/log.h
|
|
@ -14,21 +14,7 @@
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
/*!
|
#define WP_LOG_LEVEL_TRACE (1 << G_LOG_LEVEL_USER_SHIFT)
|
||||||
* \brief A custom GLib log level for trace messages (extension of GLogLevelFlags)
|
|
||||||
* \ingroup wplog
|
|
||||||
*/
|
|
||||||
static const guint WP_LOG_LEVEL_TRACE = (1 << 8);
|
|
||||||
|
|
||||||
/*
|
|
||||||
The above WP_LOG_LEVEL_TRACE constant is intended to be defined as
|
|
||||||
(1 << G_LOG_LEVEL_USER_SHIFT), but due to a gobject-introspection bug
|
|
||||||
we define it with the value of G_LOG_LEVEL_USER_SHIFT, which is 8, so
|
|
||||||
that it ends up correctly in the bindings. To avoid value mismatches,
|
|
||||||
we statically verify here that G_LOG_LEVEL_USER_SHIFT is indeed 8.
|
|
||||||
See https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/540
|
|
||||||
*/
|
|
||||||
G_STATIC_ASSERT (G_LOG_LEVEL_USER_SHIFT == 8);
|
|
||||||
|
|
||||||
#define WP_OBJECT_FORMAT "<%s:%p>"
|
#define WP_OBJECT_FORMAT "<%s:%p>"
|
||||||
#define WP_OBJECT_ARGS(object) \
|
#define WP_OBJECT_ARGS(object) \
|
||||||
|
|
@ -38,68 +24,57 @@ WP_PRIVATE_API
|
||||||
void wp_log_init (gint flags);
|
void wp_log_init (gint flags);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_log_set_level (const gchar *log_level);
|
gboolean wp_log_set_global_level (const gchar *log_level);
|
||||||
|
|
||||||
/*!
|
typedef struct _WpLogTopic WpLogTopic;
|
||||||
* \brief WpLogTopic flags
|
struct _WpLogTopic {
|
||||||
* \ingroup wplog
|
|
||||||
*/
|
|
||||||
typedef enum { /*< flags >*/
|
|
||||||
/*! the lower 16 bits of the flags are GLogLevelFlags */
|
|
||||||
WP_LOG_TOPIC_LEVEL_MASK = 0xFFFF,
|
|
||||||
/*! the log topic has infinite lifetime (lives on static storage) */
|
|
||||||
WP_LOG_TOPIC_FLAG_STATIC = 1u << 30,
|
|
||||||
/*! the log topic has been initialized */
|
|
||||||
WP_LOG_TOPIC_FLAG_INITIALIZED = 1u << 31,
|
|
||||||
} WpLogTopicFlags;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief A structure representing a log topic
|
|
||||||
* \ingroup wplog
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
const char *topic_name;
|
const char *topic_name;
|
||||||
WpLogTopicFlags flags;
|
|
||||||
|
|
||||||
/*< private >*/
|
/*< private >*/
|
||||||
WP_PADDING(3)
|
/*
|
||||||
} WpLogTopic;
|
* lower 16 bits: GLogLevelFlags
|
||||||
|
* bit 29: has_custom_level
|
||||||
|
* bit 30: a g_bit_lock
|
||||||
|
* bit 31: 1 - initialized, 0 - not initialized
|
||||||
|
*/
|
||||||
|
gint flags;
|
||||||
|
gint *global_flags;
|
||||||
|
WP_PADDING(2)
|
||||||
|
};
|
||||||
|
|
||||||
#define WP_LOG_TOPIC_EXTERN(var) \
|
#define WP_LOG_TOPIC_EXTERN(var) \
|
||||||
extern WpLogTopic * var;
|
extern WpLogTopic * var;
|
||||||
|
|
||||||
#define WP_LOG_TOPIC(var, name) \
|
#define WP_LOG_TOPIC(var, t) \
|
||||||
WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
|
WpLogTopic var##_struct = { .topic_name = t, .flags = 0 }; \
|
||||||
WpLogTopic * var = &(var##_struct);
|
WpLogTopic * var = &(var##_struct);
|
||||||
|
|
||||||
#define WP_LOG_TOPIC_STATIC(var, name) \
|
#define WP_LOG_TOPIC_STATIC(var, t) \
|
||||||
static WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
|
static WpLogTopic var##_struct = { .topic_name = t, .flags = 0 }; \
|
||||||
static G_GNUC_UNUSED WpLogTopic * var = &(var##_struct);
|
static G_GNUC_UNUSED WpLogTopic * var = &(var##_struct);
|
||||||
|
|
||||||
#define WP_DEFINE_LOCAL_LOG_TOPIC(name) \
|
#define WP_DEFINE_LOCAL_LOG_TOPIC(t) \
|
||||||
WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, name)
|
WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, t)
|
||||||
|
|
||||||
/* make glib log functions also use the local log topic */
|
/* make glib log functions also use the local log topic */
|
||||||
#ifdef WP_USE_LOCAL_LOG_TOPIC_IN_G_LOG
|
#ifdef G_LOG_DOMAIN
|
||||||
# ifdef G_LOG_DOMAIN
|
# undef G_LOG_DOMAIN
|
||||||
# undef G_LOG_DOMAIN
|
|
||||||
# endif
|
|
||||||
# define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
|
|
||||||
#endif
|
#endif
|
||||||
|
#define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
void wp_log_topic_init (WpLogTopic *topic);
|
void wp_log_topic_init (WpLogTopic *topic);
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_log_topic_register (WpLogTopic *topic);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_log_topic_unregister (WpLogTopic *topic);
|
|
||||||
|
|
||||||
static inline gboolean
|
static inline gboolean
|
||||||
wp_log_topic_is_initialized (WpLogTopic *topic)
|
wp_log_topic_is_initialized (WpLogTopic *topic)
|
||||||
{
|
{
|
||||||
return (topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) != 0;
|
return (topic->flags & (1u << 31)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline gboolean
|
||||||
|
wp_log_topic_has_custom_level (WpLogTopic *topic)
|
||||||
|
{
|
||||||
|
return (topic->flags & (1u << 29)) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline gboolean
|
static inline gboolean
|
||||||
|
|
@ -109,7 +84,10 @@ wp_log_topic_is_enabled (WpLogTopic *topic, GLogLevelFlags log_level)
|
||||||
if (G_UNLIKELY (!wp_log_topic_is_initialized (topic)))
|
if (G_UNLIKELY (!wp_log_topic_is_initialized (topic)))
|
||||||
wp_log_topic_init (topic);
|
wp_log_topic_init (topic);
|
||||||
|
|
||||||
return (topic->flags & log_level & WP_LOG_TOPIC_LEVEL_MASK) != 0;
|
if (wp_log_topic_has_custom_level (topic))
|
||||||
|
return (topic->flags & (log_level & 0xFFFF)) != 0;
|
||||||
|
else
|
||||||
|
return (*topic->global_flags & (log_level & 0xFFFF)) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define wp_local_log_topic_is_enabled(log_level) \
|
#define wp_local_log_topic_is_enabled(log_level) \
|
||||||
|
|
@ -121,12 +99,6 @@ GLogWriterOutput wp_log_writer_default (GLogLevelFlags log_level,
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
|
void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
|
||||||
const gchar *file, const gchar *line, const gchar *func,
|
|
||||||
GType object_type, gconstpointer object,
|
|
||||||
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9) G_GNUC_DEPRECATED_FOR (wp_logt_checked);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_logt_checked (const WpLogTopic *topic, GLogLevelFlags log_level,
|
|
||||||
const gchar *file, const gchar *line, const gchar *func,
|
const gchar *file, const gchar *line, const gchar *func,
|
||||||
GType object_type, gconstpointer object,
|
GType object_type, gconstpointer object,
|
||||||
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9);
|
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9);
|
||||||
|
|
@ -134,7 +106,7 @@ void wp_logt_checked (const WpLogTopic *topic, GLogLevelFlags log_level,
|
||||||
#define wp_log(topic, level, type, object, ...) \
|
#define wp_log(topic, level, type, object, ...) \
|
||||||
({ \
|
({ \
|
||||||
if (G_UNLIKELY (wp_log_topic_is_enabled (topic, level))) \
|
if (G_UNLIKELY (wp_log_topic_is_enabled (topic, level))) \
|
||||||
wp_logt_checked (topic, level, __FILE__, G_STRINGIFY (__LINE__), \
|
wp_log_checked (topic->topic_name, level, __FILE__, G_STRINGIFY (__LINE__), \
|
||||||
G_STRFUNC, type, object, __VA_ARGS__); \
|
G_STRFUNC, type, object, __VA_ARGS__); \
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
wp_lib_sources = files(
|
wp_lib_sources = files(
|
||||||
'base-dirs.c',
|
|
||||||
'client.c',
|
'client.c',
|
||||||
'component-loader.c',
|
'component-loader.c',
|
||||||
'conf.c',
|
'conf.c',
|
||||||
|
|
@ -22,10 +21,8 @@ wp_lib_sources = files(
|
||||||
'object.c',
|
'object.c',
|
||||||
'object-interest.c',
|
'object-interest.c',
|
||||||
'object-manager.c',
|
'object-manager.c',
|
||||||
'permission-manager.c',
|
|
||||||
'plugin.c',
|
'plugin.c',
|
||||||
'port.c',
|
'port.c',
|
||||||
'proc-utils.c',
|
|
||||||
'properties.c',
|
'properties.c',
|
||||||
'proxy.c',
|
'proxy.c',
|
||||||
'proxy-interfaces.c',
|
'proxy-interfaces.c',
|
||||||
|
|
@ -43,11 +40,9 @@ wp_lib_sources = files(
|
||||||
wp_lib_priv_sources = files(
|
wp_lib_priv_sources = files(
|
||||||
'private/pipewire-object-mixin.c',
|
'private/pipewire-object-mixin.c',
|
||||||
'private/internal-comp-loader.c',
|
'private/internal-comp-loader.c',
|
||||||
'private/registry.c',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
wp_lib_headers = files(
|
wp_lib_headers = files(
|
||||||
'base-dirs.h',
|
|
||||||
'client.h',
|
'client.h',
|
||||||
'component-loader.h',
|
'component-loader.h',
|
||||||
'conf.h',
|
'conf.h',
|
||||||
|
|
@ -70,10 +65,8 @@ wp_lib_headers = files(
|
||||||
'object.h',
|
'object.h',
|
||||||
'object-interest.h',
|
'object-interest.h',
|
||||||
'object-manager.h',
|
'object-manager.h',
|
||||||
'permission-manager.h',
|
|
||||||
'plugin.h',
|
'plugin.h',
|
||||||
'port.h',
|
'port.h',
|
||||||
'proc-utils.h',
|
|
||||||
'properties.h',
|
'properties.h',
|
||||||
'proxy.h',
|
'proxy.h',
|
||||||
'proxy-interfaces.h',
|
'proxy-interfaces.h',
|
||||||
|
|
@ -116,26 +109,22 @@ wpversion = configure_file(
|
||||||
)
|
)
|
||||||
wp_gen_sources += [wpversion]
|
wp_gen_sources += [wpversion]
|
||||||
|
|
||||||
wpbuildbasedirs_data = configuration_data()
|
|
||||||
wpbuildbasedirs_data.set('BUILD_SYSCONFDIR', '"@0@"'.format(get_option('prefix') / get_option('sysconfdir')))
|
|
||||||
wpbuildbasedirs_data.set('BUILD_DATADIR', '"@0@"'.format(get_option('prefix') / get_option('datadir')))
|
|
||||||
wpbuildbasedirs_data.set('BUILD_LIBDIR', '"@0@"'.format(get_option('prefix') / get_option('libdir')))
|
|
||||||
wpbuildbasedirs_data.set('BUILD_LOCALEDIR', '"@0@"'.format(get_option('prefix') / get_option('localedir')))
|
|
||||||
wpbuildbasedirs = configure_file (
|
|
||||||
output : 'wpbuildbasedirs.h',
|
|
||||||
configuration : wpbuildbasedirs_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
wp_lib = library('wireplumber-' + wireplumber_api_version,
|
wp_lib = library('wireplumber-' + wireplumber_api_version,
|
||||||
wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion, wpbuildbasedirs,
|
wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion,
|
||||||
c_args : [
|
c_args : [
|
||||||
|
'-D_GNU_SOURCE',
|
||||||
|
'-DG_LOG_USE_STRUCTURED',
|
||||||
|
'-DWIREPLUMBER_DEFAULT_MODULE_DIR="@0@"'.format(wireplumber_module_dir),
|
||||||
|
'-DWIREPLUMBER_DEFAULT_CONFIG_DIR="@0@"'.format(wireplumber_config_dir),
|
||||||
|
'-DWIREPLUMBER_DEFAULT_DATA_DIR="@0@"'.format(wireplumber_data_dir),
|
||||||
|
'-DLOCALE_DIR="@0@"'.format(wireplumber_locale_dir),
|
||||||
'-DBUILDING_WP',
|
'-DBUILDING_WP',
|
||||||
],
|
],
|
||||||
install: true,
|
install: true,
|
||||||
include_directories: wp_lib_include_dir,
|
include_directories: wp_lib_include_dir,
|
||||||
dependencies : [gobject_dep, gmodule_dep, gio_dep, pipewire_dep, libintl_dep],
|
dependencies : [gobject_dep, gmodule_dep, gio_dep, pipewire_dep, libintl_dep],
|
||||||
soversion: wireplumber_so_version,
|
soversion: wireplumber_so_version,
|
||||||
version: wireplumber_libversion,
|
version: meson.project_version(),
|
||||||
)
|
)
|
||||||
|
|
||||||
wp_dep = declare_dependency(
|
wp_dep = declare_dependency(
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-metadata")
|
||||||
* gchar * value,
|
* gchar * value,
|
||||||
* gpointer user_data)
|
* gpointer user_data)
|
||||||
* \endcode
|
* \endcode
|
||||||
* Emitted when metadata change
|
* Emited when metadata change
|
||||||
*
|
*
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* - `subject` - the metadata subject id
|
* - `subject` - the metadata subject id
|
||||||
|
|
@ -319,129 +319,6 @@ wp_metadata_class_init (WpMetadataClass * klass)
|
||||||
G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \struct WpMetadataItem
|
|
||||||
*
|
|
||||||
* WpMetadataItem holds the subject, key, type and value of a metadata entry.
|
|
||||||
*/
|
|
||||||
struct _WpMetadataItem
|
|
||||||
{
|
|
||||||
WpMetadata *metadata;
|
|
||||||
guint32 subject;
|
|
||||||
const gchar *key;
|
|
||||||
const gchar *type;
|
|
||||||
const gchar *value;
|
|
||||||
};
|
|
||||||
|
|
||||||
G_DEFINE_BOXED_TYPE (WpMetadataItem, wp_metadata_item,
|
|
||||||
wp_metadata_item_ref, wp_metadata_item_unref)
|
|
||||||
|
|
||||||
static WpMetadataItem *
|
|
||||||
wp_metadata_item_new (WpMetadata *metadata, guint32 subject, const gchar *key,
|
|
||||||
const gchar *type, const gchar *value)
|
|
||||||
{
|
|
||||||
WpMetadataItem *self = g_rc_box_new0 (WpMetadataItem);
|
|
||||||
self->metadata = g_object_ref (metadata);
|
|
||||||
self->subject = subject;
|
|
||||||
self->key = key;
|
|
||||||
self->type = type;
|
|
||||||
self->value = value;
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_metadata_item_free (gpointer p)
|
|
||||||
{
|
|
||||||
WpMetadataItem *self = p;
|
|
||||||
g_clear_object (&self->metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Increases the reference count of a metadata item object
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
* \param self a metadata item object
|
|
||||||
* \returns (transfer full): \a self with an additional reference count on it
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
WpMetadataItem *
|
|
||||||
wp_metadata_item_ref (WpMetadataItem *self)
|
|
||||||
{
|
|
||||||
return g_rc_box_acquire (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Decreases the reference count on \a self and frees it when the ref
|
|
||||||
* count reaches zero.
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
* \param self (transfer full): a metadata item object
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_metadata_item_unref (WpMetadataItem *self)
|
|
||||||
{
|
|
||||||
g_rc_box_release_full (self, wp_metadata_item_free);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the subject from a metadata item
|
|
||||||
*
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
* \param self the item held by the GValue that was returned from the WpIterator
|
|
||||||
* of wp_metadata_new_iterator()
|
|
||||||
* \returns the metadata subject of the \a item
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
guint32
|
|
||||||
wp_metadata_item_get_subject (WpMetadataItem * self)
|
|
||||||
{
|
|
||||||
return self->subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the key from a metadata item
|
|
||||||
*
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
* \param self the item held by the GValue that was returned from the WpIterator
|
|
||||||
* of wp_metadata_new_iterator()
|
|
||||||
* \returns (transfer none): the metadata key of the \a item
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
const gchar *
|
|
||||||
wp_metadata_item_get_key (WpMetadataItem * self)
|
|
||||||
{
|
|
||||||
return self->key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the value type from a metadata item
|
|
||||||
*
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
* \param self the item held by the GValue that was returned from the WpIterator
|
|
||||||
* of wp_metadata_new_iterator()
|
|
||||||
* \returns (transfer none): the metadata value type of the \a item
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
const gchar *
|
|
||||||
wp_metadata_item_get_value_type (WpMetadataItem * self)
|
|
||||||
{
|
|
||||||
return self->type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the value from a metadata item
|
|
||||||
*
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
* \param self the item held by the GValue that was returned from the WpIterator
|
|
||||||
* of wp_metadata_new_iterator()
|
|
||||||
* \returns (transfer none): the metadata value of the \a item
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
const gchar *
|
|
||||||
wp_metadata_item_get_value (WpMetadataItem * self)
|
|
||||||
{
|
|
||||||
return self->value;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct metadata_iterator_data
|
struct metadata_iterator_data
|
||||||
{
|
{
|
||||||
WpMetadata *metadata;
|
WpMetadata *metadata;
|
||||||
|
|
@ -469,11 +346,8 @@ metadata_iterator_next (WpIterator *it, GValue *item)
|
||||||
while (pw_array_check (&priv->metadata, it_data->item)) {
|
while (pw_array_check (&priv->metadata, it_data->item)) {
|
||||||
if ((it_data->subject == PW_ID_ANY ||
|
if ((it_data->subject == PW_ID_ANY ||
|
||||||
it_data->subject == it_data->item->subject)) {
|
it_data->subject == it_data->item->subject)) {
|
||||||
g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
|
g_value_init (item, G_TYPE_POINTER);
|
||||||
it_data->item->subject, it_data->item->key, it_data->item->type,
|
g_value_set_pointer (item, (gpointer) it_data->item);
|
||||||
it_data->item->value);
|
|
||||||
g_value_init (item, WP_TYPE_METADATA_ITEM);
|
|
||||||
g_value_take_boxed (item, g_steal_pointer (&mi));
|
|
||||||
it_data->item++;
|
it_data->item++;
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
@ -495,11 +369,8 @@ metadata_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
||||||
if ((it_data->subject == PW_ID_ANY ||
|
if ((it_data->subject == PW_ID_ANY ||
|
||||||
it_data->subject == it_data->item->subject)) {
|
it_data->subject == it_data->item->subject)) {
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
g_auto (GValue) item = G_VALUE_INIT;
|
||||||
g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
|
g_value_init (&item, G_TYPE_POINTER);
|
||||||
it_data->item->subject, it_data->item->key, it_data->item->type,
|
g_value_set_pointer (&item, (gpointer) i);
|
||||||
it_data->item->value);
|
|
||||||
g_value_init (&item, WP_TYPE_METADATA_ITEM);
|
|
||||||
g_value_take_boxed (&item, g_steal_pointer (&mi));
|
|
||||||
if (!func (&item, ret, data))
|
if (!func (&item, ret, data))
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
@ -536,7 +407,8 @@ static const WpIteratorMethods metadata_iterator_methods = {
|
||||||
* \param self a metadata object
|
* \param self a metadata object
|
||||||
* \param subject the metadata subject id, or -1 (PW_ID_ANY)
|
* \param subject the metadata subject id, or -1 (PW_ID_ANY)
|
||||||
* \returns (transfer full): an iterator that iterates over the found metadata.
|
* \returns (transfer full): an iterator that iterates over the found metadata.
|
||||||
* The type of the iterator item is WpMetadataItem.
|
* Use wp_metadata_iterator_item_extract() to parse the items returned by
|
||||||
|
* this iterator.
|
||||||
*/
|
*/
|
||||||
WpIterator *
|
WpIterator *
|
||||||
wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
|
wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
|
||||||
|
|
@ -557,6 +429,33 @@ wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
|
||||||
return g_steal_pointer (&it);
|
return g_steal_pointer (&it);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Extracts the metadata subject, key, type and value out of a
|
||||||
|
* GValue that was returned from the WpIterator of wp_metadata_find()
|
||||||
|
*
|
||||||
|
* \ingroup wpmetadata
|
||||||
|
* \param item a GValue that was returned from the WpIterator of wp_metadata_find()
|
||||||
|
* \param subject (out)(optional): the subject id of the current item
|
||||||
|
* \param key (out)(optional)(transfer none): the key of the current item
|
||||||
|
* \param type (out)(optional)(transfer none): the type of the current item
|
||||||
|
* \param value (out)(optional)(transfer none): the value of the current item
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
wp_metadata_iterator_item_extract (const GValue * item, guint32 * subject,
|
||||||
|
const gchar ** key, const gchar ** type, const gchar ** value)
|
||||||
|
{
|
||||||
|
const struct item *i = g_value_get_pointer (item);
|
||||||
|
g_return_if_fail (i != NULL);
|
||||||
|
if (subject)
|
||||||
|
*subject = i->subject;
|
||||||
|
if (key)
|
||||||
|
*key = i->key;
|
||||||
|
if (type)
|
||||||
|
*type = i->type;
|
||||||
|
if (value)
|
||||||
|
*value = i->value;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Finds the metadata value given its \a subject and \a key.
|
* \brief Finds the metadata value given its \a subject and \a key.
|
||||||
*
|
*
|
||||||
|
|
@ -575,10 +474,8 @@ wp_metadata_find (WpMetadata * self, guint32 subject, const gchar * key,
|
||||||
g_auto (GValue) val = G_VALUE_INIT;
|
g_auto (GValue) val = G_VALUE_INIT;
|
||||||
it = wp_metadata_new_iterator (self, subject);
|
it = wp_metadata_new_iterator (self, subject);
|
||||||
for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
|
for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
|
||||||
WpMetadataItem *mi = g_value_get_boxed (&val);
|
const gchar *k = NULL, *t = NULL, *v = NULL;
|
||||||
const gchar *k = wp_metadata_item_get_key (mi);
|
wp_metadata_iterator_item_extract (&val, NULL, &k, &t, &v);
|
||||||
const gchar *t = wp_metadata_item_get_value_type (mi);
|
|
||||||
const gchar *v = wp_metadata_item_get_value (mi);
|
|
||||||
if (g_strcmp0 (k, key) == 0) {
|
if (g_strcmp0 (k, key) == 0) {
|
||||||
if (type)
|
if (type)
|
||||||
*type = t;
|
*type = t;
|
||||||
|
|
|
||||||
|
|
@ -13,36 +13,6 @@
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief The WpMetadataItem GType
|
|
||||||
* \ingroup wpmetadata
|
|
||||||
*/
|
|
||||||
#define WP_TYPE_METADATA_ITEM (wp_metadata_item_get_type ())
|
|
||||||
WP_API
|
|
||||||
GType wp_metadata_item_get_type (void);
|
|
||||||
|
|
||||||
typedef struct _WpMetadataItem WpMetadataItem;
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpMetadataItem *wp_metadata_item_ref (WpMetadataItem *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_metadata_item_unref (WpMetadataItem *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
guint32 wp_metadata_item_get_subject (WpMetadataItem * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar * wp_metadata_item_get_key (WpMetadataItem * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar * wp_metadata_item_get_value_type (WpMetadataItem * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar * wp_metadata_item_get_value (WpMetadataItem * self);
|
|
||||||
|
|
||||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpMetadataItem, wp_metadata_item_unref)
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief An extension of WpProxyFeatures for WpMetadata objects
|
* \brief An extension of WpProxyFeatures for WpMetadata objects
|
||||||
* \ingroup wpmetadata
|
* \ingroup wpmetadata
|
||||||
|
|
@ -72,6 +42,10 @@ struct _WpMetadataClass
|
||||||
WP_API
|
WP_API
|
||||||
WpIterator * wp_metadata_new_iterator (WpMetadata * self, guint32 subject);
|
WpIterator * wp_metadata_new_iterator (WpMetadata * self, guint32 subject);
|
||||||
|
|
||||||
|
WP_API
|
||||||
|
void wp_metadata_iterator_item_extract (const GValue * item, guint32 * subject,
|
||||||
|
const gchar ** key, const gchar ** type, const gchar ** value);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
const gchar * wp_metadata_find (WpMetadata * self, guint32 subject,
|
const gchar * wp_metadata_find (WpMetadata * self, guint32 subject,
|
||||||
const gchar * key, const gchar ** type);
|
const gchar * key, const gchar ** type);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ struct _WpImplModule
|
||||||
WpProperties *props; /* only used during module load */
|
WpProperties *props; /* only used during module load */
|
||||||
|
|
||||||
struct pw_impl_module *pw_impl_module;
|
struct pw_impl_module *pw_impl_module;
|
||||||
struct spa_hook impl_module_listener;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DEFINE_TYPE (WpImplModule, wp_impl_module, G_TYPE_OBJECT);
|
G_DEFINE_TYPE (WpImplModule, wp_impl_module, G_TYPE_OBJECT);
|
||||||
|
|
@ -47,17 +46,6 @@ enum {
|
||||||
PROP_PW_IMPL_MODULE,
|
PROP_PW_IMPL_MODULE,
|
||||||
};
|
};
|
||||||
|
|
||||||
static void impl_module_free (void *data)
|
|
||||||
{
|
|
||||||
WpImplModule *self = WP_IMPL_MODULE (data);
|
|
||||||
self->pw_impl_module = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct pw_impl_module_events impl_module_events = {
|
|
||||||
PW_VERSION_IMPL_MODULE_EVENTS,
|
|
||||||
.free = impl_module_free,
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
wp_impl_module_init (WpImplModule * self)
|
wp_impl_module_init (WpImplModule * self)
|
||||||
{
|
{
|
||||||
|
|
@ -92,15 +80,10 @@ wp_impl_module_constructed (GObject * object)
|
||||||
self->pw_impl_module =
|
self->pw_impl_module =
|
||||||
pw_context_load_module (context, self->name, self->args, props);
|
pw_context_load_module (context, self->name, self->args, props);
|
||||||
|
|
||||||
if (self->pw_impl_module) {
|
if (self->pw_impl_module && self->props) {
|
||||||
if (self->props) {
|
/* With the module loaded, properties are just passthrough now */
|
||||||
/* With the module loaded, properties are just passthrough now */
|
wp_properties_unref (self->props);
|
||||||
wp_properties_unref (self->props);
|
self->props = NULL;
|
||||||
self->props = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
pw_impl_module_add_listener (self->pw_impl_module,
|
|
||||||
&self->impl_module_listener, &impl_module_events, self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_impl_module_parent_class)->constructed (object);
|
G_OBJECT_CLASS (wp_impl_module_parent_class)->constructed (object);
|
||||||
|
|
@ -121,8 +104,6 @@ wp_impl_module_finalize (GObject * object)
|
||||||
|
|
||||||
if (self->props)
|
if (self->props)
|
||||||
wp_properties_unref (self->props);
|
wp_properties_unref (self->props);
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_impl_module_parent_class)->finalize (object);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-node")
|
||||||
* \code
|
* \code
|
||||||
* void
|
* void
|
||||||
* state_changed_callback (WpNode * self,
|
* state_changed_callback (WpNode * self,
|
||||||
* WpNodeState old_state,
|
* WpNodeState * old_state,
|
||||||
* WpNodeState new_state,
|
* WpNodeState * new_state,
|
||||||
* gpointer user_data)
|
* gpointer user_data)
|
||||||
* \endcode
|
* \endcode
|
||||||
*
|
*
|
||||||
|
|
@ -631,7 +631,7 @@ wp_node_send_command (WpNode * self, const gchar * command)
|
||||||
|
|
||||||
struct spa_command cmd =
|
struct spa_command cmd =
|
||||||
SPA_NODE_COMMAND_INIT(wp_spa_id_value_number (command_value));
|
SPA_NODE_COMMAND_INIT(wp_spa_id_value_number (command_value));
|
||||||
pw_node_send_command ((struct pw_node*)wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
|
pw_node_send_command (wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! \defgroup wpimplnode WpImplNode */
|
/*! \defgroup wpimplnode WpImplNode */
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ G_DEFINE_BOXED_TYPE (WpObjectInterest, wp_object_interest,
|
||||||
* For further reading on the constraint's arguments, see
|
* For further reading on the constraint's arguments, see
|
||||||
* wp_object_interest_add_constraint()
|
* wp_object_interest_add_constraint()
|
||||||
*
|
*
|
||||||
* For example, this interest matches objects that are descendants of WpProxy
|
* For example, this interest matches objects that are descendands of WpProxy
|
||||||
* with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
|
* with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
|
||||||
* called "format.dsp" that contains the string "audio" somewhere in the value
|
* called "format.dsp" that contains the string "audio" somewhere in the value
|
||||||
* and with a pipewire property "port.name" being present (with any value):
|
* and with a pipewire property "port.name" being present (with any value):
|
||||||
|
|
@ -770,8 +770,6 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
||||||
if (!pw_global_props && WP_IS_SESSION_ITEM (object)) {
|
if (!pw_global_props && WP_IS_SESSION_ITEM (object)) {
|
||||||
WpSessionItem *si = (WpSessionItem *) object;
|
WpSessionItem *si = (WpSessionItem *) object;
|
||||||
pw_global_props = props = wp_session_item_get_properties (si);
|
pw_global_props = props = wp_session_item_get_properties (si);
|
||||||
if (!pw_props)
|
|
||||||
pw_props = props;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -881,51 +879,3 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Finds all the defined constraint values for a subject in \a self.
|
|
||||||
*
|
|
||||||
* A defined constraint value is the value of a constraint with the 'equal' or
|
|
||||||
* 'in-list' verb, because the full value must be defined with those verbs. This
|
|
||||||
* can be useful for cases where we want to enumerate interests that are
|
|
||||||
* interested in specific subjects.
|
|
||||||
*
|
|
||||||
* \ingroup wpobjectinterest
|
|
||||||
* \param self the object interest
|
|
||||||
* \param type the constraint type
|
|
||||||
* \param subject the subject that the constraint applies to
|
|
||||||
* \returns (element-type GVariant) (transfer full) (nullable): the defined
|
|
||||||
* constraint values for this object interest.
|
|
||||||
* \since 0.5.13
|
|
||||||
*/
|
|
||||||
GPtrArray *
|
|
||||||
wp_object_interest_find_defined_constraint_values (WpObjectInterest * self,
|
|
||||||
WpConstraintType type, const gchar * subject)
|
|
||||||
{
|
|
||||||
GPtrArray *res = g_ptr_array_new_with_free_func (
|
|
||||||
(GDestroyNotify)g_variant_unref);
|
|
||||||
struct constraint *c;
|
|
||||||
|
|
||||||
pw_array_for_each (c, &self->constraints) {
|
|
||||||
if ((c->type == type || WP_CONSTRAINT_TYPE_NONE == type) &&
|
|
||||||
g_str_equal (c->subject, subject)) {
|
|
||||||
switch (c->verb) {
|
|
||||||
case WP_CONSTRAINT_VERB_EQUALS:
|
|
||||||
g_ptr_array_add (res, g_variant_ref (c->value));
|
|
||||||
break;
|
|
||||||
case WP_CONSTRAINT_VERB_IN_LIST: {
|
|
||||||
GVariantIter iter;
|
|
||||||
GVariant *child;
|
|
||||||
g_variant_iter_init (&iter, c->value);
|
|
||||||
while ((child = g_variant_iter_next_value (&iter)))
|
|
||||||
g_ptr_array_add (res, child);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -67,25 +67,26 @@ typedef enum { /*< flags >*/
|
||||||
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES = (1 << 1),
|
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES = (1 << 1),
|
||||||
WP_INTEREST_MATCH_PW_PROPERTIES = (1 << 2),
|
WP_INTEREST_MATCH_PW_PROPERTIES = (1 << 2),
|
||||||
WP_INTEREST_MATCH_G_PROPERTIES = (1 << 3),
|
WP_INTEREST_MATCH_G_PROPERTIES = (1 << 3),
|
||||||
|
|
||||||
/*!
|
|
||||||
* Special WpInterestMatch value that indicates that all constraints
|
|
||||||
* have been matched
|
|
||||||
*/
|
|
||||||
WP_INTEREST_MATCH_ALL =
|
|
||||||
(WP_INTEREST_MATCH_GTYPE |
|
|
||||||
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
|
|
||||||
WP_INTEREST_MATCH_PW_PROPERTIES |
|
|
||||||
WP_INTEREST_MATCH_G_PROPERTIES),
|
|
||||||
} WpInterestMatch;
|
} WpInterestMatch;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Special WpInterestMatch value that indicates that all constraints
|
||||||
|
* have been matched
|
||||||
|
* \ingroup wpobjectinterest
|
||||||
|
*/
|
||||||
|
#define WP_INTEREST_MATCH_ALL \
|
||||||
|
(WP_INTEREST_MATCH_GTYPE | \
|
||||||
|
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES | \
|
||||||
|
WP_INTEREST_MATCH_PW_PROPERTIES | \
|
||||||
|
WP_INTEREST_MATCH_G_PROPERTIES)
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Flags to alter the behaviour of wp_object_interest_matches_full()
|
* \brief Flags to alter the behaviour of wp_object_interest_matches_full()
|
||||||
* \ingroup wpobjectinterest
|
* \ingroup wpobjectinterest
|
||||||
*/
|
*/
|
||||||
typedef enum { /*< flags >*/
|
typedef enum { /*< flags >*/
|
||||||
WP_INTEREST_MATCH_FLAGS_NONE = 0,
|
WP_INTEREST_MATCH_FLAGS_NONE = 0,
|
||||||
/*! check all the constraints instead of returning after the first mismatch */
|
/*! check all the constraints instead of returning after the first mis-match */
|
||||||
WP_INTEREST_MATCH_FLAGS_CHECK_ALL = (1 << 0),
|
WP_INTEREST_MATCH_FLAGS_CHECK_ALL = (1 << 0),
|
||||||
} WpInterestMatchFlags;
|
} WpInterestMatchFlags;
|
||||||
|
|
||||||
|
|
@ -130,10 +131,6 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
|
||||||
WpInterestMatchFlags flags, GType object_type, gpointer object,
|
WpInterestMatchFlags flags, GType object_type, gpointer object,
|
||||||
WpProperties * pw_props, WpProperties * pw_global_props);
|
WpProperties * pw_props, WpProperties * pw_global_props);
|
||||||
|
|
||||||
WP_API
|
|
||||||
GPtrArray * wp_object_interest_find_defined_constraint_values (
|
|
||||||
WpObjectInterest * self, WpConstraintType type, const gchar * subject);
|
|
||||||
|
|
||||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
|
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
|
||||||
|
|
@ -587,7 +587,7 @@ wp_object_manager_lookup (WpObjectManager * self, GType gtype, ...)
|
||||||
*
|
*
|
||||||
* \ingroup wpobjectmanager
|
* \ingroup wpobjectmanager
|
||||||
* \param self the object manager
|
* \param self the object manager
|
||||||
* \param interest (transfer full): the interest
|
* \param interest (transfer full): the interst
|
||||||
* \returns (type GObject)(transfer full)(nullable): the first managed object
|
* \returns (type GObject)(transfer full)(nullable): the first managed object
|
||||||
* that matches the lookup interest, or NULL if no object matches
|
* that matches the lookup interest, or NULL if no object matches
|
||||||
*/
|
*/
|
||||||
|
|
@ -672,13 +672,7 @@ idle_emit_objects_changed (WpObjectManager * self)
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
static void
|
||||||
* \brief Checks if the object manager should emit the 'objects-changed' signal
|
|
||||||
* \private
|
|
||||||
* \ingroup wpobjectmanager
|
|
||||||
* \param self the object manager
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_object_manager_maybe_objects_changed (WpObjectManager * self)
|
wp_object_manager_maybe_objects_changed (WpObjectManager * self)
|
||||||
{
|
{
|
||||||
wp_trace_object (self, "pending:%u changed:%d idle_source:%p installed:%d",
|
wp_trace_object (self, "pending:%u changed:%d idle_source:%p installed:%d",
|
||||||
|
|
@ -730,15 +724,8 @@ wp_object_manager_maybe_objects_changed (WpObjectManager * self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/* caller must also call wp_object_manager_maybe_objects_changed() after */
|
||||||
* \brief Adds an object to the object manager.
|
static void
|
||||||
* \private
|
|
||||||
* \ingroup wpobjectmanager
|
|
||||||
* \param self the object manager
|
|
||||||
* \param object (transfer none): the object to add
|
|
||||||
* \note caller must also call wp_object_manager_maybe_objects_changed() after
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_object_manager_add_object (WpObjectManager * self, gpointer object)
|
wp_object_manager_add_object (WpObjectManager * self, gpointer object)
|
||||||
{
|
{
|
||||||
if (wp_object_manager_is_interested_in_object (self, object)) {
|
if (wp_object_manager_is_interested_in_object (self, object)) {
|
||||||
|
|
@ -749,15 +736,8 @@ wp_object_manager_add_object (WpObjectManager * self, gpointer object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/* caller must also call wp_object_manager_maybe_objects_changed() after */
|
||||||
* \brief Removes an object from the object manager.
|
static void
|
||||||
* \private
|
|
||||||
* \ingroup wpobjectmanager
|
|
||||||
* \param self the object manager
|
|
||||||
* \param object the object to remove
|
|
||||||
* \note caller must also call wp_object_manager_maybe_objects_changed() after
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
|
wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
|
||||||
{
|
{
|
||||||
guint index;
|
guint index;
|
||||||
|
|
@ -785,15 +765,8 @@ on_proxy_ready (GObject * proxy, GAsyncResult * res, gpointer data)
|
||||||
wp_object_manager_maybe_objects_changed (self);
|
wp_object_manager_maybe_objects_changed (self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/* caller must also call wp_object_manager_maybe_objects_changed() after */
|
||||||
* \brief Adds a global object to the object manager.
|
static void
|
||||||
* \private
|
|
||||||
* \ingroup wpobjectmanager
|
|
||||||
* \param self the object manager
|
|
||||||
* \param global the global object to add
|
|
||||||
* \note caller must also call wp_object_manager_maybe_objects_changed() after
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
|
wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
|
||||||
{
|
{
|
||||||
WpProxyFeatures features = 0;
|
WpProxyFeatures features = 0;
|
||||||
|
|
@ -822,6 +795,405 @@ wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* WpRegistry:
|
||||||
|
*
|
||||||
|
* The registry keeps track of registered objects on the wireplumber core.
|
||||||
|
* There are 3 kinds of registered objects:
|
||||||
|
*
|
||||||
|
* 1) PipeWire global objects, which live in another process.
|
||||||
|
*
|
||||||
|
* These objects are represented by a WpGlobal with the
|
||||||
|
* WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
|
||||||
|
* the registry_global() event is fired and are removed by
|
||||||
|
* registry_global_remove(). These objects do not have an associated
|
||||||
|
* WpProxy, unless there is at least one WpObjectManager that is interested
|
||||||
|
* in them. In this case, a WpProxy is constructed and it is owned by the
|
||||||
|
* WpGlobal until the global is removed by the registry_global_remove() event.
|
||||||
|
*
|
||||||
|
* 2) PipeWire global objects, which were constructed by this process, either
|
||||||
|
* by calling into a remove factory (see wp_node_new_from_factory()) or
|
||||||
|
* by exporting a local object (WpImplSession etc...).
|
||||||
|
*
|
||||||
|
* These objects are also represented by a WpGlobal, which may however be
|
||||||
|
* constructed before they appear on the registry. The associated WpProxy
|
||||||
|
* calls into wp_registry_prepare_new_global() at the time it receives
|
||||||
|
* the 'bound' event and creates a global that has the
|
||||||
|
* WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
|
||||||
|
* these globals are "owned" by the WpProxy and the WpGlobal has no ref
|
||||||
|
* on the WpProxy itself. This allows destroying the proxy in client code
|
||||||
|
* by dropping its last reference.
|
||||||
|
*
|
||||||
|
* Normally, these global objects also appear on the pipewire registry. When
|
||||||
|
* this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
|
||||||
|
* and that keeps an additional reference on the global (both flags must
|
||||||
|
* be dropped before the WpGlobal is destroyed).
|
||||||
|
*
|
||||||
|
* In some cases, such an object might appear first on the registry and
|
||||||
|
* then receive the 'bound' event. In order to handle this situation, globals
|
||||||
|
* are not advertised immediately when they appear on the registry, but
|
||||||
|
* they are added on a tmp_globals list instead, which is emptied on the
|
||||||
|
* next core sync. In all cases, the proxy 'bound' and the registry 'global'
|
||||||
|
* events will be fired in the same sync cycle, so we can catch a late
|
||||||
|
* 'bound' event and still associate the proxy with the WpGlobal before
|
||||||
|
* object managers are notified about the existence of this global.
|
||||||
|
*
|
||||||
|
* 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
|
||||||
|
*
|
||||||
|
* These are local objects that have nothing to do with PipeWire. They do not
|
||||||
|
* have a global id and they are also not subclasses of WpProxy. The registry
|
||||||
|
* always owns a reference on them, so that they are kept alive for as long
|
||||||
|
* as the WpCore is alive.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#undef G_LOG_DOMAIN
|
||||||
|
#define G_LOG_DOMAIN "wp-registry"
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
|
||||||
|
{
|
||||||
|
for (guint i = 0; i < self->object_managers->len; i++) {
|
||||||
|
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
||||||
|
wp_object_manager_add_object (om, object);
|
||||||
|
wp_object_manager_maybe_objects_changed (om);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
|
||||||
|
{
|
||||||
|
for (guint i = 0; i < self->object_managers->len; i++) {
|
||||||
|
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
||||||
|
wp_object_manager_rm_object (om, object);
|
||||||
|
wp_object_manager_maybe_objects_changed (om);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
object_manager_destroyed (gpointer data, GObject * om)
|
||||||
|
{
|
||||||
|
WpRegistry *self = data;
|
||||||
|
g_ptr_array_remove_fast (self->object_managers, om);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* find the subclass of WpPipewireGloabl that can handle
|
||||||
|
the given pipewire interface type of the given version */
|
||||||
|
static inline GType
|
||||||
|
find_proxy_instance_type (const char * type, guint32 version)
|
||||||
|
{
|
||||||
|
g_autofree GType *children;
|
||||||
|
guint n_children;
|
||||||
|
|
||||||
|
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
|
||||||
|
|
||||||
|
for (guint i = 0; i < n_children; i++) {
|
||||||
|
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
|
||||||
|
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
|
||||||
|
klass->pw_iface_version == version) {
|
||||||
|
g_type_class_unref (klass);
|
||||||
|
return children[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
g_type_class_unref (klass);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WP_TYPE_GLOBAL_PROXY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* called by the registry when a global appears */
|
||||||
|
static void
|
||||||
|
registry_global (void *data, uint32_t id, uint32_t permissions,
|
||||||
|
const char *type, uint32_t version, const struct spa_dict *props)
|
||||||
|
{
|
||||||
|
WpRegistry *self = data;
|
||||||
|
GType gtype = find_proxy_instance_type (type, version);
|
||||||
|
|
||||||
|
wp_debug_object (wp_registry_get_core (self),
|
||||||
|
"global:%u perm:0x%x type:%s/%u -> %s",
|
||||||
|
id, permissions, type, version, g_type_name (gtype));
|
||||||
|
|
||||||
|
wp_registry_prepare_new_global (self, id, permissions,
|
||||||
|
WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* called by the registry when a global is removed */
|
||||||
|
static void
|
||||||
|
registry_global_remove (void *data, uint32_t id)
|
||||||
|
{
|
||||||
|
WpRegistry *self = data;
|
||||||
|
WpGlobal *global = NULL;
|
||||||
|
|
||||||
|
if (id < self->globals->len)
|
||||||
|
global = g_ptr_array_index (self->globals, id);
|
||||||
|
|
||||||
|
/* if not found, look in the tmp_globals, as it may still not be exposed */
|
||||||
|
if (!global) {
|
||||||
|
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
||||||
|
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
||||||
|
if (g->id == id) {
|
||||||
|
global = g;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_return_if_fail (global &&
|
||||||
|
global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||||
|
|
||||||
|
wp_debug_object (wp_registry_get_core (self),
|
||||||
|
"global removed:%u type:%s", id, g_type_name (global->type));
|
||||||
|
|
||||||
|
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct pw_registry_events registry_events = {
|
||||||
|
PW_VERSION_REGISTRY_EVENTS,
|
||||||
|
.global = registry_global,
|
||||||
|
.global_remove = registry_global_remove,
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_registry_init (WpRegistry *self)
|
||||||
|
{
|
||||||
|
self->globals =
|
||||||
|
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||||
|
self->tmp_globals =
|
||||||
|
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||||
|
self->objects = g_ptr_array_new_with_free_func (g_object_unref);
|
||||||
|
self->object_managers = g_ptr_array_new ();
|
||||||
|
self->features = g_ptr_array_new_with_free_func (g_free);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_registry_clear (WpRegistry *self)
|
||||||
|
{
|
||||||
|
wp_registry_detach (self);
|
||||||
|
g_clear_pointer (&self->globals, g_ptr_array_unref);
|
||||||
|
g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
|
||||||
|
g_clear_pointer (&self->features, g_ptr_array_unref);
|
||||||
|
|
||||||
|
/* remove all the registered objects
|
||||||
|
this will normally also destroy the object managers, eventually, since
|
||||||
|
they are normally ref'ed by modules, which are registered objects */
|
||||||
|
{
|
||||||
|
g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);
|
||||||
|
|
||||||
|
while (objlist->len > 0) {
|
||||||
|
g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
|
||||||
|
objlist->len - 1);
|
||||||
|
wp_registry_notify_rm_object (self, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* in case there are any object managers left,
|
||||||
|
remove the weak ref on them and let them be... */
|
||||||
|
{
|
||||||
|
g_autoptr (GPtrArray) object_mgrs;
|
||||||
|
GObject *om;
|
||||||
|
|
||||||
|
object_mgrs = g_steal_pointer (&self->object_managers);
|
||||||
|
|
||||||
|
while (object_mgrs->len > 0) {
|
||||||
|
om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
|
||||||
|
g_object_weak_unref (om, object_manager_destroyed, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
|
||||||
|
{
|
||||||
|
self->pw_registry = pw_core_get_registry (pw_core,
|
||||||
|
PW_VERSION_REGISTRY, 0);
|
||||||
|
pw_registry_add_listener (self->pw_registry, &self->listener,
|
||||||
|
®istry_events, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_registry_detach (WpRegistry *self)
|
||||||
|
{
|
||||||
|
if (self->pw_registry) {
|
||||||
|
spa_hook_remove (&self->listener);
|
||||||
|
pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
|
||||||
|
self->pw_registry = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* remove pipewire globals */
|
||||||
|
GPtrArray *objlist = self->globals;
|
||||||
|
while (objlist && objlist->len > 0) {
|
||||||
|
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
||||||
|
objlist->len - 1);
|
||||||
|
|
||||||
|
if (!global)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (global->proxy)
|
||||||
|
wp_registry_notify_rm_object (self, global->proxy);
|
||||||
|
|
||||||
|
/* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
|
||||||
|
by the registry; set registry to NULL to avoid further interference */
|
||||||
|
global->registry = NULL;
|
||||||
|
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||||
|
|
||||||
|
/* the registry's ref on global is dropped here; it may still live if
|
||||||
|
there is a proxy that owns a ref on it, but global->registry is set
|
||||||
|
to NULL, so there is no further interference */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* drop tmp globals as well */
|
||||||
|
objlist = self->tmp_globals;
|
||||||
|
while (objlist && objlist->len > 0) {
|
||||||
|
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
||||||
|
objlist->len - 1);
|
||||||
|
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
expose_tmp_globals (WpCore *core)
|
||||||
|
{
|
||||||
|
WpRegistry *self = wp_core_get_registry (core);
|
||||||
|
g_autoptr (GPtrArray) tmp_globals = NULL;
|
||||||
|
g_autoptr (GPtrArray) object_managers = NULL;
|
||||||
|
|
||||||
|
/* in case the registry was cleared in the meantime... */
|
||||||
|
if (G_UNLIKELY (!self->tmp_globals))
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
|
||||||
|
/* steal the tmp_globals list and replace it with an empty one */
|
||||||
|
tmp_globals = self->tmp_globals;
|
||||||
|
self->tmp_globals =
|
||||||
|
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||||
|
|
||||||
|
wp_debug_object (core, "exposing %u new globals", tmp_globals->len);
|
||||||
|
|
||||||
|
/* traverse in the order that the globals appeared on the registry */
|
||||||
|
for (guint i = 0; i < tmp_globals->len; i++) {
|
||||||
|
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
||||||
|
|
||||||
|
/* if global was already removed, drop it */
|
||||||
|
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/* if old global is owned by proxy, remove it */
|
||||||
|
if (self->globals->len > g->id) {
|
||||||
|
WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
|
||||||
|
if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
|
||||||
|
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_return_val_if_fail (self->globals->len <= g->id ||
|
||||||
|
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
|
||||||
|
|
||||||
|
/* set the registry, so that wp_global_rm_flag() can work full-scale */
|
||||||
|
g->registry = self;
|
||||||
|
|
||||||
|
/* store it in the globals list */
|
||||||
|
if (self->globals->len <= g->id)
|
||||||
|
g_ptr_array_set_size (self->globals, g->id + 1);
|
||||||
|
g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
|
||||||
|
}
|
||||||
|
|
||||||
|
object_managers = g_ptr_array_copy (self->object_managers,
|
||||||
|
(GCopyFunc) g_object_ref, NULL);
|
||||||
|
g_ptr_array_set_free_func (object_managers, g_object_unref);
|
||||||
|
|
||||||
|
/* notify object managers */
|
||||||
|
for (guint i = 0; i < object_managers->len; i++) {
|
||||||
|
WpObjectManager *om = g_ptr_array_index (object_managers, i);
|
||||||
|
|
||||||
|
for (guint i = 0; i < tmp_globals->len; i++) {
|
||||||
|
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
||||||
|
|
||||||
|
/* if global was already removed, drop it */
|
||||||
|
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
wp_object_manager_add_global (om, g);
|
||||||
|
}
|
||||||
|
wp_object_manager_maybe_objects_changed (om);
|
||||||
|
}
|
||||||
|
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* \param new_global (out) (transfer full) (optional): the new global
|
||||||
|
*
|
||||||
|
* This is normally called up to 2 times in the same sync cycle:
|
||||||
|
* one from registry_global(), another from the proxy bound event
|
||||||
|
* Unfortunately the order in which those 2 events happen is specific
|
||||||
|
* to the implementation of the object, which is why this is implemented
|
||||||
|
* with a temporary globals list that get exposed later to the object managers
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
||||||
|
guint32 permissions, guint32 flag, GType type,
|
||||||
|
WpGlobalProxy *proxy, const struct spa_dict *props,
|
||||||
|
WpGlobal ** new_global)
|
||||||
|
{
|
||||||
|
g_autoptr (WpGlobal) global = NULL;
|
||||||
|
WpCore *core = wp_registry_get_core (self);
|
||||||
|
|
||||||
|
g_return_if_fail (flag != 0);
|
||||||
|
|
||||||
|
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
||||||
|
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
||||||
|
if (g->id == id) {
|
||||||
|
global = wp_global_ref (g);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
|
||||||
|
global ? "reuse" : "new", id, g_type_name (type),
|
||||||
|
(global && global->proxy) ? global->proxy : proxy);
|
||||||
|
|
||||||
|
if (!global) {
|
||||||
|
global = g_rc_box_new0 (WpGlobal);
|
||||||
|
global->flags = flag;
|
||||||
|
global->id = id;
|
||||||
|
global->type = type;
|
||||||
|
global->permissions = permissions;
|
||||||
|
global->properties = props ?
|
||||||
|
wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
|
||||||
|
global->proxy = proxy;
|
||||||
|
g_ptr_array_add (self->tmp_globals, wp_global_ref (global));
|
||||||
|
|
||||||
|
/* ensure we have 'object.id' so that we can filter by id on object managers */
|
||||||
|
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);
|
||||||
|
|
||||||
|
/* schedule exposing when adding the first global */
|
||||||
|
if (self->tmp_globals->len == 1) {
|
||||||
|
wp_core_idle_add_closure (core, NULL,
|
||||||
|
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* store the most permissive permissions */
|
||||||
|
if (permissions > global->permissions)
|
||||||
|
global->permissions = permissions;
|
||||||
|
|
||||||
|
global->flags |= flag;
|
||||||
|
|
||||||
|
/* store the most deep type (i.e. WpImplNode instead of WpNode),
|
||||||
|
so that object-manager interests can work more accurately
|
||||||
|
if the interest is on a specific subclass */
|
||||||
|
if (g_type_depth (type) > g_type_depth (global->type))
|
||||||
|
global->type = type;
|
||||||
|
|
||||||
|
if (proxy) {
|
||||||
|
g_return_if_fail (global->proxy == NULL);
|
||||||
|
global->proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props)
|
||||||
|
wp_properties_update_from_dict (global->properties, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_global)
|
||||||
|
*new_global = g_steal_pointer (&global);
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Installs the object manager on this core, activating its internal
|
* \brief Installs the object manager on this core, activating its internal
|
||||||
* management engine.
|
* management engine.
|
||||||
|
|
@ -837,12 +1209,111 @@ void
|
||||||
wp_core_install_object_manager (WpCore * self, WpObjectManager * om)
|
wp_core_install_object_manager (WpCore * self, WpObjectManager * om)
|
||||||
{
|
{
|
||||||
WpRegistry *reg;
|
WpRegistry *reg;
|
||||||
|
guint i;
|
||||||
|
|
||||||
g_return_if_fail (WP_IS_CORE (self));
|
g_return_if_fail (WP_IS_CORE (self));
|
||||||
g_return_if_fail (WP_IS_OBJECT_MANAGER (om));
|
g_return_if_fail (WP_IS_OBJECT_MANAGER (om));
|
||||||
|
|
||||||
|
reg = wp_core_get_registry (self);
|
||||||
|
|
||||||
|
g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, reg);
|
||||||
|
g_ptr_array_add (reg->object_managers, om);
|
||||||
g_weak_ref_set (&om->core, self);
|
g_weak_ref_set (&om->core, self);
|
||||||
|
|
||||||
reg = wp_core_get_registry (self);
|
/* add pre-existing objects to the object manager,
|
||||||
wp_registry_install_object_manager (reg, om);
|
in case it's interested in them */
|
||||||
|
for (i = 0; i < reg->globals->len; i++) {
|
||||||
|
WpGlobal *g = g_ptr_array_index (reg->globals, i);
|
||||||
|
/* check if null because the globals array can have gaps */
|
||||||
|
if (g)
|
||||||
|
wp_object_manager_add_global (om, g);
|
||||||
|
}
|
||||||
|
for (i = 0; i < reg->objects->len; i++) {
|
||||||
|
GObject *o = g_ptr_array_index (reg->objects, i);
|
||||||
|
wp_object_manager_add_object (om, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_object_manager_maybe_objects_changed (om);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WpGlobal */
|
||||||
|
|
||||||
|
G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)
|
||||||
|
|
||||||
|
void
|
||||||
|
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
|
||||||
|
{
|
||||||
|
WpRegistry *reg = global->registry;
|
||||||
|
guint32 id = global->id;
|
||||||
|
|
||||||
|
/* no flag to remove */
|
||||||
|
if (!(global->flags & rm_flag))
|
||||||
|
return;
|
||||||
|
|
||||||
|
wp_trace_boxed (WP_TYPE_GLOBAL, global,
|
||||||
|
"remove global %u flag 0x%x [flags:0x%x, reg:%p]",
|
||||||
|
id, rm_flag, global->flags, reg);
|
||||||
|
|
||||||
|
/* global was owned by the proxy; by removing the flag, we clear out
|
||||||
|
also the proxy pointer, which is presumably no longer valid and we
|
||||||
|
notify all listeners that the proxy is gone */
|
||||||
|
if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
|
||||||
|
global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
|
||||||
|
if (reg && global->proxy) {
|
||||||
|
wp_registry_notify_rm_object (reg, global->proxy);
|
||||||
|
}
|
||||||
|
global->proxy = NULL;
|
||||||
|
}
|
||||||
|
/* registry removed the global */
|
||||||
|
else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
|
||||||
|
global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;
|
||||||
|
|
||||||
|
/* destroy the proxy if it exists */
|
||||||
|
if (global->proxy) {
|
||||||
|
/* steal the proxy to avoid calling wp_registry_notify_rm_object()
|
||||||
|
again while removing OWNED_BY_PROXY;
|
||||||
|
keep a temporary ref so that _deactivate() doesn't crash in case the
|
||||||
|
pw-proxy-destroyed signal causes external references to be dropped */
|
||||||
|
g_autoptr (WpGlobalProxy) proxy =
|
||||||
|
g_object_ref (g_steal_pointer (&global->proxy));
|
||||||
|
|
||||||
|
/* notify all listeners that the proxy is gone */
|
||||||
|
if (reg)
|
||||||
|
wp_registry_notify_rm_object (reg, proxy);
|
||||||
|
|
||||||
|
/* remove FEATURE_BOUND to destroy the underlying pw_proxy */
|
||||||
|
wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);
|
||||||
|
|
||||||
|
/* stop all in-progress activations */
|
||||||
|
wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");
|
||||||
|
|
||||||
|
/* if the proxy is not owning the global, unref it */
|
||||||
|
if (global->flags == 0)
|
||||||
|
g_object_unref (proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* It's possible to receive consecutive {add, remove, add} events for the
|
||||||
|
* same id. Since the WpGlobal might not be destroyed immediately below,
|
||||||
|
* (e.g. it's in tmp_globals list), we must invalidate the id now, so that
|
||||||
|
* this WpGlobal is not used in reference to objects added later.
|
||||||
|
*/
|
||||||
|
global->id = SPA_ID_INVALID;
|
||||||
|
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* drop the registry's ref on global when it does not appear on the registry anymore */
|
||||||
|
if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
|
||||||
|
g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct pw_proxy *
|
||||||
|
wp_global_bind (WpGlobal * global)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail (global->proxy, NULL);
|
||||||
|
g_return_val_if_fail (global->registry, NULL);
|
||||||
|
|
||||||
|
WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
|
||||||
|
return pw_registry_bind (global->registry->pw_registry, global->id,
|
||||||
|
klass->pw_iface_type, klass->pw_iface_version, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,22 +72,6 @@ WP_API
|
||||||
gpointer wp_object_manager_lookup_full (WpObjectManager * self,
|
gpointer wp_object_manager_lookup_full (WpObjectManager * self,
|
||||||
WpObjectInterest * interest);
|
WpObjectInterest * interest);
|
||||||
|
|
||||||
/* private */
|
|
||||||
|
|
||||||
typedef struct _WpGlobal WpGlobal;
|
|
||||||
|
|
||||||
WP_PRIVATE_API
|
|
||||||
void wp_object_manager_maybe_objects_changed (WpObjectManager * self);
|
|
||||||
|
|
||||||
WP_PRIVATE_API
|
|
||||||
void wp_object_manager_add_object (WpObjectManager * self, gpointer object);
|
|
||||||
|
|
||||||
WP_PRIVATE_API
|
|
||||||
void wp_object_manager_rm_object (WpObjectManager * self, gpointer object);
|
|
||||||
|
|
||||||
WP_PRIVATE_API
|
|
||||||
void wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -441,8 +441,8 @@ on_transition_completed (WpTransition * transition, GParamSpec * param,
|
||||||
* \param self the object
|
* \param self the object
|
||||||
* \param features the features to enable
|
* \param features the features to enable
|
||||||
* \param cancellable (nullable): a cancellable for the async operation
|
* \param cancellable (nullable): a cancellable for the async operation
|
||||||
* \param callback (scope async)(closure user_data): a function to call when activation is complete
|
* \param callback (scope async): a function to call when activation is complete
|
||||||
* \param user_data data for \a callback
|
* \param user_data (closure): data for \a callback
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
wp_object_activate (WpObject * self,
|
wp_object_activate (WpObject * self,
|
||||||
|
|
|
||||||
|
|
@ -1,707 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2026 Collabora Ltd.
|
|
||||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <pipewire/permission.h>
|
|
||||||
#include <pipewire/pipewire.h>
|
|
||||||
|
|
||||||
#include "private/permission-manager.h"
|
|
||||||
#include "permission-manager.h"
|
|
||||||
#include "proxy-interfaces.h"
|
|
||||||
#include "object-manager.h"
|
|
||||||
#include "json-utils.h"
|
|
||||||
#include "error.h"
|
|
||||||
#include "core.h"
|
|
||||||
#include "log.h"
|
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-permission-manager")
|
|
||||||
|
|
||||||
/*! \defgroup wppermissionmanager WpPermissionManager */
|
|
||||||
/*!
|
|
||||||
* \struct WpPermissionManager
|
|
||||||
*
|
|
||||||
* The WpPermissionManager class is in charge of updating automatically
|
|
||||||
* permissions on interested objects every time they are added or removed for
|
|
||||||
* a particular client.
|
|
||||||
*
|
|
||||||
* WpPermissionManager API.
|
|
||||||
*/
|
|
||||||
|
|
||||||
typedef struct _PermissionMatch PermissionMatch;
|
|
||||||
struct _PermissionMatch
|
|
||||||
{
|
|
||||||
guint32 id;
|
|
||||||
guint32 permissions;
|
|
||||||
GClosure *closure;
|
|
||||||
WpObjectInterest *interest;
|
|
||||||
WpSpaJson *rules;
|
|
||||||
};
|
|
||||||
|
|
||||||
static guint
|
|
||||||
get_next_id ()
|
|
||||||
{
|
|
||||||
static guint32 next_id = 0;
|
|
||||||
g_atomic_int_inc (&next_id);
|
|
||||||
return next_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PermissionMatch *
|
|
||||||
permission_match_new (guint32 perms, GClosure *closure,
|
|
||||||
WpObjectInterest * interest, WpSpaJson * rules)
|
|
||||||
{
|
|
||||||
PermissionMatch *match = g_new0 (PermissionMatch, 1);
|
|
||||||
match->id = get_next_id ();
|
|
||||||
match->permissions = perms;
|
|
||||||
match->closure = closure ? g_closure_ref (closure) : NULL;
|
|
||||||
match->interest = interest ? wp_object_interest_ref (interest) : NULL;
|
|
||||||
match->rules = rules ? wp_spa_json_ref (rules) : NULL;
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
permission_interest_free (PermissionMatch *self)
|
|
||||||
{
|
|
||||||
g_clear_pointer (&self->closure, g_closure_unref);
|
|
||||||
g_clear_pointer (&self->interest, wp_object_interest_unref);
|
|
||||||
g_clear_pointer (&self->rules, wp_spa_json_unref);
|
|
||||||
g_free (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct _WpPermissionManager
|
|
||||||
{
|
|
||||||
WpObject parent;
|
|
||||||
|
|
||||||
guint32 default_perms;
|
|
||||||
guint32 core_perms;
|
|
||||||
GPtrArray *clients;
|
|
||||||
GHashTable *matches;
|
|
||||||
|
|
||||||
WpObjectManager *om;
|
|
||||||
};
|
|
||||||
|
|
||||||
G_DEFINE_TYPE (WpPermissionManager, wp_permission_manager, WP_TYPE_OBJECT)
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_permission_manager_init (WpPermissionManager * self)
|
|
||||||
{
|
|
||||||
/* Init default permissions to all */
|
|
||||||
self->default_perms = PW_PERM_R | PW_PERM_W | PW_PERM_X;
|
|
||||||
|
|
||||||
/* Core permissions not set by default (inherit from default_perms) */
|
|
||||||
self->core_perms = PW_PERM_INVALID;
|
|
||||||
|
|
||||||
/* Init permission interests table */
|
|
||||||
self->matches = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
|
||||||
(GDestroyNotify)permission_interest_free);
|
|
||||||
|
|
||||||
/* Init clients list */
|
|
||||||
self->clients = g_ptr_array_new_with_free_func (
|
|
||||||
(GDestroyNotify) g_object_unref);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum {
|
|
||||||
STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
|
|
||||||
};
|
|
||||||
|
|
||||||
static WpObjectFeatures
|
|
||||||
wp_permission_manager_get_supported_features (WpObject * self)
|
|
||||||
{
|
|
||||||
return WP_PERMISSION_MANAGER_LOADED;
|
|
||||||
}
|
|
||||||
|
|
||||||
static guint
|
|
||||||
wp_permission_manager_activate_get_next_step (WpObject * self,
|
|
||||||
WpFeatureActivationTransition * transition, guint step,
|
|
||||||
WpObjectFeatures missing)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (missing == WP_PERMISSION_MANAGER_LOADED,
|
|
||||||
WP_TRANSITION_STEP_ERROR);
|
|
||||||
|
|
||||||
return STEP_LOAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
static guint32
|
|
||||||
invoke_permissions_closure (WpPermissionManager *self, WpClient *client,
|
|
||||||
WpGlobalProxy *object, GClosure *closure)
|
|
||||||
{
|
|
||||||
GValue args[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };
|
|
||||||
GValue ret = G_VALUE_INIT;
|
|
||||||
guint32 perms;
|
|
||||||
|
|
||||||
g_value_init (&args[0], WP_TYPE_PERMISSION_MANAGER);
|
|
||||||
g_value_set_object (&args[0], self);
|
|
||||||
g_value_init (&args[1], WP_TYPE_CLIENT);
|
|
||||||
g_value_set_object (&args[1], client);
|
|
||||||
g_value_init (&args[2], WP_TYPE_GLOBAL_PROXY);
|
|
||||||
g_value_set_object (&args[2], object);
|
|
||||||
g_value_init (&ret, G_TYPE_UINT);
|
|
||||||
|
|
||||||
g_closure_invoke (closure, &ret, 3, args, NULL);
|
|
||||||
perms = g_value_get_uint (&ret);
|
|
||||||
|
|
||||||
g_value_unset (&args[0]);
|
|
||||||
g_value_unset (&args[1]);
|
|
||||||
g_value_unset (&args[2]);
|
|
||||||
g_value_unset (&ret);
|
|
||||||
|
|
||||||
return perms;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct _MatchRulesCallbackData MatchRulesCallbackData;
|
|
||||||
struct _MatchRulesCallbackData {
|
|
||||||
gboolean matched;
|
|
||||||
guint32 perms;
|
|
||||||
};
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
match_rules_cb (gpointer data, const gchar * action, WpSpaJson * value,
|
|
||||||
GError ** e)
|
|
||||||
{
|
|
||||||
MatchRulesCallbackData *cb_data = (MatchRulesCallbackData *)data;
|
|
||||||
g_autofree gchar *perms_str = NULL;
|
|
||||||
guint32 perms = 0;
|
|
||||||
|
|
||||||
if (!g_str_equal (action, "set-permissions")) {
|
|
||||||
if (e)
|
|
||||||
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"Action name '%s' is not valid", action);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wp_spa_json_is_string (value)) {
|
|
||||||
if (e)
|
|
||||||
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"Action '%s' must be a string", action);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parse permissions */
|
|
||||||
perms_str = wp_spa_json_parse_string (value);
|
|
||||||
if (g_strcmp0 (perms_str, "all") == 0) {
|
|
||||||
perms = PW_PERM_ALL;
|
|
||||||
} else if (perms_str) {
|
|
||||||
for (guint i = 0; i < strlen (perms_str); i++) {
|
|
||||||
switch (perms_str[i]) {
|
|
||||||
case 'r': perms |= PW_PERM_R; break;
|
|
||||||
case 'w': perms |= PW_PERM_W; break;
|
|
||||||
case 'x': perms |= PW_PERM_X; break;
|
|
||||||
case 'm': perms |= PW_PERM_M; break;
|
|
||||||
case 'l': perms |= PW_PERM_L; break;
|
|
||||||
case '-': break;
|
|
||||||
default: {
|
|
||||||
if (e)
|
|
||||||
g_set_error (e, WP_DOMAIN_LIBRARY,
|
|
||||||
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"Permissions '%s' are not valid", perms_str);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cb_data) {
|
|
||||||
cb_data->matched = TRUE;
|
|
||||||
cb_data->perms |= perms;
|
|
||||||
}
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
get_rules_matched_object_permissions (WpPermissionManager *self,
|
|
||||||
WpSpaJson *rules, WpGlobalProxy *object, guint32 *perms)
|
|
||||||
{
|
|
||||||
g_autoptr (GError) e = NULL;
|
|
||||||
g_autoptr (WpProperties) gp_props = NULL;
|
|
||||||
g_autoptr (WpProperties) po_props = NULL;
|
|
||||||
MatchRulesCallbackData data = { FALSE, 0 };
|
|
||||||
|
|
||||||
/* Check global proxy properties */
|
|
||||||
gp_props = wp_global_proxy_get_global_properties (object);
|
|
||||||
if (gp_props && !wp_json_utils_match_rules (rules, gp_props, match_rules_cb,
|
|
||||||
&data, &e))
|
|
||||||
goto error;
|
|
||||||
|
|
||||||
/* Also check pipewire object properties if it is a pipewire object */
|
|
||||||
if (WP_IS_PIPEWIRE_OBJECT (object)) {
|
|
||||||
po_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object));
|
|
||||||
if (po_props && !wp_json_utils_match_rules (rules, po_props, match_rules_cb,
|
|
||||||
&data, &e))
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set permissions if there was a match */
|
|
||||||
if (data.matched && perms)
|
|
||||||
*perms = data.perms;
|
|
||||||
|
|
||||||
return data.matched;
|
|
||||||
|
|
||||||
error:
|
|
||||||
wp_warning_object (self, "Malformed JSON match rules: %s", e->message);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
get_matched_object_permissions (WpPermissionManager *self, PermissionMatch *m,
|
|
||||||
WpClient *client, WpGlobalProxy *object, guint32 *perms)
|
|
||||||
{
|
|
||||||
/* Check interest */
|
|
||||||
if (m->interest && wp_object_interest_matches (m->interest, object)) {
|
|
||||||
if (!perms)
|
|
||||||
return TRUE;
|
|
||||||
*perms = m->closure ? invoke_permissions_closure (self, client, object,
|
|
||||||
m->closure) : m->permissions;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Check rules */
|
|
||||||
if (m->rules)
|
|
||||||
return get_rules_matched_object_permissions (self, m->rules, object, perms);
|
|
||||||
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static GArray *
|
|
||||||
build_permissions_array (WpPermissionManager *self, WpClient *client)
|
|
||||||
{
|
|
||||||
g_autoptr (WpIterator) it = NULL;
|
|
||||||
g_auto (GValue) value = G_VALUE_INIT;
|
|
||||||
struct pw_permission def_perm = { PW_ID_ANY, self->default_perms };
|
|
||||||
GArray *arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));
|
|
||||||
|
|
||||||
/* Add default permissions */
|
|
||||||
g_array_append_val (arr, def_perm);
|
|
||||||
|
|
||||||
/* Add core permissions if explicitly set (core is not in the OM since it is
|
|
||||||
* implicit in the PipeWire connection and not sent through the registry) */
|
|
||||||
if (self->core_perms != PW_PERM_INVALID) {
|
|
||||||
struct pw_permission core_perm = { PW_ID_CORE, self->core_perms };
|
|
||||||
g_array_append_val (arr, core_perm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add object specific permissions in the array */
|
|
||||||
it = wp_object_manager_new_iterator (self->om);
|
|
||||||
for (; wp_iterator_next (it, &value); g_value_unset (&value)) {
|
|
||||||
WpGlobalProxy *object = g_value_get_object (&value);
|
|
||||||
GHashTableIter iter;
|
|
||||||
PermissionMatch *match = NULL;
|
|
||||||
g_hash_table_iter_init (&iter, self->matches);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&match)) {
|
|
||||||
guint32 perms = PW_PERM_INVALID;
|
|
||||||
if (get_matched_object_permissions (self, match, client, object, &perms)
|
|
||||||
&& perms != PW_PERM_INVALID) {
|
|
||||||
struct pw_permission obj_perm = { 0, };
|
|
||||||
obj_perm.id = wp_proxy_get_bound_id (WP_PROXY (object));
|
|
||||||
obj_perm.permissions = perms;
|
|
||||||
g_array_append_val (arr, obj_perm);;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Merge permissions with same object ID */
|
|
||||||
for (guint i = 0; i < arr->len; i++) {
|
|
||||||
for (guint j = i + 1; j < arr->len; ) {
|
|
||||||
struct pw_permission *a = &g_array_index (arr, struct pw_permission, i);
|
|
||||||
struct pw_permission *b = &g_array_index (arr, struct pw_permission, j);
|
|
||||||
if (a->id == b->id) {
|
|
||||||
a->permissions |= b->permissions;
|
|
||||||
g_array_remove_index (arr, j);
|
|
||||||
} else {
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
update_client_permissions (WpPermissionManager *self, WpClient *client)
|
|
||||||
{
|
|
||||||
guint32 bound_id = 0;
|
|
||||||
g_autoptr (GArray) perms = NULL;
|
|
||||||
|
|
||||||
/* Dont do anything if the permission manager is not activated */
|
|
||||||
if (!(wp_object_get_active_features (WP_OBJECT (self)) &
|
|
||||||
WP_PERMISSION_MANAGER_LOADED))
|
|
||||||
return;
|
|
||||||
|
|
||||||
/* Make sure the client proxy is still valid */
|
|
||||||
if (!wp_proxy_get_pw_proxy (WP_PROXY (client)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
bound_id = wp_proxy_get_bound_id (WP_PROXY (client));
|
|
||||||
perms = build_permissions_array (self, client);
|
|
||||||
|
|
||||||
wp_info_object (self,
|
|
||||||
"Updating permissions on client %u: any=%c%c%c%c%c len=%u",
|
|
||||||
bound_id,
|
|
||||||
!!(self->default_perms & PW_PERM_R) ? 'r' : '-',
|
|
||||||
!!(self->default_perms & PW_PERM_W) ? 'w' : '-',
|
|
||||||
!!(self->default_perms & PW_PERM_X) ? 'x' : '-',
|
|
||||||
!!(self->default_perms & PW_PERM_M) ? 'm' : '-',
|
|
||||||
!!(self->default_perms & PW_PERM_L) ? 'l' : '-',
|
|
||||||
perms->len);
|
|
||||||
|
|
||||||
wp_client_update_permissions_array (client, perms->len,
|
|
||||||
(const struct pw_permission *) perms->data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
has_object_match (WpPermissionManager *self, WpGlobalProxy *object)
|
|
||||||
{
|
|
||||||
GHashTableIter iter;
|
|
||||||
PermissionMatch *m = NULL;
|
|
||||||
|
|
||||||
g_hash_table_iter_init (&iter, self->matches);
|
|
||||||
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&m)) {
|
|
||||||
if (m->interest && wp_object_interest_matches (m->interest, object))
|
|
||||||
return TRUE;
|
|
||||||
if (m->rules && get_rules_matched_object_permissions (self, m->rules,
|
|
||||||
object, NULL))
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
update_permissions (WpPermissionManager *self)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < self->clients->len; i++) {
|
|
||||||
WpClient *client = g_ptr_array_index (self->clients, i);
|
|
||||||
update_client_permissions (self, client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
on_object_added_or_removed (WpObjectManager *om, WpGlobalProxy *object,
|
|
||||||
gpointer d)
|
|
||||||
{
|
|
||||||
WpPermissionManager * self = WP_PERMISSION_MANAGER (d);
|
|
||||||
|
|
||||||
if (has_object_match (self, object))
|
|
||||||
update_permissions (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
on_object_manager_installed (WpObjectManager *om, gpointer d)
|
|
||||||
{
|
|
||||||
WpTransition * transition = WP_TRANSITION (d);
|
|
||||||
WpPermissionManager * self = wp_transition_get_source_object (transition);
|
|
||||||
|
|
||||||
wp_object_update_features (WP_OBJECT (self), WP_PERMISSION_MANAGER_LOADED, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_permission_manager_activate_execute_step (WpObject * object,
|
|
||||||
WpFeatureActivationTransition * transition, guint step,
|
|
||||||
WpObjectFeatures missing)
|
|
||||||
{
|
|
||||||
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
|
|
||||||
g_autoptr (WpCore) core = wp_object_get_core (object);
|
|
||||||
|
|
||||||
switch (step) {
|
|
||||||
case STEP_LOAD: {
|
|
||||||
/* Install object manager */
|
|
||||||
g_clear_object (&self->om);
|
|
||||||
self->om = wp_object_manager_new ();
|
|
||||||
wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
|
|
||||||
wp_object_manager_request_object_features (self->om,
|
|
||||||
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
|
|
||||||
g_signal_connect_object (self->om, "object-added",
|
|
||||||
G_CALLBACK (on_object_added_or_removed), self, 0);
|
|
||||||
g_signal_connect_object (self->om, "object-removed",
|
|
||||||
G_CALLBACK (on_object_added_or_removed), self, 0);
|
|
||||||
g_signal_connect_object (self->om, "installed",
|
|
||||||
G_CALLBACK (on_object_manager_installed), transition, 0);
|
|
||||||
wp_core_install_object_manager (core, self->om);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case WP_TRANSITION_STEP_ERROR:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
g_assert_not_reached ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_permission_manager_deactivate (WpObject * object, WpObjectFeatures features)
|
|
||||||
{
|
|
||||||
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
|
|
||||||
|
|
||||||
g_clear_object (&self->om);
|
|
||||||
|
|
||||||
wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_permission_manager_finalize (GObject * object)
|
|
||||||
{
|
|
||||||
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
|
|
||||||
|
|
||||||
g_clear_pointer (&self->clients, g_ptr_array_unref);
|
|
||||||
g_clear_pointer (&self->matches, g_hash_table_unref);
|
|
||||||
|
|
||||||
g_clear_object (&self->om);
|
|
||||||
|
|
||||||
G_OBJECT_CLASS (wp_permission_manager_parent_class)->finalize (object);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_permission_manager_class_init (WpPermissionManagerClass * klass)
|
|
||||||
{
|
|
||||||
GObjectClass * object_class = (GObjectClass *) klass;
|
|
||||||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
|
||||||
|
|
||||||
object_class->finalize = wp_permission_manager_finalize;
|
|
||||||
|
|
||||||
wpobject_class->get_supported_features =
|
|
||||||
wp_permission_manager_get_supported_features;
|
|
||||||
wpobject_class->activate_get_next_step =
|
|
||||||
wp_permission_manager_activate_get_next_step;
|
|
||||||
wpobject_class->activate_execute_step =
|
|
||||||
wp_permission_manager_activate_execute_step;
|
|
||||||
wpobject_class->deactivate = wp_permission_manager_deactivate;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_permission_manager_add_client (WpPermissionManager *self, WpClient *client)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
|
||||||
|
|
||||||
g_ptr_array_add (self->clients, g_object_ref (client));
|
|
||||||
update_client_permissions (self, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_permission_manager_remove_client (WpPermissionManager *self,
|
|
||||||
WpClient *client)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
|
||||||
|
|
||||||
g_ptr_array_remove_fast (self->clients, client);
|
|
||||||
update_client_permissions (self, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Creates a new WpPermissionManager object
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param core the WpCore
|
|
||||||
* \returns (transfer full): a new WpPermissionManager object
|
|
||||||
*/
|
|
||||||
WpPermissionManager *
|
|
||||||
wp_permission_manager_new (WpCore * core)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (core, NULL);
|
|
||||||
|
|
||||||
return g_object_new (WP_TYPE_PERMISSION_MANAGER, "core", core, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Sets the default permissions that will be applied to all objects that
|
|
||||||
* don't match any interest
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param permissions the default permissions to apply
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_permission_manager_set_default_permissions (WpPermissionManager *self,
|
|
||||||
guint32 permissions)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
|
||||||
|
|
||||||
if (self->default_perms != permissions) {
|
|
||||||
self->default_perms = permissions;
|
|
||||||
update_permissions (self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Sets the permissions that will be applied to the core object (ID 0).
|
|
||||||
*
|
|
||||||
* The core object is not visible to the permission manager's object manager
|
|
||||||
* because it is implicit in the PipeWire connection and not sent through the
|
|
||||||
* registry. This method allows setting explicit permissions on it, independent
|
|
||||||
* of the default permissions.
|
|
||||||
*
|
|
||||||
* If not set (or set to PW_PERM_INVALID), the core inherits default_permissions.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param permissions the permissions to apply to the core object
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_permission_manager_set_core_permissions (WpPermissionManager *self,
|
|
||||||
guint32 permissions)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
|
||||||
|
|
||||||
if (self->core_perms != permissions) {
|
|
||||||
self->core_perms = permissions;
|
|
||||||
update_permissions (self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static guint32
|
|
||||||
wp_permission_manager_add_match (WpPermissionManager *self,
|
|
||||||
PermissionMatch *match)
|
|
||||||
{
|
|
||||||
guint id = match->id;
|
|
||||||
g_hash_table_insert (self->matches, GUINT_TO_POINTER (id), match);
|
|
||||||
update_permissions (self);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Adds an interest match to apply permissions with callback in matched
|
|
||||||
* objects.
|
|
||||||
*
|
|
||||||
* Interest consists of a GType that the object must be an ancestor of
|
|
||||||
* (g_type_is_a() must match) and optionally, a set of additional constraints
|
|
||||||
* on certain properties of the object. Refer to WpObjectInterest for more details.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param callback (scope async): the permissions match callback
|
|
||||||
* \param user_data data to pass to \a callback
|
|
||||||
* \param interest (transfer full): the interest
|
|
||||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
|
||||||
*/
|
|
||||||
guint32
|
|
||||||
wp_permission_manager_add_interest_match (WpPermissionManager *self,
|
|
||||||
WpPermissionMatchCallback callback, gpointer user_data,
|
|
||||||
WpObjectInterest * interest)
|
|
||||||
{
|
|
||||||
GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);
|
|
||||||
return wp_permission_manager_add_interest_match_closure (self, closure,
|
|
||||||
interest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Adds an interest match to apply permissions with closure in matched
|
|
||||||
* objects.
|
|
||||||
*
|
|
||||||
* Interest consists of a GType that the object must be an ancestor of
|
|
||||||
* (g_type_is_a() must match) and optionally, a set of additional constraints
|
|
||||||
* on certain properties of the object. Refer to WpObjectInterest for more details.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param closure (transfer full): the closure to apply permissions
|
|
||||||
* \param interest (transfer full): the interest
|
|
||||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
|
||||||
*/
|
|
||||||
guint32
|
|
||||||
wp_permission_manager_add_interest_match_closure (WpPermissionManager *self,
|
|
||||||
GClosure *closure, WpObjectInterest * interest)
|
|
||||||
{
|
|
||||||
g_autoptr (WpObjectInterest) i = interest;
|
|
||||||
g_autoptr (GClosure) c = closure;
|
|
||||||
PermissionMatch *match;
|
|
||||||
|
|
||||||
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
|
|
||||||
g_return_val_if_fail (closure, SPA_ID_INVALID);
|
|
||||||
g_return_val_if_fail (i, SPA_ID_INVALID);
|
|
||||||
|
|
||||||
if (G_CLOSURE_NEEDS_MARSHAL (closure))
|
|
||||||
g_closure_set_marshal (closure, g_cclosure_marshal_generic);
|
|
||||||
|
|
||||||
match = permission_match_new (PW_PERM_INVALID, c, i, NULL);
|
|
||||||
return wp_permission_manager_add_match (self, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Adds an interest match to apply same permissions in matched objects.
|
|
||||||
*
|
|
||||||
* Interest consists of a GType that the object must be an ancestor of
|
|
||||||
* (g_type_is_a() must match) and optionally, a set of additional constraints
|
|
||||||
* on certain properties of the object. Refer to WpObjectInterest for more details.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param permissions the permissions to apply
|
|
||||||
* \param interest (transfer full): the interest
|
|
||||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
|
||||||
*/
|
|
||||||
guint32
|
|
||||||
wp_permission_manager_add_interest_match_simple (WpPermissionManager *self,
|
|
||||||
guint32 permissions, WpObjectInterest * interest)
|
|
||||||
{
|
|
||||||
g_autoptr (WpObjectInterest) i = interest;
|
|
||||||
PermissionMatch *match;
|
|
||||||
|
|
||||||
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
|
|
||||||
g_return_val_if_fail (i, SPA_ID_INVALID);
|
|
||||||
|
|
||||||
match = permission_match_new (permissions, NULL, i, NULL);
|
|
||||||
return wp_permission_manager_add_match (self, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Adds a rules match to apply permissions in matched objects.
|
|
||||||
*
|
|
||||||
* The rules must be defined in a JSON object using the same format as all
|
|
||||||
* the wireplumber/pipewire rules.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param rules (transfer full): the JSON rules
|
|
||||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
|
||||||
*/
|
|
||||||
guint32
|
|
||||||
wp_permission_manager_add_rules_match (WpPermissionManager *self,
|
|
||||||
WpSpaJson *rules)
|
|
||||||
{
|
|
||||||
g_autoptr (WpSpaJson) r = rules;
|
|
||||||
PermissionMatch *match;
|
|
||||||
|
|
||||||
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
|
|
||||||
g_return_val_if_fail (r, SPA_ID_INVALID);
|
|
||||||
|
|
||||||
match = permission_match_new (PW_PERM_INVALID, NULL, NULL, rules);
|
|
||||||
return wp_permission_manager_add_match (self, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Removes the previously added match so that the associated permissions
|
|
||||||
* are not applied anymore.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param match_id the match ID to remove
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_permission_manager_remove_match (WpPermissionManager *self, guint32 match_id)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
|
||||||
g_return_if_fail (match_id != SPA_ID_INVALID);
|
|
||||||
|
|
||||||
g_hash_table_remove (self->matches, GUINT_TO_POINTER (match_id));
|
|
||||||
update_permissions (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Updates permissions on all clients the permission manager has.
|
|
||||||
*
|
|
||||||
* The permission manager already updates permissions on all clients
|
|
||||||
* automatically when a new client or object is added, however, this might be
|
|
||||||
* needed if interests with closures or callbacks were added and something
|
|
||||||
* changed externally.
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_permission_manager_update_permissions (WpPermissionManager *self)
|
|
||||||
{
|
|
||||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
|
||||||
|
|
||||||
update_permissions (self);
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2026 Collabora Ltd.
|
|
||||||
* @author Julian Bouzas <julian.bouzas@ollabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __WIREPLUMBER_PERMISSION_MANAGER_H__
|
|
||||||
#define __WIREPLUMBER_PERMISSION_MANAGER_H__
|
|
||||||
|
|
||||||
#include "object-interest.h"
|
|
||||||
#include "global-proxy.h"
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Flags to be used as WpObjectFeatures for WpPermissionManager.
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
*/
|
|
||||||
typedef enum { /*< flags >*/
|
|
||||||
/*! Loads the permission manager */
|
|
||||||
WP_PERMISSION_MANAGER_LOADED = (1 << 0),
|
|
||||||
} WpPermissionManagerFeatures;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief The WpPermissionManager GType
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
*/
|
|
||||||
#define WP_TYPE_PERMISSION_MANAGER (wp_permission_manager_get_type ())
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
G_DECLARE_FINAL_TYPE (WpPermissionManager, wp_permission_manager, WP,
|
|
||||||
PERMISSION_MANAGER, WpObject)
|
|
||||||
|
|
||||||
typedef struct _WpClient WpClient;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief callback to set permissions on the matched global object
|
|
||||||
*
|
|
||||||
* \ingroup wppermissionmanager
|
|
||||||
* \param self the permission manager
|
|
||||||
* \param client the client that will have its permissions updated
|
|
||||||
* \param object the matched global
|
|
||||||
* \param user_data the passed data
|
|
||||||
*/
|
|
||||||
typedef guint32 (*WpPermissionMatchCallback) (WpPermissionManager *self,
|
|
||||||
WpClient *client, WpGlobalProxy *object, gpointer user_data);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpPermissionManager * wp_permission_manager_new (WpCore * core);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_permission_manager_set_default_permissions (
|
|
||||||
WpPermissionManager *self, guint32 permissions);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_permission_manager_set_core_permissions (
|
|
||||||
WpPermissionManager *self, guint32 permissions);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
guint32 wp_permission_manager_add_interest_match (WpPermissionManager *self,
|
|
||||||
WpPermissionMatchCallback callback, gpointer user_data,
|
|
||||||
WpObjectInterest * interest);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
guint32 wp_permission_manager_add_interest_match_closure (
|
|
||||||
WpPermissionManager *self, GClosure *closure, WpObjectInterest * interest);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
guint32 wp_permission_manager_add_interest_match_simple (
|
|
||||||
WpPermissionManager *self, guint32 permissions,
|
|
||||||
WpObjectInterest * interest);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
guint32 wp_permission_manager_add_rules_match (WpPermissionManager *self,
|
|
||||||
WpSpaJson *rules);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_permission_manager_remove_match (WpPermissionManager *self,
|
|
||||||
guint32 match_id);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_permission_manager_update_permissions (WpPermissionManager *self);
|
|
||||||
|
|
||||||
G_END_DECLS
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -198,7 +198,7 @@ wp_plugin_find (WpCore * core, const gchar * plugin_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Retrieves the name of a plugin.
|
* \brief Retreives the name of a plugin.
|
||||||
*
|
*
|
||||||
* \ingroup wpplugin
|
* \ingroup wpplugin
|
||||||
* \param self the plugin
|
* \param self the plugin
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ struct _ComponentData
|
||||||
grefcount ref;
|
grefcount ref;
|
||||||
/* an identifier for this component that is understandable by the end user */
|
/* an identifier for this component that is understandable by the end user */
|
||||||
gchar *printable_id;
|
gchar *printable_id;
|
||||||
/* the provided feature name */
|
/* the provided feature name (points to same storage as the id) or NULL */
|
||||||
gchar *provides;
|
gchar *provides;
|
||||||
/* the original state of the feature (required / optional / disabled) */
|
/* the original state of the feature (required / optional / disabled) */
|
||||||
FeatureState state;
|
FeatureState state;
|
||||||
|
|
@ -39,8 +39,6 @@ struct _ComponentData
|
||||||
WpSpaJson *arguments;
|
WpSpaJson *arguments;
|
||||||
GPtrArray *requires; /* value-type: string (owned) */
|
GPtrArray *requires; /* value-type: string (owned) */
|
||||||
GPtrArray *wants; /* value-type: string (owned) */
|
GPtrArray *wants; /* value-type: string (owned) */
|
||||||
GPtrArray *before; /* value-type: string (owned) */
|
|
||||||
GPtrArray *after; /* value-type: string (owned) */
|
|
||||||
|
|
||||||
/* TRUE when the component is in the final sorted list */
|
/* TRUE when the component is in the final sorted list */
|
||||||
gboolean visited;
|
gboolean visited;
|
||||||
|
|
@ -176,8 +174,6 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
|
||||||
g_ref_count_init (&comp->ref);
|
g_ref_count_init (&comp->ref);
|
||||||
comp->requires = g_ptr_array_new_with_free_func (g_free);
|
comp->requires = g_ptr_array_new_with_free_func (g_free);
|
||||||
comp->wants = g_ptr_array_new_with_free_func (g_free);
|
comp->wants = g_ptr_array_new_with_free_func (g_free);
|
||||||
comp->before = g_ptr_array_new_with_free_func (g_free);
|
|
||||||
comp->after = g_ptr_array_new_with_free_func (g_free);
|
|
||||||
|
|
||||||
props = wp_properties_new_json (json);
|
props = wp_properties_new_json (json);
|
||||||
if (rules && !wp_json_utils_match_rules (rules, props, component_rule_match_cb,
|
if (rules && !wp_json_utils_match_rules (rules, props, component_rule_match_cb,
|
||||||
|
|
@ -205,7 +201,7 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
|
||||||
comp->printable_id = g_strdup_printf ("%s [%s]", comp->provides, comp->type);
|
comp->printable_id = g_strdup_printf ("%s [%s]", comp->provides, comp->type);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
comp->provides = g_strdup_printf ("__anonymous_%p", comp);
|
comp->provides = NULL;
|
||||||
comp->state = FEATURE_STATE_REQUIRED;
|
comp->state = FEATURE_STATE_REQUIRED;
|
||||||
comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
|
comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
|
||||||
}
|
}
|
||||||
|
|
@ -232,28 +228,6 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((str = wp_properties_get (props, "before"))) {
|
|
||||||
g_autoptr (WpSpaJson) comp_before = wp_spa_json_new_wrap_string (str);
|
|
||||||
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_before);
|
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
|
||||||
|
|
||||||
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
||||||
WpSpaJson *dep = g_value_get_boxed (&item);
|
|
||||||
g_ptr_array_add (comp->before, wp_spa_json_to_string (dep));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((str = wp_properties_get (props, "after"))) {
|
|
||||||
g_autoptr (WpSpaJson) comp_after = wp_spa_json_new_wrap_string (str);
|
|
||||||
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_after);
|
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
|
||||||
|
|
||||||
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
||||||
WpSpaJson *dep = g_value_get_boxed (&item);
|
|
||||||
g_ptr_array_add (comp->after, wp_spa_json_to_string (dep));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return g_steal_pointer (&comp);
|
return g_steal_pointer (&comp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,8 +241,6 @@ component_data_free (ComponentData * self)
|
||||||
g_clear_pointer (&self->arguments, wp_spa_json_unref);
|
g_clear_pointer (&self->arguments, wp_spa_json_unref);
|
||||||
g_clear_pointer (&self->requires, g_ptr_array_unref);
|
g_clear_pointer (&self->requires, g_ptr_array_unref);
|
||||||
g_clear_pointer (&self->wants, g_ptr_array_unref);
|
g_clear_pointer (&self->wants, g_ptr_array_unref);
|
||||||
g_clear_pointer (&self->before, g_ptr_array_unref);
|
|
||||||
g_clear_pointer (&self->after, g_ptr_array_unref);
|
|
||||||
g_free (self);
|
g_free (self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,100 +297,6 @@ wp_component_array_load_task_get_next_step (WpTransition * transition, guint ste
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
component_equals (const ComponentData * comp, const gchar * provides)
|
|
||||||
{
|
|
||||||
return g_str_equal (provides, comp->provides);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline gboolean
|
|
||||||
component_exists_in (const gchar *comp_provides, GPtrArray *list)
|
|
||||||
{
|
|
||||||
return g_ptr_array_find_with_equal_func (list, comp_provides,
|
|
||||||
(GEqualFunc) component_equals, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
sort_components_before_after (WpComponentArrayLoadTask * self, GError ** error)
|
|
||||||
{
|
|
||||||
g_autoptr (GPtrArray) remaining = g_ptr_array_new_with_free_func (
|
|
||||||
(GDestroyNotify) component_data_unref);
|
|
||||||
g_autoptr (GPtrArray) result = g_ptr_array_new_with_free_func (
|
|
||||||
(GDestroyNotify) component_data_unref);
|
|
||||||
|
|
||||||
for (guint i = 0; i < self->components->len; i++) {
|
|
||||||
ComponentData *comp = g_ptr_array_index (self->components, i);
|
|
||||||
|
|
||||||
/* implicitly add all "requires" and "wants" as "after" dependencies */
|
|
||||||
g_ptr_array_extend (comp->after, comp->requires, (GCopyFunc) g_strdup, NULL);
|
|
||||||
g_ptr_array_extend (comp->after, comp->wants, (GCopyFunc) g_strdup, NULL);
|
|
||||||
|
|
||||||
/* convert "before" dependencies into "after" dependencies */
|
|
||||||
for (guint j = 0; j < comp->before->len; j++) {
|
|
||||||
gchar *target_provides = g_ptr_array_index (comp->before, j);
|
|
||||||
for (guint k = 0; k < self->components->len; k++) {
|
|
||||||
ComponentData *target = g_ptr_array_index (self->components, k);
|
|
||||||
if (g_str_equal (target_provides, target->provides)) {
|
|
||||||
g_ptr_array_insert (target->after, -1, g_strdup (comp->provides));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sort */
|
|
||||||
while (self->components->len > 0) {
|
|
||||||
gboolean made_progress = FALSE;
|
|
||||||
|
|
||||||
/* examine each component to see if its dependencies are satisfied in the
|
|
||||||
result list; if yes, then append it to the result too */
|
|
||||||
while (self->components->len > 0) {
|
|
||||||
ComponentData *comp = g_ptr_array_steal_index (self->components, 0);
|
|
||||||
guint deps_satisfied = 0;
|
|
||||||
|
|
||||||
wp_trace_object (self, "examining: %s", comp->printable_id);
|
|
||||||
|
|
||||||
for (guint i = 0; i < comp->after->len; i++) {
|
|
||||||
const gchar *dep = g_ptr_array_index (comp->after, i);
|
|
||||||
/* if the dependency is already in the sorted result list or if
|
|
||||||
it doesn't exist at all, we consider it satisfied */
|
|
||||||
if (component_exists_in (dep, result) ||
|
|
||||||
!(component_exists_in (dep, self->components) ||
|
|
||||||
component_exists_in (dep, remaining))) {
|
|
||||||
deps_satisfied++;
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_trace_object (self, "depends: %s, satisfied: %u/%u",
|
|
||||||
dep, deps_satisfied, comp->after->len);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deps_satisfied == comp->after->len) {
|
|
||||||
wp_trace_object (self, "sorted: %s", comp->printable_id);
|
|
||||||
|
|
||||||
g_ptr_array_add (result, comp);
|
|
||||||
made_progress = TRUE;
|
|
||||||
} else {
|
|
||||||
g_ptr_array_add (remaining, comp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (made_progress) {
|
|
||||||
/* run again with the remaining components */
|
|
||||||
g_ptr_array_extend_and_steal (self->components, g_ptr_array_ref (remaining));
|
|
||||||
}
|
|
||||||
else if (remaining->len > 0) {
|
|
||||||
/* if we did not make any progress towards growing the result list,
|
|
||||||
it means the dependencies cannot be satisfied because of circles */
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"detected circular before/after dependencies in the components!");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* transfer the result array back to self->components */
|
|
||||||
g_ptr_array_extend_and_steal (self->components, g_steal_pointer (&result));
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gchar *
|
static gchar *
|
||||||
print_dep_chain (ComponentData *comp)
|
print_dep_chain (ComponentData *comp)
|
||||||
{
|
{
|
||||||
|
|
@ -531,8 +409,9 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
|
||||||
if (comp->state == FEATURE_STATE_REQUIRED)
|
if (comp->state == FEATURE_STATE_REQUIRED)
|
||||||
g_ptr_array_add (required_components, component_data_ref (comp));
|
g_ptr_array_add (required_components, component_data_ref (comp));
|
||||||
|
|
||||||
g_hash_table_insert (self->feat_components, comp->provides,
|
if (comp->provides)
|
||||||
component_data_ref (comp));
|
g_hash_table_insert (self->feat_components, comp->provides,
|
||||||
|
component_data_ref (comp));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* topological sorting based on depth-first search */
|
/* topological sorting based on depth-first search */
|
||||||
|
|
@ -545,10 +424,6 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sort again, taking into account before/after dependencies */
|
|
||||||
if (!sort_components_before_after (self, error))
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
/* terminate the array with NULL */
|
/* terminate the array with NULL */
|
||||||
g_ptr_array_add (self->components, NULL);
|
g_ptr_array_add (self->components, NULL);
|
||||||
|
|
||||||
|
|
@ -723,7 +598,7 @@ ensure_no_media_session_task_idle (GTask * task)
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
ensure_no_media_session (GTask * task, WpCore * core, WpSpaJson * args)
|
ensure_no_media_session (GTask * task, WpCore * core)
|
||||||
{
|
{
|
||||||
WpObjectManager *om = wp_object_manager_new ();
|
WpObjectManager *om = wp_object_manager_new ();
|
||||||
|
|
||||||
|
|
@ -744,7 +619,7 @@ ensure_no_media_session (GTask * task, WpCore * core, WpSpaJson * args)
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
load_export_core (GTask * task, WpCore * core, WpSpaJson * args)
|
load_export_core (GTask * task, WpCore * core)
|
||||||
{
|
{
|
||||||
g_autofree gchar *export_core_name = NULL;
|
g_autofree gchar *export_core_name = NULL;
|
||||||
g_autoptr (WpCore) export_core = NULL;
|
g_autoptr (WpCore) export_core = NULL;
|
||||||
|
|
@ -766,27 +641,12 @@ load_export_core (GTask * task, WpCore * core, WpSpaJson * args)
|
||||||
g_task_return_pointer (task, g_steal_pointer (&export_core), g_object_unref);
|
g_task_return_pointer (task, g_steal_pointer (&export_core), g_object_unref);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
load_settings_instance (GTask * task, WpCore * core, WpSpaJson * args)
|
|
||||||
{
|
|
||||||
g_autofree gchar *metadata_name = NULL;
|
|
||||||
if (args)
|
|
||||||
wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL);
|
|
||||||
|
|
||||||
wp_info_object (core, "loading settings instance '%s'...",
|
|
||||||
metadata_name ? metadata_name : "(default: sm-settings)");
|
|
||||||
|
|
||||||
WpSettings *settings = wp_settings_new (core, metadata_name);
|
|
||||||
g_task_return_pointer (task, settings, g_object_unref);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct {
|
static const struct {
|
||||||
const gchar * name;
|
const gchar * name;
|
||||||
void (*load) (GTask *, WpCore *, WpSpaJson *);
|
void (*load) (GTask *, WpCore *);
|
||||||
} builtin_components[] = {
|
} builtin_components[] = {
|
||||||
{ "ensure-no-media-session", ensure_no_media_session },
|
{ "ensure-no-media-session", ensure_no_media_session },
|
||||||
{ "export-core", load_export_core },
|
{ "export-core", load_export_core },
|
||||||
{ "settings-instance", load_settings_instance },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*** WpInternalCompLoader ***/
|
/*** WpInternalCompLoader ***/
|
||||||
|
|
@ -823,12 +683,10 @@ load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
|
||||||
GModule *gmodule;
|
GModule *gmodule;
|
||||||
gpointer module_init;
|
gpointer module_init;
|
||||||
|
|
||||||
module_path = wp_base_dirs_find_file (WP_BASE_DIRS_MODULE, NULL, module_name);
|
if (!g_file_test (module_name, G_FILE_TEST_EXISTS))
|
||||||
if (!module_path) {
|
module_path = g_module_build_path (wp_get_module_dir (), module_name);
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
else
|
||||||
"Failed to locate module %s", module_name);
|
module_path = g_strdup (module_name);
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_trace_object (core, "loading %s from %s", module_name, module_path);
|
wp_trace_object (core, "loading %s from %s", module_name, module_path);
|
||||||
|
|
||||||
|
|
@ -850,65 +708,6 @@ load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
|
||||||
return ((WpModuleInitFunc) module_init) (core, args, error);
|
return ((WpModuleInitFunc) module_init) (core, args, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
parse_profile_description (WpProperties * profile, WpSpaJson * all_profiles_j,
|
|
||||||
const gchar * profile_name, GPtrArray * inherited_set, GError ** error)
|
|
||||||
{
|
|
||||||
g_autoptr (WpSpaJson) profile_j = NULL;
|
|
||||||
g_autoptr (WpSpaJson) inherits_j = NULL;
|
|
||||||
|
|
||||||
if (!all_profiles_j) {
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"wireplumber.profiles section does not exist in the configuration");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wp_spa_json_is_object (all_profiles_j)) {
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"wireplumber.profiles section is not an object");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wp_spa_json_object_get (all_profiles_j, profile_name, "J", &profile_j, NULL)) {
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"profile '%s' not found in the configuration", profile_name);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wp_spa_json_is_object (profile_j)) {
|
|
||||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
||||||
"profile description of '%s' is not an object", profile_name);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* mark as inherited */
|
|
||||||
g_ptr_array_add (inherited_set, g_strdup (profile_name));
|
|
||||||
|
|
||||||
if (wp_spa_json_object_get (profile_j, "inherits", "J", &inherits_j, NULL) &&
|
|
||||||
wp_spa_json_is_array (inherits_j)) {
|
|
||||||
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (inherits_j);
|
|
||||||
g_auto (GValue) item = G_VALUE_INIT;
|
|
||||||
|
|
||||||
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
||||||
WpSpaJson *inherited_j = g_value_get_boxed (&item);
|
|
||||||
g_autofree gchar *inherited_profile = wp_spa_json_to_string (inherited_j);
|
|
||||||
|
|
||||||
/* skip if already inherited - avoid loops */
|
|
||||||
if (g_ptr_array_find_with_equal_func (inherited_set, inherited_profile,
|
|
||||||
g_str_equal, NULL))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!parse_profile_description (profile, all_profiles_j, inherited_profile,
|
|
||||||
inherited_set, error))
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_properties_update_from_json (profile, profile_j);
|
|
||||||
wp_properties_set (profile, "inherits", NULL);
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
wp_internal_comp_loader_supports_type (WpComponentLoader * cl,
|
wp_internal_comp_loader_supports_type (WpComponentLoader * cl,
|
||||||
const gchar * type)
|
const gchar * type)
|
||||||
|
|
@ -935,26 +734,25 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
|
||||||
if (g_str_equal (type, "profile")) {
|
if (g_str_equal (type, "profile")) {
|
||||||
/* component name is the profile name;
|
/* component name is the profile name;
|
||||||
component list and profile features are loaded from config */
|
component list and profile features are loaded from config */
|
||||||
g_autoptr (WpConf) conf = wp_core_get_conf (core);
|
g_autoptr (WpConf) conf = wp_conf_get_instance (core);
|
||||||
g_autoptr (GPtrArray) inherited_set = g_ptr_array_new_with_free_func (g_free);
|
g_autoptr (WpSpaJson) profile_json = NULL;
|
||||||
g_autoptr (WpSpaJson) all_profiles_j = NULL;
|
|
||||||
g_autoptr (GError) error = NULL;
|
|
||||||
const gchar *profile_name = component;
|
|
||||||
|
|
||||||
wp_info ("Loading profile '%s'", profile_name);
|
profile_json =
|
||||||
|
wp_conf_get_value (conf, "wireplumber.profiles", component, NULL);
|
||||||
all_profiles_j = wp_conf_get_section (conf, "wireplumber.profiles");
|
if (!profile_json) {
|
||||||
|
|
||||||
if (!parse_profile_description (profile, all_profiles_j, profile_name,
|
|
||||||
inherited_set, &error)) {
|
|
||||||
g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
|
g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
|
||||||
g_task_set_source_tag (task, wp_internal_comp_loader_load);
|
g_task_set_source_tag (task, wp_internal_comp_loader_load);
|
||||||
g_task_return_error (G_TASK (task), g_steal_pointer (&error));
|
g_task_return_new_error (G_TASK (task), WP_DOMAIN_LIBRARY,
|
||||||
|
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||||
|
"profile '%s' not found in configuration", component);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
components = wp_conf_get_section (conf, "wireplumber.components");
|
wp_properties_update_from_json (profile, profile_json);
|
||||||
rules = wp_conf_get_section (conf, "wireplumber.components.rules");
|
|
||||||
|
components = wp_conf_get_section (conf, "wireplumber.components", NULL);
|
||||||
|
|
||||||
|
rules = wp_conf_get_section (conf, "wireplumber.components.rules", NULL);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
/* component list is retrieved from args; profile features are empty */
|
/* component list is retrieved from args; profile features are empty */
|
||||||
|
|
@ -998,7 +796,7 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
|
||||||
else if (g_str_equal (type, "built-in")) {
|
else if (g_str_equal (type, "built-in")) {
|
||||||
for (guint i = 0; i < G_N_ELEMENTS (builtin_components); i++) {
|
for (guint i = 0; i < G_N_ELEMENTS (builtin_components); i++) {
|
||||||
if (g_str_equal (component, builtin_components[i].name)) {
|
if (g_str_equal (component, builtin_components[i].name)) {
|
||||||
builtin_components[i].load (task, core, args);
|
builtin_components[i].load (task, core);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
/* PipeWire */
|
|
||||||
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
|
|
||||||
/* SPDX-License-Identifier: MIT */
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is a partial copy of functions from libpipewire's conf.c that is meant to
|
|
||||||
live here temporarily until pw_context_parse_conf_section() is fixed upstream.
|
|
||||||
See https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1925
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <spa/utils/result.h>
|
|
||||||
#include <spa/utils/string.h>
|
|
||||||
#include <spa/utils/json.h>
|
|
||||||
#include <spa/utils/cleanup.h>
|
|
||||||
|
|
||||||
#include <pipewire/impl.h>
|
|
||||||
|
|
||||||
struct data {
|
|
||||||
struct pw_context *context;
|
|
||||||
struct pw_properties *props;
|
|
||||||
int count;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* context.spa-libs = {
|
|
||||||
* <factory-name regex> = <library-name>
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
static int parse_spa_libs(void *user_data, const char *location,
|
|
||||||
const char *section, const char *str, size_t len)
|
|
||||||
{
|
|
||||||
struct data *d = user_data;
|
|
||||||
struct pw_context *context = d->context;
|
|
||||||
struct spa_json it[2];
|
|
||||||
char key[512], value[512];
|
|
||||||
|
|
||||||
spa_json_init(&it[0], str, len);
|
|
||||||
if (spa_json_enter_object(&it[0], &it[1]) < 0) {
|
|
||||||
pw_log_error("config file error: context.spa-libs is not an object");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
|
|
||||||
if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
|
|
||||||
pw_context_add_spa_lib(context, key, value);
|
|
||||||
d->count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static int load_module(struct pw_context *context, const char *key, const char *args, const char *flags)
|
|
||||||
{
|
|
||||||
if (pw_context_load_module(context, key, args, NULL) == NULL) {
|
|
||||||
if (errno == ENOENT && flags && strstr(flags, "ifexists") != NULL) {
|
|
||||||
pw_log_info("%p: skipping unavailable module %s",
|
|
||||||
context, key);
|
|
||||||
} else if (flags == NULL || strstr(flags, "nofail") == NULL) {
|
|
||||||
pw_log_error("%p: could not load mandatory module \"%s\": %m",
|
|
||||||
context, key);
|
|
||||||
return -errno;
|
|
||||||
} else {
|
|
||||||
pw_log_info("%p: could not load optional module \"%s\": %m",
|
|
||||||
context, key);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pw_log_info("%p: loaded module %s", context, key);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* context.modules = [
|
|
||||||
* { name = <module-name>
|
|
||||||
* ( args = { <key> = <value> ... } )
|
|
||||||
* ( flags = [ ( ifexists ) ( nofail ) ]
|
|
||||||
* ( condition = [ { key = value, .. } .. ] )
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
*/
|
|
||||||
static int parse_modules(void *user_data, const char *location,
|
|
||||||
const char *section, const char *str, size_t len)
|
|
||||||
{
|
|
||||||
struct data *d = user_data;
|
|
||||||
struct pw_context *context = d->context;
|
|
||||||
struct spa_json it[4];
|
|
||||||
char key[512];
|
|
||||||
int res = 0;
|
|
||||||
|
|
||||||
spa_autofree char *s = strndup(str, len);
|
|
||||||
spa_json_init(&it[0], s, len);
|
|
||||||
if (spa_json_enter_array(&it[0], &it[1]) < 0) {
|
|
||||||
pw_log_error("config file error: context.modules is not an array");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (spa_json_enter_object(&it[1], &it[2]) > 0) {
|
|
||||||
char *name = NULL, *args = NULL, *flags = NULL;
|
|
||||||
bool have_match = true;
|
|
||||||
|
|
||||||
while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
|
|
||||||
const char *val;
|
|
||||||
int len;
|
|
||||||
|
|
||||||
if ((len = spa_json_next(&it[2], &val)) <= 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (spa_streq(key, "name")) {
|
|
||||||
name = (char*)val;
|
|
||||||
spa_json_parse_stringn(val, len, name, len+1);
|
|
||||||
} else if (spa_streq(key, "args")) {
|
|
||||||
if (spa_json_is_container(val, len))
|
|
||||||
len = spa_json_container_len(&it[2], val, len);
|
|
||||||
|
|
||||||
args = (char*)val;
|
|
||||||
spa_json_parse_stringn(val, len, args, len+1);
|
|
||||||
} else if (spa_streq(key, "flags")) {
|
|
||||||
if (spa_json_is_container(val, len))
|
|
||||||
len = spa_json_container_len(&it[2], val, len);
|
|
||||||
flags = (char*)val;
|
|
||||||
spa_json_parse_stringn(val, len, flags, len+1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!have_match)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (name != NULL)
|
|
||||||
res = load_module(context, name, args, flags);
|
|
||||||
|
|
||||||
if (res < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
d->count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int _pw_context_parse_conf_section(struct pw_context *context,
|
|
||||||
struct pw_properties *conf, const char *section)
|
|
||||||
{
|
|
||||||
struct data data = { .context = context };
|
|
||||||
int res;
|
|
||||||
|
|
||||||
if (spa_streq(section, "context.spa-libs"))
|
|
||||||
res = pw_conf_section_for_each(&conf->dict, section,
|
|
||||||
parse_spa_libs, &data);
|
|
||||||
else if (spa_streq(section, "context.modules"))
|
|
||||||
res = pw_conf_section_for_each(&conf->dict, section,
|
|
||||||
parse_modules, &data);
|
|
||||||
else
|
|
||||||
res = -EINVAL;
|
|
||||||
|
|
||||||
return res == 0 ? data.count : res;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2026 Collabora Ltd.
|
|
||||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
|
|
||||||
#define __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
|
|
||||||
|
|
||||||
#include "client.h"
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
|
||||||
|
|
||||||
typedef struct _WpPermissionManager WpPermissionManager;
|
|
||||||
|
|
||||||
void wp_permission_manager_add_client (WpPermissionManager *self,
|
|
||||||
WpClient *client);
|
|
||||||
|
|
||||||
void wp_permission_manager_remove_client (WpPermissionManager *self,
|
|
||||||
WpClient *client);
|
|
||||||
|
|
||||||
G_END_DECLS
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -783,7 +783,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
|
||||||
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
|
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
|
||||||
|
|
||||||
g_clear_pointer (&d->properties, wp_properties_unref);
|
g_clear_pointer (&d->properties, wp_properties_unref);
|
||||||
d->properties = wp_properties_new_copy_dict (props);
|
d->properties = wp_properties_new_wrap_dict (props);
|
||||||
|
|
||||||
g_object_notify (G_OBJECT (instance), "properties");
|
g_object_notify (G_OBJECT (instance), "properties");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,515 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2019-2024 Collabora Ltd.
|
|
||||||
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "registry.h"
|
|
||||||
#include "object-manager.h"
|
|
||||||
#include "log.h"
|
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-registry")
|
|
||||||
|
|
||||||
/*
|
|
||||||
* WpRegistry:
|
|
||||||
*
|
|
||||||
* The registry keeps track of registered objects on the wireplumber core.
|
|
||||||
* There are 3 kinds of registered objects:
|
|
||||||
*
|
|
||||||
* 1) PipeWire global objects, which live in another process.
|
|
||||||
*
|
|
||||||
* These objects are represented by a WpGlobal with the
|
|
||||||
* WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
|
|
||||||
* the registry_global() event is fired and are removed by
|
|
||||||
* registry_global_remove(). These objects do not have an associated
|
|
||||||
* WpProxy, unless there is at least one WpObjectManager that is interested
|
|
||||||
* in them. In this case, a WpProxy is constructed and it is owned by the
|
|
||||||
* WpGlobal until the global is removed by the registry_global_remove() event.
|
|
||||||
*
|
|
||||||
* 2) PipeWire global objects, which were constructed by this process, either
|
|
||||||
* by calling into a remove factory (see wp_node_new_from_factory()) or
|
|
||||||
* by exporting a local object (WpImplSession etc...).
|
|
||||||
*
|
|
||||||
* These objects are also represented by a WpGlobal, which may however be
|
|
||||||
* constructed before they appear on the registry. The associated WpProxy
|
|
||||||
* calls into wp_registry_prepare_new_global() at the time it receives
|
|
||||||
* the 'bound' event and creates a global that has the
|
|
||||||
* WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
|
|
||||||
* these globals are "owned" by the WpProxy and the WpGlobal has no ref
|
|
||||||
* on the WpProxy itself. This allows destroying the proxy in client code
|
|
||||||
* by dropping its last reference.
|
|
||||||
*
|
|
||||||
* Normally, these global objects also appear on the pipewire registry. When
|
|
||||||
* this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
|
|
||||||
* and that keeps an additional reference on the global (both flags must
|
|
||||||
* be dropped before the WpGlobal is destroyed).
|
|
||||||
*
|
|
||||||
* In some cases, such an object might appear first on the registry and
|
|
||||||
* then receive the 'bound' event. In order to handle this situation, globals
|
|
||||||
* are not advertised immediately when they appear on the registry, but
|
|
||||||
* they are added on a tmp_globals list instead, which is emptied on the
|
|
||||||
* next core sync. In all cases, the proxy 'bound' and the registry 'global'
|
|
||||||
* events will be fired in the same sync cycle, so we can catch a late
|
|
||||||
* 'bound' event and still associate the proxy with the WpGlobal before
|
|
||||||
* object managers are notified about the existence of this global.
|
|
||||||
*
|
|
||||||
* 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
|
|
||||||
*
|
|
||||||
* These are local objects that have nothing to do with PipeWire. They do not
|
|
||||||
* have a global id and they are also not subclasses of WpProxy. The registry
|
|
||||||
* always owns a reference on them, so that they are kept alive for as long
|
|
||||||
* as the WpCore is alive.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void
|
|
||||||
object_manager_destroyed (gpointer data, GObject * om)
|
|
||||||
{
|
|
||||||
WpRegistry *self = data;
|
|
||||||
g_ptr_array_remove_fast (self->object_managers, om);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* find the subclass of WpPipewireGloabl that can handle
|
|
||||||
the given pipewire interface type of the given version */
|
|
||||||
static inline GType
|
|
||||||
find_proxy_instance_type (const char * type, guint32 version)
|
|
||||||
{
|
|
||||||
g_autofree GType *children;
|
|
||||||
guint n_children;
|
|
||||||
|
|
||||||
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
|
|
||||||
|
|
||||||
for (guint i = 0; i < n_children; i++) {
|
|
||||||
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
|
|
||||||
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
|
|
||||||
klass->pw_iface_version == version) {
|
|
||||||
g_type_class_unref (klass);
|
|
||||||
return children[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
g_type_class_unref (klass);
|
|
||||||
}
|
|
||||||
|
|
||||||
return WP_TYPE_GLOBAL_PROXY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* called by the registry when a global appears */
|
|
||||||
static void
|
|
||||||
registry_global (void *data, uint32_t id, uint32_t permissions,
|
|
||||||
const char *type, uint32_t version, const struct spa_dict *props)
|
|
||||||
{
|
|
||||||
WpRegistry *self = data;
|
|
||||||
GType gtype = find_proxy_instance_type (type, version);
|
|
||||||
|
|
||||||
wp_debug_object (wp_registry_get_core (self),
|
|
||||||
"global:%u perm:0x%x type:%s/%u -> %s",
|
|
||||||
id, permissions, type, version, g_type_name (gtype));
|
|
||||||
|
|
||||||
wp_registry_prepare_new_global (self, id, permissions,
|
|
||||||
WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* called by the registry when a global is removed */
|
|
||||||
static void
|
|
||||||
registry_global_remove (void *data, uint32_t id)
|
|
||||||
{
|
|
||||||
WpRegistry *self = data;
|
|
||||||
WpGlobal *global = NULL;
|
|
||||||
|
|
||||||
if (id < self->globals->len)
|
|
||||||
global = g_ptr_array_index (self->globals, id);
|
|
||||||
|
|
||||||
/* if not found, look in the tmp_globals, as it may still not be exposed */
|
|
||||||
if (!global) {
|
|
||||||
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
|
||||||
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
|
||||||
if (g->id == id) {
|
|
||||||
global = g;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g_return_if_fail (global &&
|
|
||||||
global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
|
||||||
|
|
||||||
wp_debug_object (wp_registry_get_core (self),
|
|
||||||
"global removed:%u type:%s", id, g_type_name (global->type));
|
|
||||||
|
|
||||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct pw_registry_events registry_events = {
|
|
||||||
PW_VERSION_REGISTRY_EVENTS,
|
|
||||||
.global = registry_global,
|
|
||||||
.global_remove = registry_global_remove,
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_init (WpRegistry *self)
|
|
||||||
{
|
|
||||||
self->globals =
|
|
||||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
|
||||||
self->tmp_globals =
|
|
||||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
|
||||||
self->objects = g_ptr_array_new_with_free_func (g_object_unref);
|
|
||||||
self->object_managers = g_ptr_array_new ();
|
|
||||||
self->features = g_ptr_array_new_with_free_func (g_free);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_clear (WpRegistry *self)
|
|
||||||
{
|
|
||||||
wp_registry_detach (self);
|
|
||||||
g_clear_pointer (&self->globals, g_ptr_array_unref);
|
|
||||||
g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
|
|
||||||
g_clear_pointer (&self->features, g_ptr_array_unref);
|
|
||||||
|
|
||||||
/* remove all the registered objects
|
|
||||||
this will normally also destroy the object managers, eventually, since
|
|
||||||
they are normally ref'ed by modules, which are registered objects */
|
|
||||||
{
|
|
||||||
g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);
|
|
||||||
|
|
||||||
while (objlist->len > 0) {
|
|
||||||
g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
|
|
||||||
objlist->len - 1);
|
|
||||||
wp_registry_notify_rm_object (self, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* in case there are any object managers left,
|
|
||||||
remove the weak ref on them and let them be... */
|
|
||||||
{
|
|
||||||
g_autoptr (GPtrArray) object_mgrs;
|
|
||||||
GObject *om;
|
|
||||||
|
|
||||||
object_mgrs = g_steal_pointer (&self->object_managers);
|
|
||||||
|
|
||||||
while (object_mgrs->len > 0) {
|
|
||||||
om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
|
|
||||||
g_object_weak_unref (om, object_manager_destroyed, self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
|
|
||||||
{
|
|
||||||
self->pw_registry = pw_core_get_registry (pw_core,
|
|
||||||
PW_VERSION_REGISTRY, 0);
|
|
||||||
pw_registry_add_listener (self->pw_registry, &self->listener,
|
|
||||||
®istry_events, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_detach (WpRegistry *self)
|
|
||||||
{
|
|
||||||
if (self->pw_registry) {
|
|
||||||
spa_hook_remove (&self->listener);
|
|
||||||
pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
|
|
||||||
self->pw_registry = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* remove pipewire globals */
|
|
||||||
GPtrArray *objlist = self->globals;
|
|
||||||
while (objlist && objlist->len > 0) {
|
|
||||||
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
|
||||||
objlist->len - 1);
|
|
||||||
|
|
||||||
if (!global)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (global->proxy)
|
|
||||||
wp_registry_notify_rm_object (self, global->proxy);
|
|
||||||
|
|
||||||
/* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
|
|
||||||
by the registry; set registry to NULL to avoid further interference */
|
|
||||||
global->registry = NULL;
|
|
||||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
|
||||||
|
|
||||||
/* the registry's ref on global is dropped here; it may still live if
|
|
||||||
there is a proxy that owns a ref on it, but global->registry is set
|
|
||||||
to NULL, so there is no further interference */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* drop tmp globals as well */
|
|
||||||
objlist = self->tmp_globals;
|
|
||||||
while (objlist && objlist->len > 0) {
|
|
||||||
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
|
||||||
objlist->len - 1);
|
|
||||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
expose_tmp_globals (WpCore *core)
|
|
||||||
{
|
|
||||||
WpRegistry *self = wp_core_get_registry (core);
|
|
||||||
g_autoptr (GPtrArray) tmp_globals = NULL;
|
|
||||||
g_autoptr (GPtrArray) object_managers = NULL;
|
|
||||||
|
|
||||||
/* in case the registry was cleared in the meantime... */
|
|
||||||
if (G_UNLIKELY (!self->tmp_globals))
|
|
||||||
return G_SOURCE_REMOVE;
|
|
||||||
|
|
||||||
/* steal the tmp_globals list and replace it with an empty one */
|
|
||||||
tmp_globals = self->tmp_globals;
|
|
||||||
self->tmp_globals =
|
|
||||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
|
||||||
|
|
||||||
wp_debug_object (core, "exposing %u new globals", tmp_globals->len);
|
|
||||||
|
|
||||||
/* traverse in the order that the globals appeared on the registry */
|
|
||||||
for (guint i = 0; i < tmp_globals->len; i++) {
|
|
||||||
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
|
||||||
|
|
||||||
/* if global was already removed, drop it */
|
|
||||||
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/* if old global is owned by proxy, remove it */
|
|
||||||
if (self->globals->len > g->id) {
|
|
||||||
WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
|
|
||||||
if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
|
|
||||||
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
|
||||||
}
|
|
||||||
|
|
||||||
g_return_val_if_fail (self->globals->len <= g->id ||
|
|
||||||
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
|
|
||||||
|
|
||||||
/* set the registry, so that wp_global_rm_flag() can work full-scale */
|
|
||||||
g->registry = self;
|
|
||||||
|
|
||||||
/* store it in the globals list */
|
|
||||||
if (self->globals->len <= g->id)
|
|
||||||
g_ptr_array_set_size (self->globals, g->id + 1);
|
|
||||||
g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
|
|
||||||
}
|
|
||||||
|
|
||||||
object_managers = g_ptr_array_copy (self->object_managers,
|
|
||||||
(GCopyFunc) g_object_ref, NULL);
|
|
||||||
g_ptr_array_set_free_func (object_managers, g_object_unref);
|
|
||||||
|
|
||||||
/* notify object managers */
|
|
||||||
for (guint i = 0; i < object_managers->len; i++) {
|
|
||||||
WpObjectManager *om = g_ptr_array_index (object_managers, i);
|
|
||||||
|
|
||||||
for (guint i = 0; i < tmp_globals->len; i++) {
|
|
||||||
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
|
||||||
|
|
||||||
/* if global was already removed, drop it */
|
|
||||||
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
wp_object_manager_add_global (om, g);
|
|
||||||
}
|
|
||||||
wp_object_manager_maybe_objects_changed (om);
|
|
||||||
}
|
|
||||||
|
|
||||||
return G_SOURCE_REMOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* \param new_global (out) (transfer full) (optional): the new global
|
|
||||||
*
|
|
||||||
* This is normally called up to 2 times in the same sync cycle:
|
|
||||||
* one from registry_global(), another from the proxy bound event
|
|
||||||
* Unfortunately the order in which those 2 events happen is specific
|
|
||||||
* to the implementation of the object, which is why this is implemented
|
|
||||||
* with a temporary globals list that get exposed later to the object managers
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
|
||||||
guint32 permissions, guint32 flag, GType type,
|
|
||||||
WpGlobalProxy *proxy, const struct spa_dict *props,
|
|
||||||
WpGlobal ** new_global)
|
|
||||||
{
|
|
||||||
g_autoptr (WpGlobal) global = NULL;
|
|
||||||
WpCore *core = wp_registry_get_core (self);
|
|
||||||
|
|
||||||
g_return_if_fail (flag != 0);
|
|
||||||
|
|
||||||
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
|
||||||
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
|
||||||
if (g->id == id) {
|
|
||||||
global = wp_global_ref (g);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
|
|
||||||
global ? "reuse" : "new", id, g_type_name (type),
|
|
||||||
(global && global->proxy) ? global->proxy : proxy);
|
|
||||||
|
|
||||||
if (!global) {
|
|
||||||
global = g_rc_box_new0 (WpGlobal);
|
|
||||||
global->flags = flag;
|
|
||||||
global->id = id;
|
|
||||||
global->type = type;
|
|
||||||
global->permissions = permissions;
|
|
||||||
global->properties = props ?
|
|
||||||
wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
|
|
||||||
global->proxy = proxy;
|
|
||||||
g_ptr_array_add (self->tmp_globals, wp_global_ref (global));
|
|
||||||
|
|
||||||
/* ensure we have 'object.id' so that we can filter by id on object managers */
|
|
||||||
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);
|
|
||||||
|
|
||||||
/* schedule exposing when adding the first global */
|
|
||||||
if (self->tmp_globals->len == 1) {
|
|
||||||
wp_core_idle_add_closure (core, NULL,
|
|
||||||
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/* store the most permissive permissions */
|
|
||||||
if (permissions > global->permissions)
|
|
||||||
global->permissions = permissions;
|
|
||||||
|
|
||||||
global->flags |= flag;
|
|
||||||
|
|
||||||
/* store the most deep type (i.e. WpImplNode instead of WpNode),
|
|
||||||
so that object-manager interests can work more accurately
|
|
||||||
if the interest is on a specific subclass */
|
|
||||||
if (g_type_depth (type) > g_type_depth (global->type))
|
|
||||||
global->type = type;
|
|
||||||
|
|
||||||
if (proxy) {
|
|
||||||
g_return_if_fail (global->proxy == NULL);
|
|
||||||
global->proxy = proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props)
|
|
||||||
wp_properties_update_from_dict (global->properties, props);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_global)
|
|
||||||
*new_global = g_steal_pointer (&global);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < self->object_managers->len; i++) {
|
|
||||||
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
|
||||||
wp_object_manager_add_object (om, object);
|
|
||||||
wp_object_manager_maybe_objects_changed (om);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
|
|
||||||
{
|
|
||||||
for (guint i = 0; i < self->object_managers->len; i++) {
|
|
||||||
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
|
||||||
wp_object_manager_rm_object (om, object);
|
|
||||||
wp_object_manager_maybe_objects_changed (om);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_registry_install_object_manager (WpRegistry * self, WpObjectManager * om)
|
|
||||||
{
|
|
||||||
guint i;
|
|
||||||
|
|
||||||
g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, self);
|
|
||||||
g_ptr_array_add (self->object_managers, om);
|
|
||||||
|
|
||||||
/* add pre-existing objects to the object manager,
|
|
||||||
in case it's interested in them */
|
|
||||||
for (i = 0; i < self->globals->len; i++) {
|
|
||||||
WpGlobal *g = g_ptr_array_index (self->globals, i);
|
|
||||||
/* check if null because the globals array can have gaps */
|
|
||||||
if (g)
|
|
||||||
wp_object_manager_add_global (om, g);
|
|
||||||
}
|
|
||||||
for (i = 0; i < self->objects->len; i++) {
|
|
||||||
GObject *o = g_ptr_array_index (self->objects, i);
|
|
||||||
wp_object_manager_add_object (om, o);
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_object_manager_maybe_objects_changed (om);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* WpGlobal */
|
|
||||||
|
|
||||||
G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)
|
|
||||||
|
|
||||||
void
|
|
||||||
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
|
|
||||||
{
|
|
||||||
WpRegistry *reg = global->registry;
|
|
||||||
guint32 id = global->id;
|
|
||||||
|
|
||||||
/* no flag to remove */
|
|
||||||
if (!(global->flags & rm_flag))
|
|
||||||
return;
|
|
||||||
|
|
||||||
wp_trace_boxed (WP_TYPE_GLOBAL, global,
|
|
||||||
"remove global %u flag 0x%x [flags:0x%x, reg:%p]",
|
|
||||||
id, rm_flag, global->flags, reg);
|
|
||||||
|
|
||||||
/* global was owned by the proxy; by removing the flag, we clear out
|
|
||||||
also the proxy pointer, which is presumably no longer valid and we
|
|
||||||
notify all listeners that the proxy is gone */
|
|
||||||
if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
|
|
||||||
global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
|
|
||||||
if (reg && global->proxy) {
|
|
||||||
wp_registry_notify_rm_object (reg, global->proxy);
|
|
||||||
}
|
|
||||||
global->proxy = NULL;
|
|
||||||
}
|
|
||||||
/* registry removed the global */
|
|
||||||
else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
|
|
||||||
global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;
|
|
||||||
|
|
||||||
/* destroy the proxy if it exists */
|
|
||||||
if (global->proxy) {
|
|
||||||
/* steal the proxy to avoid calling wp_registry_notify_rm_object()
|
|
||||||
again while removing OWNED_BY_PROXY;
|
|
||||||
keep a temporary ref so that _deactivate() doesn't crash in case the
|
|
||||||
pw-proxy-destroyed signal causes external references to be dropped */
|
|
||||||
g_autoptr (WpGlobalProxy) proxy =
|
|
||||||
g_object_ref (g_steal_pointer (&global->proxy));
|
|
||||||
|
|
||||||
/* notify all listeners that the proxy is gone */
|
|
||||||
if (reg)
|
|
||||||
wp_registry_notify_rm_object (reg, proxy);
|
|
||||||
|
|
||||||
/* remove FEATURE_BOUND to destroy the underlying pw_proxy */
|
|
||||||
wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);
|
|
||||||
|
|
||||||
/* stop all in-progress activations */
|
|
||||||
wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");
|
|
||||||
|
|
||||||
/* if the proxy is not owning the global, unref it */
|
|
||||||
if (global->flags == 0)
|
|
||||||
g_object_unref (proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* It's possible to receive consecutive {add, remove, add} events for the
|
|
||||||
* same id. Since the WpGlobal might not be destroyed immediately below,
|
|
||||||
* (e.g. it's in tmp_globals list), we must invalidate the id now, so that
|
|
||||||
* this WpGlobal is not used in reference to objects added later.
|
|
||||||
*/
|
|
||||||
global->id = SPA_ID_INVALID;
|
|
||||||
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* drop the registry's ref on global when it does not appear on the registry anymore */
|
|
||||||
if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
|
|
||||||
g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct pw_proxy *
|
|
||||||
wp_global_bind (WpGlobal * global)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (global->proxy, NULL);
|
|
||||||
g_return_val_if_fail (global->registry, NULL);
|
|
||||||
|
|
||||||
WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
|
|
||||||
return pw_registry_bind (global->registry->pw_registry, global->id,
|
|
||||||
klass->pw_iface_type, klass->pw_iface_version, 0);
|
|
||||||
}
|
|
||||||
|
|
@ -46,9 +46,6 @@ void wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
||||||
void wp_registry_notify_add_object (WpRegistry * self, gpointer object);
|
void wp_registry_notify_add_object (WpRegistry * self, gpointer object);
|
||||||
void wp_registry_notify_rm_object (WpRegistry * self, gpointer object);
|
void wp_registry_notify_rm_object (WpRegistry * self, gpointer object);
|
||||||
|
|
||||||
void wp_registry_install_object_manager (WpRegistry * self,
|
|
||||||
WpObjectManager * om);
|
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
wp_registry_mark_feature_provided (WpRegistry * reg, const gchar * feature)
|
wp_registry_mark_feature_provided (WpRegistry * reg, const gchar * feature)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2024 Collabora Ltd.
|
|
||||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <spa/utils/cleanup.h>
|
|
||||||
|
|
||||||
#include "log.h"
|
|
||||||
#include "proc-utils.h"
|
|
||||||
|
|
||||||
#define MAX_ARGS 1024
|
|
||||||
|
|
||||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-proc-utils")
|
|
||||||
|
|
||||||
/*! \defgroup wpprocutils Process Utilities */
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \struct WpProcInfo
|
|
||||||
*
|
|
||||||
* WpProcInfo holds information of a process.
|
|
||||||
*/
|
|
||||||
struct _WpProcInfo {
|
|
||||||
grefcount ref;
|
|
||||||
pid_t pid;
|
|
||||||
pid_t parent;
|
|
||||||
gchar *cgroup;
|
|
||||||
gchar *args[MAX_ARGS];
|
|
||||||
guint n_args;
|
|
||||||
};
|
|
||||||
|
|
||||||
G_DEFINE_BOXED_TYPE (WpProcInfo, wp_proc_info, wp_proc_info_ref,
|
|
||||||
wp_proc_info_unref)
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Increases the reference count of a process information object
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self a process information object
|
|
||||||
* \returns (transfer full): \a self with an additional reference count on it
|
|
||||||
*/
|
|
||||||
WpProcInfo *
|
|
||||||
wp_proc_info_ref (WpProcInfo * self)
|
|
||||||
{
|
|
||||||
g_ref_count_inc (&self->ref);
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wp_proc_info_free (WpProcInfo * self)
|
|
||||||
{
|
|
||||||
g_clear_pointer (&self->cgroup, g_free);
|
|
||||||
for (guint i = 0; i < MAX_ARGS; i++)
|
|
||||||
g_clear_pointer (&self->args[i], free);
|
|
||||||
g_slice_free (WpProcInfo, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Decreases the reference count on \a self and frees it when the ref
|
|
||||||
* count reaches zero.
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self (transfer full): a process information object
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
wp_proc_info_unref (WpProcInfo * self)
|
|
||||||
{
|
|
||||||
if (g_ref_count_dec (&self->ref))
|
|
||||||
wp_proc_info_free (self);
|
|
||||||
}
|
|
||||||
|
|
||||||
static WpProcInfo *
|
|
||||||
wp_proc_info_new (pid_t pid)
|
|
||||||
{
|
|
||||||
WpProcInfo *self = g_slice_new0 (WpProcInfo);
|
|
||||||
g_ref_count_init (&self->ref);
|
|
||||||
self->pid = pid;
|
|
||||||
self->parent = 0;
|
|
||||||
self->cgroup = NULL;
|
|
||||||
for (guint i = 0; i < MAX_ARGS; i++)
|
|
||||||
self->args[i] = NULL;
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the PID of a process information object
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self the process information object
|
|
||||||
* \returns the PID of the process information object
|
|
||||||
*/
|
|
||||||
pid_t
|
|
||||||
wp_proc_info_get_pid (WpProcInfo * self)
|
|
||||||
{
|
|
||||||
return self->pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the parent PID of a process information object
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self the process information object
|
|
||||||
* \returns the parent PID of the process information object
|
|
||||||
*/
|
|
||||||
pid_t
|
|
||||||
wp_proc_info_get_parent_pid (WpProcInfo * self)
|
|
||||||
{
|
|
||||||
return self->parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the number of args of a process information object
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self the process information object
|
|
||||||
* \returns the number of args of the process information object
|
|
||||||
*/
|
|
||||||
guint
|
|
||||||
wp_proc_info_get_n_args (WpProcInfo * self)
|
|
||||||
{
|
|
||||||
return self->n_args;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the indexed arg of a process information object
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self the process information object
|
|
||||||
* \param index the index of the arg
|
|
||||||
* \returns the indexed arg of the process information object
|
|
||||||
*/
|
|
||||||
const gchar *
|
|
||||||
wp_proc_info_get_arg (WpProcInfo * self, guint index)
|
|
||||||
{
|
|
||||||
if (index >= self->n_args)
|
|
||||||
return NULL;
|
|
||||||
return self->args[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets the systemd cgroup of a process information object
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
* \param self the process information object
|
|
||||||
* \returns the systemd cgroup of the process information object
|
|
||||||
*/
|
|
||||||
const gchar *
|
|
||||||
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
|
|
||||||
* \param pid the PID to get the process information from
|
|
||||||
* \returns: (transfer full): the process information of the given PID
|
|
||||||
*/
|
|
||||||
WpProcInfo *
|
|
||||||
wp_proc_utils_get_proc_info (pid_t pid)
|
|
||||||
{
|
|
||||||
WpProcInfo *ret = wp_proc_info_new (pid);
|
|
||||||
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 */
|
|
||||||
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 */
|
|
||||||
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 */
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/* WirePlumber
|
|
||||||
*
|
|
||||||
* Copyright © 2024 Collabora Ltd.
|
|
||||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __WIREPLUMBER_PROC_UTILS_H__
|
|
||||||
#define __WIREPLUMBER_PROC_UTILS_H__
|
|
||||||
|
|
||||||
#include <gio/gio.h>
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief The WpProcInfo GType
|
|
||||||
* \ingroup wpprocutils
|
|
||||||
*/
|
|
||||||
#define WP_TYPE_PROC_INFO (wp_proc_info_get_type ())
|
|
||||||
WP_API
|
|
||||||
GType wp_proc_info_get_type (void);
|
|
||||||
|
|
||||||
typedef struct _WpProcInfo WpProcInfo;
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpProcInfo *wp_proc_info_ref (WpProcInfo * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_proc_info_unref (WpProcInfo * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
pid_t wp_proc_info_get_pid (WpProcInfo * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
pid_t wp_proc_info_get_parent_pid (WpProcInfo * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
guint wp_proc_info_get_n_args (WpProcInfo * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar *wp_proc_info_get_arg (WpProcInfo * self, guint index);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar *wp_proc_info_get_cgroup (WpProcInfo * self);
|
|
||||||
|
|
||||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpProcInfo, wp_proc_info_unref)
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpProcInfo *wp_proc_utils_get_proc_info (pid_t pid);
|
|
||||||
|
|
||||||
G_END_DECLS
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -203,7 +203,7 @@ wp_properties_new_wrap (const struct pw_properties * props)
|
||||||
* allowing reading & writing properties on that \a props structure through
|
* allowing reading & writing properties on that \a props structure through
|
||||||
* the WpProperties API.
|
* the WpProperties API.
|
||||||
*
|
*
|
||||||
* In contrast with wp_properties_new_wrap(), this function assumes ownership
|
* In constrast with wp_properties_new_wrap(), this function assumes ownership
|
||||||
* of the \a props structure, so it will try to free \a props when it is destroyed.
|
* of the \a props structure, so it will try to free \a props when it is destroyed.
|
||||||
*
|
*
|
||||||
* \ingroup wpproperties
|
* \ingroup wpproperties
|
||||||
|
|
@ -1036,7 +1036,7 @@ wp_properties_unref_and_take_pw_properties (WpProperties * self)
|
||||||
* \ingroup wpproperties
|
* \ingroup wpproperties
|
||||||
* \param self a properties object
|
* \param self a properties object
|
||||||
* \param other a set of properties to match
|
* \param other a set of properties to match
|
||||||
* \returns TRUE if all matches were successful, FALSE if at least one
|
* \returns TRUE if all matches were successfull, FALSE if at least one
|
||||||
* property value did not match
|
* property value did not match
|
||||||
*/
|
*/
|
||||||
gboolean
|
gboolean
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ wp_pipewire_object_default_init (WpPipewireObjectInterface * iface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Retrieves the native info structure of this object
|
* \brief Retrieves the native infor structure of this object
|
||||||
* (pw_node_info, pw_port_info, etc...)
|
* (pw_node_info, pw_port_info, etc...)
|
||||||
*
|
*
|
||||||
* \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
|
* \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
|
||||||
|
|
@ -213,8 +213,8 @@ wp_pipewire_object_get_param_info (WpPipewireObject * self)
|
||||||
* \param id (nullable): the parameter id to enumerate or NULL for all parameters
|
* \param id (nullable): the parameter id to enumerate or NULL for all parameters
|
||||||
* \param filter (nullable): a param filter or NULL
|
* \param filter (nullable): a param filter or NULL
|
||||||
* \param cancellable (nullable): a cancellable for the async operation
|
* \param cancellable (nullable): a cancellable for the async operation
|
||||||
* \param callback (scope async)(closure user_data): a callback to call with the result
|
* \param callback (scope async): a callback to call with the result
|
||||||
* \param user_data data to pass to \a callback
|
* \param user_data (closure): data to pass to \a callback
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
wp_pipewire_object_enum_params (WpPipewireObject * self, const gchar * id,
|
wp_pipewire_object_enum_params (WpPipewireObject * self, const gchar * id,
|
||||||
|
|
|
||||||
|
|
@ -31,28 +31,30 @@ typedef enum { /*< flags >*/
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG = (1 << 8),
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG = (1 << 8),
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE = (1 << 9),
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE = (1 << 9),
|
||||||
|
|
||||||
/*!
|
|
||||||
* The minimal feature set for proxies implementing WpPipewireObject.
|
|
||||||
* This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
|
|
||||||
*/
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL =
|
|
||||||
(WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO),
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* The complete common feature set for proxies implementing
|
|
||||||
* WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
|
|
||||||
*/
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURES_ALL =
|
|
||||||
(WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL |
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS |
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT |
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE |
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG |
|
|
||||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE),
|
|
||||||
|
|
||||||
WP_PROXY_FEATURE_CUSTOM_START = (1 << 16), /*< skip >*/
|
WP_PROXY_FEATURE_CUSTOM_START = (1 << 16), /*< skip >*/
|
||||||
} WpProxyFeatures;
|
} WpProxyFeatures;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The minimal feature set for proxies implementing WpPipewireObject.
|
||||||
|
* This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
|
||||||
|
* \ingroup wpproxy
|
||||||
|
*/
|
||||||
|
#define WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL \
|
||||||
|
(WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO)
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The complete common feature set for proxies implementing
|
||||||
|
* WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
|
||||||
|
* \ingroup wpproxy
|
||||||
|
*/
|
||||||
|
#define WP_PIPEWIRE_OBJECT_FEATURES_ALL \
|
||||||
|
(WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | \
|
||||||
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS | \
|
||||||
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT | \
|
||||||
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE | \
|
||||||
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG | \
|
||||||
|
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE)
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief The WpProxy GType
|
* \brief The WpProxy GType
|
||||||
* \ingroup wpproxy
|
* \ingroup wpproxy
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ on_session_item_proxy_destroyed_deferred (WpSessionItem * item)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Helper callback for sub-classes that defers and unexports
|
* \brief Helper callback for sub-classes that deffers and unexports
|
||||||
* the session item.
|
* the session item.
|
||||||
*
|
*
|
||||||
* Only meant to be used when the pipewire proxy destroyed signal is triggered.
|
* Only meant to be used when the pipewire proxy destroyed signal is triggered.
|
||||||
|
|
|
||||||
1304
lib/wp/settings.c
1304
lib/wp/settings.c
File diff suppressed because it is too large
Load diff
|
|
@ -12,88 +12,8 @@
|
||||||
#include "object.h"
|
#include "object.h"
|
||||||
#include "spa-json.h"
|
#include "spa-json.h"
|
||||||
|
|
||||||
#define WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "schema-"
|
|
||||||
#define WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "persistent-"
|
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief The different spec types of a setting
|
|
||||||
* \ingroup wpsettings
|
|
||||||
*/
|
|
||||||
typedef enum {
|
|
||||||
WP_SETTINGS_SPEC_TYPE_UNKNOWN,
|
|
||||||
WP_SETTINGS_SPEC_TYPE_BOOL,
|
|
||||||
WP_SETTINGS_SPEC_TYPE_INT,
|
|
||||||
WP_SETTINGS_SPEC_TYPE_FLOAT,
|
|
||||||
WP_SETTINGS_SPEC_TYPE_STRING,
|
|
||||||
WP_SETTINGS_SPEC_TYPE_ARRAY,
|
|
||||||
WP_SETTINGS_SPEC_TYPE_OBJECT,
|
|
||||||
} WpSettingsSpecType;
|
|
||||||
|
|
||||||
typedef struct _WpSettingsSpec WpSettingsSpec;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief The WpSettingsSpec GType
|
|
||||||
* \ingroup wpsettings
|
|
||||||
*/
|
|
||||||
#define WP_TYPE_SETTINGS_SPEC (wp_settings_spec_get_type ())
|
|
||||||
WP_API
|
|
||||||
GType wp_settings_spec_get_type (void);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSettingsSpec *wp_settings_spec_ref (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_settings_spec_unref (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar * wp_settings_spec_get_name (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar * wp_settings_spec_get_description (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSettingsSpecType wp_settings_spec_get_value_type (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSpaJson * wp_settings_spec_get_default_value (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSpaJson * wp_settings_spec_get_min_value (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSpaJson * wp_settings_spec_get_max_value (WpSettingsSpec * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
gboolean wp_settings_spec_check_value (WpSettingsSpec * self, WpSpaJson *value);
|
|
||||||
|
|
||||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsSpec, wp_settings_spec_unref)
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief The WpSettingsItem GType
|
|
||||||
* \ingroup wpsettings
|
|
||||||
*/
|
|
||||||
#define WP_TYPE_SETTINGS_ITEM (wp_settings_item_get_type ())
|
|
||||||
WP_API
|
|
||||||
GType wp_settings_item_get_type (void);
|
|
||||||
|
|
||||||
typedef struct _WpSettingsItem WpSettingsItem;
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSettingsItem *wp_settings_item_ref (WpSettingsItem *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_settings_item_unref (WpSettingsItem *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
const gchar * wp_settings_item_get_key (WpSettingsItem * self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSpaJson * wp_settings_item_get_value (WpSettingsItem * self);
|
|
||||||
|
|
||||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsItem, wp_settings_item_unref)
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Flags to be used as WpObjectFeatures on WpSettings subclasses.
|
* \brief Flags to be used as WpObjectFeatures on WpSettings subclasses.
|
||||||
* \ingroup wpsettings
|
* \ingroup wpsettings
|
||||||
|
|
@ -113,10 +33,8 @@ WP_API
|
||||||
G_DECLARE_FINAL_TYPE (WpSettings, wp_settings, WP, SETTINGS, WpObject)
|
G_DECLARE_FINAL_TYPE (WpSettings, wp_settings, WP, SETTINGS, WpObject)
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpSettings * wp_settings_new (WpCore * core, const gchar * metadata_name);
|
WpSettings * wp_settings_get_instance (WpCore * core,
|
||||||
|
const gchar *metadata_name);
|
||||||
WP_API
|
|
||||||
WpSettings * wp_settings_find (WpCore * core, const gchar * metadata_name);
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief callback conveying the changed setting and its json value
|
* \brief callback conveying the changed setting and its json value
|
||||||
|
|
@ -144,38 +62,7 @@ gboolean wp_settings_unsubscribe (WpSettings *self,
|
||||||
guintptr subscription_id);
|
guintptr subscription_id);
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *name);
|
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *setting);
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSpaJson * wp_settings_get_saved (WpSettings *self, const gchar *name);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSettingsSpec * wp_settings_get_spec (WpSettings *self, const gchar *name);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
gboolean wp_settings_set (WpSettings *self, const gchar *name,
|
|
||||||
WpSpaJson *value);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
gboolean wp_settings_reset (WpSettings *self, const char *name);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
gboolean wp_settings_save (WpSettings *self, const char *name);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
gboolean wp_settings_delete (WpSettings *self, const char *name);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_settings_reset_all (WpSettings *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_settings_save_all (WpSettings *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
void wp_settings_delete_all (WpSettings *self);
|
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpIterator * wp_settings_new_iterator (WpSettings *self);
|
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ wp_si_adapter_get_ports_format (WpSiAdapter * self, const gchar **mode)
|
||||||
* \param self the session item
|
* \param self the session item
|
||||||
* \param format (transfer full) (nullable): the format to be set
|
* \param format (transfer full) (nullable): the format to be set
|
||||||
* \param mode (nullable): the mode
|
* \param mode (nullable): the mode
|
||||||
* \param callback (scope async)(closure data): the callback to call when the operation is done
|
* \param callback (scope async): the callback to call when the operation is done
|
||||||
* \param data user data for \a callback
|
* \param data (closure): user data for \a callback
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
wp_si_adapter_set_ports_format (WpSiAdapter * self, WpSpaPod *format,
|
wp_si_adapter_set_ports_format (WpSiAdapter * self, WpSpaPod *format,
|
||||||
|
|
@ -358,8 +358,8 @@ wp_si_acquisition_default_init (WpSiAcquisitionInterface * iface)
|
||||||
* \param self the session item
|
* \param self the session item
|
||||||
* \param acquisitor the link that is trying to acquire a port info item
|
* \param acquisitor the link that is trying to acquire a port info item
|
||||||
* \param item the item that is being acquired
|
* \param item the item that is being acquired
|
||||||
* \param callback (scope async)(closure data): the callback to call when the operation is done
|
* \param callback (scope async): the callback to call when the operation is done
|
||||||
* \param data user data for \a callback
|
* \param data (closure): user data for \a callback
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
wp_si_acquisition_acquire (WpSiAcquisition * self, WpSiLink * acquisitor,
|
wp_si_acquisition_acquire (WpSiAcquisition * self, WpSiLink * acquisitor,
|
||||||
|
|
|
||||||
|
|
@ -580,7 +580,7 @@ wp_spa_json_new_object_valist (const gchar *key, const gchar *format,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Checks whether the spa json is of type null or not
|
* \brief Checks wether the spa json is of type null or not
|
||||||
*
|
*
|
||||||
* \ingroup wpspajson
|
* \ingroup wpspajson
|
||||||
* \param self the spa json object
|
* \param self the spa json object
|
||||||
|
|
@ -593,7 +593,7 @@ wp_spa_json_is_null (WpSpaJson *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Checks whether the spa json is of type boolean or not
|
* \brief Checks wether the spa json is of type boolean or not
|
||||||
*
|
*
|
||||||
* \ingroup wpspajson
|
* \ingroup wpspajson
|
||||||
* \param self the spa json object
|
* \param self the spa json object
|
||||||
|
|
@ -606,7 +606,7 @@ wp_spa_json_is_boolean (WpSpaJson *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Checks whether the spa json is of type int or not
|
* \brief Checks wether the spa json is of type int or not
|
||||||
*
|
*
|
||||||
* \ingroup wpspajson
|
* \ingroup wpspajson
|
||||||
* \param self the spa json object
|
* \param self the spa json object
|
||||||
|
|
@ -619,7 +619,7 @@ wp_spa_json_is_int (WpSpaJson *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Checks whether the spa json is of type float or not
|
* \brief Checks wether the spa json is of type float or not
|
||||||
*
|
*
|
||||||
* \ingroup wpspajson
|
* \ingroup wpspajson
|
||||||
* \param self the spa json object
|
* \param self the spa json object
|
||||||
|
|
@ -632,7 +632,7 @@ wp_spa_json_is_float (WpSpaJson *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Checks whether the spa json is of type string or not
|
* \brief Checks wether the spa json is of type string or not
|
||||||
*
|
*
|
||||||
* \ingroup wpspajson
|
* \ingroup wpspajson
|
||||||
* \param self the spa json object
|
* \param self the spa json object
|
||||||
|
|
@ -1355,39 +1355,6 @@ wp_spa_json_parser_new_object (WpSpaJson *json)
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Creates a new spa json parser for undefined type of data. The \a json
|
|
||||||
* object must be valid for the entire life-cycle of the returned parser.
|
|
||||||
*
|
|
||||||
* This function allows creating a parser object for any type of spa json and is
|
|
||||||
* mostly useful to parse non-standard JSON data that should be treated as if it
|
|
||||||
* were an object or array, but does not start with a '{' or '[' character. Such
|
|
||||||
* data can be for instance a comma-separated list of single values (array) or
|
|
||||||
* key-value pairs (object). Such data is also the main configuration file,
|
|
||||||
* which is an object but doesn't start with a '{' character.
|
|
||||||
*
|
|
||||||
* \note If the data is an array or object, the parser will not enter it and the
|
|
||||||
* only token it will be able to parse is the same \a json object that is passed
|
|
||||||
* in as an argument. Use wp_spa_json_parser_new_array() or
|
|
||||||
* wp_spa_json_parser_new_object() to parse arrays or objects.
|
|
||||||
*
|
|
||||||
* \ingroup wpspajson
|
|
||||||
* \param json the spa json to parse
|
|
||||||
* \returns (transfer full): The new spa json parser
|
|
||||||
* \since 0.5.0
|
|
||||||
*/
|
|
||||||
WpSpaJsonParser *
|
|
||||||
wp_spa_json_parser_new_undefined (WpSpaJson *json)
|
|
||||||
{
|
|
||||||
WpSpaJsonParser *self;
|
|
||||||
|
|
||||||
self = g_rc_box_new0 (WpSpaJsonParser);
|
|
||||||
self->json = json;
|
|
||||||
self->data[0] = *json->json;
|
|
||||||
self->pos = &self->data[0];
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
check_nested_size (struct spa_json *parent, const gchar *data, int size)
|
check_nested_size (struct spa_json *parent, const gchar *data, int size)
|
||||||
{
|
{
|
||||||
|
|
@ -1524,10 +1491,6 @@ wp_spa_json_parser_get_string (WpSpaJsonParser *self)
|
||||||
/*!
|
/*!
|
||||||
* \brief Gets the spa json value from a spa json parser object
|
* \brief Gets the spa json value from a spa json parser object
|
||||||
*
|
*
|
||||||
* \note the returned spa json object references the original data instead
|
|
||||||
* of copying it, therefore the original data must be valid for the entire
|
|
||||||
* life-cycle of the returned object
|
|
||||||
*
|
|
||||||
* \ingroup wpspajson
|
* \ingroup wpspajson
|
||||||
* \param self the spa json parser object
|
* \param self the spa json parser object
|
||||||
* \returns (transfer full): The spa json value or NULL if it could not be
|
* \returns (transfer full): The spa json value or NULL if it could not be
|
||||||
|
|
@ -1537,8 +1500,7 @@ WpSpaJson *
|
||||||
wp_spa_json_parser_get_json (WpSpaJsonParser *self)
|
wp_spa_json_parser_get_json (WpSpaJsonParser *self)
|
||||||
{
|
{
|
||||||
return wp_spa_json_parser_advance (self) ?
|
return wp_spa_json_parser_advance (self) ?
|
||||||
wp_spa_json_new_wrap_stringn (self->curr.cur,
|
wp_spa_json_new_wrap (&self->curr) : NULL;
|
||||||
self->curr.end - self->curr.cur) : NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
|
|
|
||||||
|
|
@ -244,9 +244,6 @@ WpSpaJsonParser *wp_spa_json_parser_new_array (WpSpaJson *json);
|
||||||
WP_API
|
WP_API
|
||||||
WpSpaJsonParser *wp_spa_json_parser_new_object (WpSpaJson *json);
|
WpSpaJsonParser *wp_spa_json_parser_new_object (WpSpaJson *json);
|
||||||
|
|
||||||
WP_API
|
|
||||||
WpSpaJsonParser *wp_spa_json_parser_new_undefined (WpSpaJson *json);
|
|
||||||
|
|
||||||
WP_API
|
WP_API
|
||||||
gboolean wp_spa_json_parser_get_null (WpSpaJsonParser *self);
|
gboolean wp_spa_json_parser_get_null (WpSpaJsonParser *self);
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue