Compare commits

..

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

315 changed files with 8351 additions and 25700 deletions

View file

@ -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"

View file

@ -1,37 +0,0 @@
## Building and Testing
- To compile the project: `meson compile -C build` (compiles everything, no target needed)
- To run tests: `meson test -C build`
- The build artifacts always live in a directory called `build` or `builddir`.
If `build` doesn't exist, use `-C builddir` in the meson commands.
## Git Workflow
- Main branch: `master`
- Always create feature branches for new work
- Use descriptive commit messages following project conventions
- Reference GitLab MR/issue numbers in commits where applicable
- Never commit build artifacts or temporary files
- Use `glab` CLI tool for GitLab interactions (MRs, issues, etc.)
## Making a release
- Each release always consists of an entry in NEWS.rst, at the top of the file, which describes
the changes between the previous release and the current one. In addition, each release is given
a unique version number, which is present:
1. on the section header of that NEWS.rst entry
2. in the project() command in meson.build
3. on the commit message of the commit that introduces the above 2 changes
4. on the git tag that marks the above commit
- In order to make a release:
- Begin by analyzing the git history and the merged MRs from GitLab between the previous release
and today. GitLab MRs that are relevant always have the new release's version number set as a
"milestone"
- Create a new entry in NEWS.rst describing the changes, in a similar style and format as the
previous entries. Consolidate the changes to larger work items and also reference the relevant
gitlab MR that corresponds to each change and/or the gitlab issues that were addressed by each
change.
- Make sure to move the "Past releases" section header up, so that the only 2 top-level sections
are the new release section and the "Past releases" section.
- Edit meson.build to change the project version to the new release number
- Do not commit anything to git. Let the user review the changes and commit manually.

654
NEWS.rst
View file

@ -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
.................. ..................

View file

@ -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

View file

@ -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

View file

@ -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)
]

View file

@ -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"):

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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)

View file

@ -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**

View file

@ -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``.

View file

@ -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.

View file

@ -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

View 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.

View 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.

View file

@ -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',
) )

View file

@ -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.

View file

@ -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"
}
]

View 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.

View 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.

View file

@ -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``

View file

@ -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

View file

@ -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.

View file

@ -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
-------- --------

View file

@ -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')

View file

@ -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.

View file

@ -40,7 +40,7 @@ Synopsis:
$ meson -Dsession-managers="[ 'wireplumber' ]" build $ meson -Dsession-managers="[ 'wireplumber' ]" build
$ ninja -C build $ ninja -C build
$ make -C build run $ make run
Run independently or without installing Run independently or without installing
--------------------------------------- ---------------------------------------

View file

@ -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

View file

@ -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

View file

@ -23,7 +23,6 @@ C API Documentation
c_api/link_api.rst c_api/link_api.rst
c_api/device_api.rst c_api/device_api.rst
c_api/client_api.rst c_api/client_api.rst
c_api/permission_manager_api.rst
c_api/metadata_api.rst c_api/metadata_api.rst
c_api/spa_device_api.rst c_api/spa_device_api.rst
c_api/impl_node_api.rst c_api/impl_node_api.rst
@ -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

View file

@ -1,6 +0,0 @@
.. _base_dirs_api:
Base Directories File Lookup
============================
.. doxygengroup:: wpbasedirs
:content-only:

View file

@ -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',

View file

@ -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

View file

@ -1,17 +0,0 @@
.. _permission_manager_api:
WpPermissionManager
===================
.. graphviz::
:align: center
digraph inheritance {
rankdir=LR;
GObject -> WpObject;
WpObject -> WpPermissionManager;
}
.. doxygenstruct:: WpPermissionManager
.. doxygengroup:: wppermissionmanager
:content-only:

View file

@ -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

View file

@ -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')

View file

@ -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.``.

View file

@ -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',
)

View file

@ -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.

View file

@ -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.

View file

@ -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
}
}

View file

@ -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`

View file

@ -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')

View file

@ -1,3 +0,0 @@
sphinx_files += files(
'wpctl.rst',
)

View file

@ -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/

View file

@ -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);
}

View file

@ -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

View file

@ -9,7 +9,6 @@
#include "client.h" #include "client.h"
#include "log.h" #include "log.h"
#include "private/pipewire-object-mixin.h" #include "private/pipewire-object-mixin.h"
#include "private/permission-manager.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client") WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
@ -26,7 +25,6 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
struct _WpClient struct _WpClient
{ {
WpGlobalProxy parent; WpGlobalProxy parent;
GWeakRef permission_manager;
}; };
static void wp_client_pw_object_mixin_priv_interface_init ( static void wp_client_pw_object_mixin_priv_interface_init (
@ -41,7 +39,6 @@ G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
static void static void
wp_client_init (WpClient * self) wp_client_init (WpClient * self)
{ {
g_weak_ref_init (&self->permission_manager, NULL);
} }
static void static void
@ -79,27 +76,11 @@ wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
static void static void
wp_client_pw_proxy_destroyed (WpProxy * proxy) wp_client_pw_proxy_destroyed (WpProxy * proxy)
{ {
WpClient *self = WP_CLIENT (proxy);
wp_client_attach_permission_manager (self, NULL);
wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy); wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy); WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy);
} }
static void
wp_impl_node_finalize (GObject * object)
{
WpClient *self = WP_CLIENT (object);
wp_client_attach_permission_manager (self, NULL);
g_weak_ref_clear (&self->permission_manager);
G_OBJECT_CLASS (wp_client_parent_class)->finalize (object);
}
static void static void
wp_client_class_init (WpClientClass * klass) wp_client_class_init (WpClientClass * klass)
{ {
@ -107,7 +88,6 @@ wp_client_class_init (WpClientClass * klass)
WpObjectClass *wpobject_class = (WpObjectClass *) klass; WpObjectClass *wpobject_class = (WpObjectClass *) klass;
WpProxyClass *proxy_class = (WpProxyClass *) klass; WpProxyClass *proxy_class = (WpProxyClass *) klass;
object_class->finalize = wp_impl_node_finalize;
object_class->get_property = wp_pw_object_mixin_get_property; object_class->get_property = wp_pw_object_mixin_get_property;
wpobject_class->get_supported_features = wpobject_class->get_supported_features =
@ -241,30 +221,3 @@ wp_client_update_properties (WpClient * self, WpProperties * updates)
g_warn_if_fail (client_update_properties_result >= 0); g_warn_if_fail (client_update_properties_result >= 0);
} }
/*!
* \brief Attaches a permission manager in the client to handle permissions
* automatically.
*
* \ingroup wpclient
* \param self the client
* \param pm (transfer none) (nullable): the permission manager to attach, or
* NULL to detach the current permission manager.
*/
void
wp_client_attach_permission_manager (WpClient *self, WpPermissionManager *pm)
{
g_autoptr (WpPermissionManager) curr_pm = NULL;
g_return_if_fail (WP_IS_CLIENT (self));
curr_pm = g_weak_ref_get (&self->permission_manager);
if (curr_pm == pm)
return;
if (curr_pm)
wp_permission_manager_remove_client (curr_pm, self);
if (pm)
wp_permission_manager_add_client (pm, self);
g_weak_ref_set (&self->permission_manager, pm);
}

View file

@ -10,7 +10,6 @@
#define __WIREPLUMBER_CLIENT_H__ #define __WIREPLUMBER_CLIENT_H__
#include "global-proxy.h" #include "global-proxy.h"
#include "permission-manager.h"
G_BEGIN_DECLS G_BEGIN_DECLS
@ -38,10 +37,6 @@ void wp_client_update_permissions_array (WpClient * self,
WP_API WP_API
void wp_client_update_properties (WpClient * self, WpProperties * updates); void wp_client_update_properties (WpClient * self, WpProperties * updates);
WP_API
void wp_client_attach_permission_manager (WpClient *self,
WpPermissionManager *pm);
G_END_DECLS G_END_DECLS
#endif #endif

View file

@ -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,

View file

@ -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 (&section->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 (&section, 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;
} }

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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));
} }

View file

@ -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

View file

@ -15,161 +15,6 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher") WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
typedef struct _HookData HookData;
struct _HookData
{
struct spa_list link;
WpEventHook *hook;
GPtrArray *dependencies;
};
static inline HookData *
hook_data_new (WpEventHook * hook)
{
HookData *hook_data = g_new0 (HookData, 1);
spa_list_init (&hook_data->link);
hook_data->hook = g_object_ref (hook);
hook_data->dependencies = g_ptr_array_new ();
return hook_data;
}
static void
hook_data_free (HookData *self)
{
g_clear_object (&self->hook);
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
g_free (self);
}
static inline void
record_dependency (struct spa_list *list, const gchar *target,
const gchar *dependency)
{
HookData *hook_data;
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
break;
}
}
}
static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
return TRUE;
}
}
}
return FALSE;
}
static gboolean
sort_hooks (GPtrArray *hooks)
{
struct spa_list collected, result, remaining;
HookData *sorted_hook_data = NULL;
spa_list_init (&collected);
spa_list_init (&result);
spa_list_init (&remaining);
for (guint i = 0; i < hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (hooks, i);
HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
}
if (!spa_list_is_empty (&collected)) {
HookData *hook_data;
/* convert "before" dependencies into "after" dependencies */
spa_list_for_each (hook_data, &collected, link) {
const gchar * const * strv =
wp_event_hook_get_runs_before_hooks (hook_data->hook);
while (strv && *strv) {
/* record hook_data->hook as a dependency of the *strv hook */
record_dependency (&collected, *strv,
wp_event_hook_get_name (hook_data->hook));
strv++;
}
}
/* sort */
while (!spa_list_is_empty (&collected)) {
gboolean made_progress = FALSE;
/* examine each hook to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
spa_list_consume (hook_data, &collected, link) {
guint deps_satisfied = 0;
spa_list_remove (&hook_data->link);
for (guint i = 0; i < hook_data->dependencies->len; i++) {
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
/* if the dependency is already in the sorted result list or if
it doesn't exist at all, we consider it satisfied */
if (hook_exists_in (dep, &result) ||
!(hook_exists_in (dep, &collected) ||
hook_exists_in (dep, &remaining))) {
deps_satisfied++;
}
}
if (deps_satisfied == hook_data->dependencies->len) {
spa_list_append (&result, &hook_data->link);
made_progress = TRUE;
} else {
spa_list_append (&remaining, &hook_data->link);
}
}
if (made_progress) {
/* run again with the remaining hooks */
spa_list_insert_list (&collected, &remaining);
spa_list_init (&remaining);
}
else if (!spa_list_is_empty (&remaining)) {
/* if we did not make any progress towards growing the result list,
it means the dependencies cannot be satisfied because of circles */
spa_list_consume (hook_data, &result, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
spa_list_consume (hook_data, &remaining, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
return FALSE;
}
}
}
/* clear hooks and add the sorted ones */
g_ptr_array_set_size (hooks, 0);
spa_list_consume (sorted_hook_data, &result, link) {
spa_list_remove (&sorted_hook_data->link);
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
hook_data_free (sorted_hook_data);
}
return TRUE;
}
typedef struct _EventData EventData; typedef struct _EventData EventData;
struct _EventData struct _EventData
{ {
@ -204,8 +49,7 @@ struct _WpEventDispatcher
GObject parent; GObject parent;
GWeakRef core; GWeakRef core;
GHashTable *defined_hooks; /* registered hooks for defined events */ GPtrArray *hooks; /* registered hooks */
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
GSource *source; /* the event loop source */ GSource *source; /* the event loop source */
GList *events; /* the events stack */ GList *events; /* the events stack */
struct spa_system *system; struct spa_system *system;
@ -260,7 +104,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
/* get the highest priority event */ /* get the highest priority event */
GList *levent = g_list_first (d->events); GList *levent = g_list_first (d->events);
if (levent) { while (levent) {
EventData *event_data = (EventData *) (levent->data); EventData *event_data = (EventData *) (levent->data);
WpEvent *event = event_data->event; WpEvent *event = event_data->event;
GCancellable *cancellable = wp_event_get_cancellable (event); GCancellable *cancellable = wp_event_get_cancellable (event);
@ -300,8 +144,6 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
/* get the next event */ /* get the next event */
levent = g_list_first (d->events); levent = g_list_first (d->events);
if (levent && !((EventData *) levent->data)->current_hook_in_async)
spa_system_eventfd_write (d->system, d->eventfd, 1);
} }
return G_SOURCE_CONTINUE; return G_SOURCE_CONTINUE;
@ -318,9 +160,7 @@ static void
wp_event_dispatcher_init (WpEventDispatcher * self) wp_event_dispatcher_init (WpEventDispatcher * self)
{ {
g_weak_ref_init (&self->core, NULL); g_weak_ref_init (&self->core, NULL);
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
(GDestroyNotify)g_ptr_array_unref);
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->source = g_source_new (&source_funcs, sizeof (WpEventSource)); self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
((WpEventSource *) self->source)->dispatcher = self; ((WpEventSource *) self->source)->dispatcher = self;
@ -344,8 +184,7 @@ wp_event_dispatcher_finalize (GObject * object)
close (self->eventfd); close (self->eventfd);
g_clear_pointer (&self->defined_hooks, g_hash_table_unref); g_clear_pointer (&self->hooks, g_ptr_array_unref);
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
g_weak_ref_clear (&self->core); g_weak_ref_clear (&self->core);
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object); G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
@ -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);
} }

View file

@ -41,12 +41,7 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpEventHook * hook); WpEventHook * hook);
WP_API WP_API
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self) WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
G_GNUC_DEPRECATED_FOR (wp_event_dispatcher_new_hooks_for_event_type_iterator);
WP_API
WpIterator * wp_event_dispatcher_new_hooks_for_event_type_iterator (
WpEventDispatcher * self, const gchar *event_type);
G_END_DECLS G_END_DECLS

View file

@ -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;
} }
/*! /*!

View file

@ -39,10 +39,8 @@ struct _WpEventHookClass
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error); gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
/*< private >*/ /*< private >*/
WP_PADDING(4) WP_PADDING(5)
}; };
WP_API WP_API
@ -69,9 +67,6 @@ void wp_event_hook_run (WpEventHook * self,
WpEvent * event, GCancellable * cancellable, WpEvent * event, GCancellable * cancellable,
GAsyncReadyCallback callback, gpointer callback_data); GAsyncReadyCallback callback, gpointer callback_data);
WP_API
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
WP_API WP_API
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res, gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
GError ** error); GError ** error);

View file

@ -17,11 +17,37 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event") WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
typedef struct _HookData HookData;
struct _HookData
{
struct spa_list link;
WpEventHook *hook;
GPtrArray *dependencies;
};
static inline HookData *
hook_data_new (WpEventHook * hook)
{
HookData *hook_data = g_new0 (HookData, 1);
spa_list_init (&hook_data->link);
hook_data->hook = g_object_ref (hook);
hook_data->dependencies = g_ptr_array_new ();
return hook_data;
}
static void
hook_data_free (HookData *self)
{
g_clear_object (&self->hook);
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
g_free (self);
}
struct _WpEvent struct _WpEvent
{ {
grefcount ref; grefcount ref;
GData *datalist; GData *datalist;
GPtrArray *hooks; struct spa_list hooks;
/* immutable fields */ /* immutable fields */
gint priority; gint priority;
@ -70,7 +96,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
WpEvent * self = g_new0 (WpEvent, 1); WpEvent * self = g_new0 (WpEvent, 1);
g_ref_count_init (&self->ref); g_ref_count_init (&self->ref);
g_datalist_init (&self->datalist); g_datalist_init (&self->datalist);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref); spa_list_init (&self->hooks);
self->priority = priority; self->priority = priority;
self->properties = properties ? self->properties = properties ?
@ -129,7 +155,11 @@ wp_event_get_name(WpEvent *self)
static void static void
wp_event_free (WpEvent * self) wp_event_free (WpEvent * self)
{ {
g_clear_pointer (&self->hooks, g_ptr_array_unref); HookData *hook_data;
spa_list_consume (hook_data, &self->hooks, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
g_datalist_clear (&self->datalist); g_datalist_clear (&self->datalist);
g_clear_pointer (&self->properties, wp_properties_unref); g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_object (&self->source); g_clear_object (&self->source);
@ -286,6 +316,33 @@ wp_event_get_data (WpEvent * self, const gchar * key)
return g_datalist_get_data (&self->datalist, key); return g_datalist_get_data (&self->datalist, key);
} }
static inline void
record_dependency (struct spa_list *list, const gchar *target,
const gchar *dependency)
{
HookData *hook_data;
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
break;
}
}
}
static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
return TRUE;
}
}
}
return FALSE;
}
/*! /*!
* \brief Collects all the hooks registered in the \a dispatcher that run for * \brief Collects all the hooks registered in the \a dispatcher that run for
* this \a event * this \a event
@ -298,37 +355,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;
} }

View file

@ -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);
} }

View file

@ -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

View file

@ -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;
} }

View file

@ -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
* *

View file

@ -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);
} }

View file

@ -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__); \
}) })

View file

@ -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(

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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 */

View file

@ -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;
}

View file

@ -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

View file

@ -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,
&registry_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);
} }

View file

@ -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

View file

@ -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,

View file

@ -1,707 +0,0 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <pipewire/permission.h>
#include <pipewire/pipewire.h>
#include "private/permission-manager.h"
#include "permission-manager.h"
#include "proxy-interfaces.h"
#include "object-manager.h"
#include "json-utils.h"
#include "error.h"
#include "core.h"
#include "log.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-permission-manager")
/*! \defgroup wppermissionmanager WpPermissionManager */
/*!
* \struct WpPermissionManager
*
* The WpPermissionManager class is in charge of updating automatically
* permissions on interested objects every time they are added or removed for
* a particular client.
*
* WpPermissionManager API.
*/
typedef struct _PermissionMatch PermissionMatch;
struct _PermissionMatch
{
guint32 id;
guint32 permissions;
GClosure *closure;
WpObjectInterest *interest;
WpSpaJson *rules;
};
static guint
get_next_id ()
{
static guint32 next_id = 0;
g_atomic_int_inc (&next_id);
return next_id;
}
static PermissionMatch *
permission_match_new (guint32 perms, GClosure *closure,
WpObjectInterest * interest, WpSpaJson * rules)
{
PermissionMatch *match = g_new0 (PermissionMatch, 1);
match->id = get_next_id ();
match->permissions = perms;
match->closure = closure ? g_closure_ref (closure) : NULL;
match->interest = interest ? wp_object_interest_ref (interest) : NULL;
match->rules = rules ? wp_spa_json_ref (rules) : NULL;
return match;
}
static void
permission_interest_free (PermissionMatch *self)
{
g_clear_pointer (&self->closure, g_closure_unref);
g_clear_pointer (&self->interest, wp_object_interest_unref);
g_clear_pointer (&self->rules, wp_spa_json_unref);
g_free (self);
}
struct _WpPermissionManager
{
WpObject parent;
guint32 default_perms;
guint32 core_perms;
GPtrArray *clients;
GHashTable *matches;
WpObjectManager *om;
};
G_DEFINE_TYPE (WpPermissionManager, wp_permission_manager, WP_TYPE_OBJECT)
static void
wp_permission_manager_init (WpPermissionManager * self)
{
/* Init default permissions to all */
self->default_perms = PW_PERM_R | PW_PERM_W | PW_PERM_X;
/* Core permissions not set by default (inherit from default_perms) */
self->core_perms = PW_PERM_INVALID;
/* Init permission interests table */
self->matches = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
(GDestroyNotify)permission_interest_free);
/* Init clients list */
self->clients = g_ptr_array_new_with_free_func (
(GDestroyNotify) g_object_unref);
}
enum {
STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
};
static WpObjectFeatures
wp_permission_manager_get_supported_features (WpObject * self)
{
return WP_PERMISSION_MANAGER_LOADED;
}
static guint
wp_permission_manager_activate_get_next_step (WpObject * self,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
g_return_val_if_fail (missing == WP_PERMISSION_MANAGER_LOADED,
WP_TRANSITION_STEP_ERROR);
return STEP_LOAD;
}
static guint32
invoke_permissions_closure (WpPermissionManager *self, WpClient *client,
WpGlobalProxy *object, GClosure *closure)
{
GValue args[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };
GValue ret = G_VALUE_INIT;
guint32 perms;
g_value_init (&args[0], WP_TYPE_PERMISSION_MANAGER);
g_value_set_object (&args[0], self);
g_value_init (&args[1], WP_TYPE_CLIENT);
g_value_set_object (&args[1], client);
g_value_init (&args[2], WP_TYPE_GLOBAL_PROXY);
g_value_set_object (&args[2], object);
g_value_init (&ret, G_TYPE_UINT);
g_closure_invoke (closure, &ret, 3, args, NULL);
perms = g_value_get_uint (&ret);
g_value_unset (&args[0]);
g_value_unset (&args[1]);
g_value_unset (&args[2]);
g_value_unset (&ret);
return perms;
}
typedef struct _MatchRulesCallbackData MatchRulesCallbackData;
struct _MatchRulesCallbackData {
gboolean matched;
guint32 perms;
};
static gboolean
match_rules_cb (gpointer data, const gchar * action, WpSpaJson * value,
GError ** e)
{
MatchRulesCallbackData *cb_data = (MatchRulesCallbackData *)data;
g_autofree gchar *perms_str = NULL;
guint32 perms = 0;
if (!g_str_equal (action, "set-permissions")) {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Action name '%s' is not valid", action);
return FALSE;
}
if (!wp_spa_json_is_string (value)) {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Action '%s' must be a string", action);
return FALSE;
}
/* Parse permissions */
perms_str = wp_spa_json_parse_string (value);
if (g_strcmp0 (perms_str, "all") == 0) {
perms = PW_PERM_ALL;
} else if (perms_str) {
for (guint i = 0; i < strlen (perms_str); i++) {
switch (perms_str[i]) {
case 'r': perms |= PW_PERM_R; break;
case 'w': perms |= PW_PERM_W; break;
case 'x': perms |= PW_PERM_X; break;
case 'm': perms |= PW_PERM_M; break;
case 'l': perms |= PW_PERM_L; break;
case '-': break;
default: {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Permissions '%s' are not valid", perms_str);
return FALSE;
}
}
}
}
if (cb_data) {
cb_data->matched = TRUE;
cb_data->perms |= perms;
}
return TRUE;
}
static gboolean
get_rules_matched_object_permissions (WpPermissionManager *self,
WpSpaJson *rules, WpGlobalProxy *object, guint32 *perms)
{
g_autoptr (GError) e = NULL;
g_autoptr (WpProperties) gp_props = NULL;
g_autoptr (WpProperties) po_props = NULL;
MatchRulesCallbackData data = { FALSE, 0 };
/* Check global proxy properties */
gp_props = wp_global_proxy_get_global_properties (object);
if (gp_props && !wp_json_utils_match_rules (rules, gp_props, match_rules_cb,
&data, &e))
goto error;
/* Also check pipewire object properties if it is a pipewire object */
if (WP_IS_PIPEWIRE_OBJECT (object)) {
po_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object));
if (po_props && !wp_json_utils_match_rules (rules, po_props, match_rules_cb,
&data, &e))
goto error;
}
/* Set permissions if there was a match */
if (data.matched && perms)
*perms = data.perms;
return data.matched;
error:
wp_warning_object (self, "Malformed JSON match rules: %s", e->message);
return FALSE;
}
static gboolean
get_matched_object_permissions (WpPermissionManager *self, PermissionMatch *m,
WpClient *client, WpGlobalProxy *object, guint32 *perms)
{
/* Check interest */
if (m->interest && wp_object_interest_matches (m->interest, object)) {
if (!perms)
return TRUE;
*perms = m->closure ? invoke_permissions_closure (self, client, object,
m->closure) : m->permissions;
return TRUE;
}
/* Check rules */
if (m->rules)
return get_rules_matched_object_permissions (self, m->rules, object, perms);
return FALSE;
}
static GArray *
build_permissions_array (WpPermissionManager *self, WpClient *client)
{
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) value = G_VALUE_INIT;
struct pw_permission def_perm = { PW_ID_ANY, self->default_perms };
GArray *arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));
/* Add default permissions */
g_array_append_val (arr, def_perm);
/* Add core permissions if explicitly set (core is not in the OM since it is
* implicit in the PipeWire connection and not sent through the registry) */
if (self->core_perms != PW_PERM_INVALID) {
struct pw_permission core_perm = { PW_ID_CORE, self->core_perms };
g_array_append_val (arr, core_perm);
}
/* Add object specific permissions in the array */
it = wp_object_manager_new_iterator (self->om);
for (; wp_iterator_next (it, &value); g_value_unset (&value)) {
WpGlobalProxy *object = g_value_get_object (&value);
GHashTableIter iter;
PermissionMatch *match = NULL;
g_hash_table_iter_init (&iter, self->matches);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&match)) {
guint32 perms = PW_PERM_INVALID;
if (get_matched_object_permissions (self, match, client, object, &perms)
&& perms != PW_PERM_INVALID) {
struct pw_permission obj_perm = { 0, };
obj_perm.id = wp_proxy_get_bound_id (WP_PROXY (object));
obj_perm.permissions = perms;
g_array_append_val (arr, obj_perm);;
}
}
}
/* Merge permissions with same object ID */
for (guint i = 0; i < arr->len; i++) {
for (guint j = i + 1; j < arr->len; ) {
struct pw_permission *a = &g_array_index (arr, struct pw_permission, i);
struct pw_permission *b = &g_array_index (arr, struct pw_permission, j);
if (a->id == b->id) {
a->permissions |= b->permissions;
g_array_remove_index (arr, j);
} else {
j++;
}
}
}
return arr;
}
static void
update_client_permissions (WpPermissionManager *self, WpClient *client)
{
guint32 bound_id = 0;
g_autoptr (GArray) perms = NULL;
/* Dont do anything if the permission manager is not activated */
if (!(wp_object_get_active_features (WP_OBJECT (self)) &
WP_PERMISSION_MANAGER_LOADED))
return;
/* Make sure the client proxy is still valid */
if (!wp_proxy_get_pw_proxy (WP_PROXY (client)))
return;
bound_id = wp_proxy_get_bound_id (WP_PROXY (client));
perms = build_permissions_array (self, client);
wp_info_object (self,
"Updating permissions on client %u: any=%c%c%c%c%c len=%u",
bound_id,
!!(self->default_perms & PW_PERM_R) ? 'r' : '-',
!!(self->default_perms & PW_PERM_W) ? 'w' : '-',
!!(self->default_perms & PW_PERM_X) ? 'x' : '-',
!!(self->default_perms & PW_PERM_M) ? 'm' : '-',
!!(self->default_perms & PW_PERM_L) ? 'l' : '-',
perms->len);
wp_client_update_permissions_array (client, perms->len,
(const struct pw_permission *) perms->data);
}
static gboolean
has_object_match (WpPermissionManager *self, WpGlobalProxy *object)
{
GHashTableIter iter;
PermissionMatch *m = NULL;
g_hash_table_iter_init (&iter, self->matches);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&m)) {
if (m->interest && wp_object_interest_matches (m->interest, object))
return TRUE;
if (m->rules && get_rules_matched_object_permissions (self, m->rules,
object, NULL))
return TRUE;
}
return FALSE;
}
static void
update_permissions (WpPermissionManager *self)
{
for (guint i = 0; i < self->clients->len; i++) {
WpClient *client = g_ptr_array_index (self->clients, i);
update_client_permissions (self, client);
}
}
static void
on_object_added_or_removed (WpObjectManager *om, WpGlobalProxy *object,
gpointer d)
{
WpPermissionManager * self = WP_PERMISSION_MANAGER (d);
if (has_object_match (self, object))
update_permissions (self);
}
static void
on_object_manager_installed (WpObjectManager *om, gpointer d)
{
WpTransition * transition = WP_TRANSITION (d);
WpPermissionManager * self = wp_transition_get_source_object (transition);
wp_object_update_features (WP_OBJECT (self), WP_PERMISSION_MANAGER_LOADED, 0);
}
static void
wp_permission_manager_activate_execute_step (WpObject * object,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_autoptr (WpCore) core = wp_object_get_core (object);
switch (step) {
case STEP_LOAD: {
/* Install object manager */
g_clear_object (&self->om);
self->om = wp_object_manager_new ();
wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
wp_object_manager_request_object_features (self->om,
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
g_signal_connect_object (self->om, "object-added",
G_CALLBACK (on_object_added_or_removed), self, 0);
g_signal_connect_object (self->om, "object-removed",
G_CALLBACK (on_object_added_or_removed), self, 0);
g_signal_connect_object (self->om, "installed",
G_CALLBACK (on_object_manager_installed), transition, 0);
wp_core_install_object_manager (core, self->om);
break;
}
case WP_TRANSITION_STEP_ERROR:
break;
default:
g_assert_not_reached ();
}
}
static void
wp_permission_manager_deactivate (WpObject * object, WpObjectFeatures features)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_clear_object (&self->om);
wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
}
static void
wp_permission_manager_finalize (GObject * object)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_clear_pointer (&self->clients, g_ptr_array_unref);
g_clear_pointer (&self->matches, g_hash_table_unref);
g_clear_object (&self->om);
G_OBJECT_CLASS (wp_permission_manager_parent_class)->finalize (object);
}
static void
wp_permission_manager_class_init (WpPermissionManagerClass * klass)
{
GObjectClass * object_class = (GObjectClass *) klass;
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
object_class->finalize = wp_permission_manager_finalize;
wpobject_class->get_supported_features =
wp_permission_manager_get_supported_features;
wpobject_class->activate_get_next_step =
wp_permission_manager_activate_get_next_step;
wpobject_class->activate_execute_step =
wp_permission_manager_activate_execute_step;
wpobject_class->deactivate = wp_permission_manager_deactivate;
}
void
wp_permission_manager_add_client (WpPermissionManager *self, WpClient *client)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_ptr_array_add (self->clients, g_object_ref (client));
update_client_permissions (self, client);
}
void
wp_permission_manager_remove_client (WpPermissionManager *self,
WpClient *client)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_ptr_array_remove_fast (self->clients, client);
update_client_permissions (self, client);
}
/*!
* \brief Creates a new WpPermissionManager object
*
* \ingroup wppermissionmanager
* \param core the WpCore
* \returns (transfer full): a new WpPermissionManager object
*/
WpPermissionManager *
wp_permission_manager_new (WpCore * core)
{
g_return_val_if_fail (core, NULL);
return g_object_new (WP_TYPE_PERMISSION_MANAGER, "core", core, NULL);
}
/*!
* \brief Sets the default permissions that will be applied to all objects that
* don't match any interest
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the default permissions to apply
*/
void
wp_permission_manager_set_default_permissions (WpPermissionManager *self,
guint32 permissions)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
if (self->default_perms != permissions) {
self->default_perms = permissions;
update_permissions (self);
}
}
/*!
* \brief Sets the permissions that will be applied to the core object (ID 0).
*
* The core object is not visible to the permission manager's object manager
* because it is implicit in the PipeWire connection and not sent through the
* registry. This method allows setting explicit permissions on it, independent
* of the default permissions.
*
* If not set (or set to PW_PERM_INVALID), the core inherits default_permissions.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the permissions to apply to the core object
*/
void
wp_permission_manager_set_core_permissions (WpPermissionManager *self,
guint32 permissions)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
if (self->core_perms != permissions) {
self->core_perms = permissions;
update_permissions (self);
}
}
static guint32
wp_permission_manager_add_match (WpPermissionManager *self,
PermissionMatch *match)
{
guint id = match->id;
g_hash_table_insert (self->matches, GUINT_TO_POINTER (id), match);
update_permissions (self);
return id;
}
/*!
* \brief Adds an interest match to apply permissions with callback in matched
* objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param callback (scope async): the permissions match callback
* \param user_data data to pass to \a callback
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match (WpPermissionManager *self,
WpPermissionMatchCallback callback, gpointer user_data,
WpObjectInterest * interest)
{
GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);
return wp_permission_manager_add_interest_match_closure (self, closure,
interest);
}
/*!
* \brief Adds an interest match to apply permissions with closure in matched
* objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param closure (transfer full): the closure to apply permissions
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match_closure (WpPermissionManager *self,
GClosure *closure, WpObjectInterest * interest)
{
g_autoptr (WpObjectInterest) i = interest;
g_autoptr (GClosure) c = closure;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (closure, SPA_ID_INVALID);
g_return_val_if_fail (i, SPA_ID_INVALID);
if (G_CLOSURE_NEEDS_MARSHAL (closure))
g_closure_set_marshal (closure, g_cclosure_marshal_generic);
match = permission_match_new (PW_PERM_INVALID, c, i, NULL);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Adds an interest match to apply same permissions in matched objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the permissions to apply
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match_simple (WpPermissionManager *self,
guint32 permissions, WpObjectInterest * interest)
{
g_autoptr (WpObjectInterest) i = interest;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (i, SPA_ID_INVALID);
match = permission_match_new (permissions, NULL, i, NULL);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Adds a rules match to apply permissions in matched objects.
*
* The rules must be defined in a JSON object using the same format as all
* the wireplumber/pipewire rules.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param rules (transfer full): the JSON rules
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_rules_match (WpPermissionManager *self,
WpSpaJson *rules)
{
g_autoptr (WpSpaJson) r = rules;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (r, SPA_ID_INVALID);
match = permission_match_new (PW_PERM_INVALID, NULL, NULL, rules);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Removes the previously added match so that the associated permissions
* are not applied anymore.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param match_id the match ID to remove
*/
void
wp_permission_manager_remove_match (WpPermissionManager *self, guint32 match_id)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_return_if_fail (match_id != SPA_ID_INVALID);
g_hash_table_remove (self->matches, GUINT_TO_POINTER (match_id));
update_permissions (self);
}
/*!
* \brief Updates permissions on all clients the permission manager has.
*
* The permission manager already updates permissions on all clients
* automatically when a new client or object is added, however, this might be
* needed if interests with closures or callbacks were added and something
* changed externally.
*
* \ingroup wppermissionmanager
* \param self the permission manager
*/
void
wp_permission_manager_update_permissions (WpPermissionManager *self)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
update_permissions (self);
}

View file

@ -1,88 +0,0 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@ollabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PERMISSION_MANAGER_H__
#define __WIREPLUMBER_PERMISSION_MANAGER_H__
#include "object-interest.h"
#include "global-proxy.h"
G_BEGIN_DECLS
/*!
* \brief Flags to be used as WpObjectFeatures for WpPermissionManager.
* \ingroup wppermissionmanager
*/
typedef enum { /*< flags >*/
/*! Loads the permission manager */
WP_PERMISSION_MANAGER_LOADED = (1 << 0),
} WpPermissionManagerFeatures;
/*!
* \brief The WpPermissionManager GType
* \ingroup wppermissionmanager
*/
#define WP_TYPE_PERMISSION_MANAGER (wp_permission_manager_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpPermissionManager, wp_permission_manager, WP,
PERMISSION_MANAGER, WpObject)
typedef struct _WpClient WpClient;
/*!
* \brief callback to set permissions on the matched global object
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param client the client that will have its permissions updated
* \param object the matched global
* \param user_data the passed data
*/
typedef guint32 (*WpPermissionMatchCallback) (WpPermissionManager *self,
WpClient *client, WpGlobalProxy *object, gpointer user_data);
WP_API
WpPermissionManager * wp_permission_manager_new (WpCore * core);
WP_API
void wp_permission_manager_set_default_permissions (
WpPermissionManager *self, guint32 permissions);
WP_API
void wp_permission_manager_set_core_permissions (
WpPermissionManager *self, guint32 permissions);
WP_API
guint32 wp_permission_manager_add_interest_match (WpPermissionManager *self,
WpPermissionMatchCallback callback, gpointer user_data,
WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_interest_match_closure (
WpPermissionManager *self, GClosure *closure, WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_interest_match_simple (
WpPermissionManager *self, guint32 permissions,
WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_rules_match (WpPermissionManager *self,
WpSpaJson *rules);
WP_API
void wp_permission_manager_remove_match (WpPermissionManager *self,
guint32 match_id);
WP_API
void wp_permission_manager_update_permissions (WpPermissionManager *self);
G_END_DECLS
#endif

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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;
}

View file

@ -1,26 +0,0 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
#define __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
#include "client.h"
G_BEGIN_DECLS
typedef struct _WpPermissionManager WpPermissionManager;
void wp_permission_manager_add_client (WpPermissionManager *self,
WpClient *client);
void wp_permission_manager_remove_client (WpPermissionManager *self,
WpClient *client);
G_END_DECLS
#endif

View file

@ -783,7 +783,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset); G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
g_clear_pointer (&d->properties, wp_properties_unref); g_clear_pointer (&d->properties, wp_properties_unref);
d->properties = wp_properties_new_copy_dict (props); d->properties = wp_properties_new_wrap_dict (props);
g_object_notify (G_OBJECT (instance), "properties"); g_object_notify (G_OBJECT (instance), "properties");
} }

View file

@ -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,
&registry_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);
}

View file

@ -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)
{ {

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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.

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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