From 5de55f217f8efd89ab800b0bd49264148bc6fc54 Mon Sep 17 00:00:00 2001 From: Apertis CI <devel@lists.apertis.org> Date: Tue, 18 Jan 2022 07:50:54 +0000 Subject: [PATCH] Import Upstream version 0.4.7 --- .gitlab-ci.yml | 15 +- NEWS.rst | 73 ++- docs/rst/configuration.rst | 34 ++ docs/rst/configuration/access.rst | 63 +++ docs/rst/configuration/alsa.rst | 456 ++++++++++++++++++ docs/rst/configuration/bluetooth.rst | 160 ++++++ docs/rst/configuration/config_lua.rst | 170 +++++++ docs/rst/configuration/locations.rst | 94 ++++ docs/rst/configuration/main.rst | 126 +++++ docs/rst/configuration/meson.build | 11 + docs/rst/configuration/multi_instance.rst | 45 ++ docs/rst/configuration/policy.rst | 116 +++++ docs/rst/daemon-configuration.rst | 440 ----------------- docs/rst/daemon-logging.rst | 11 +- docs/rst/index.rst | 2 +- docs/rst/installing-wireplumber.rst | 6 +- docs/rst/lua_api/lua_introduction.rst | 10 +- docs/rst/meson.build | 3 +- docs/rst/running-wireplumber-daemon.rst | 122 +++-- lib/wp/core.c | 51 +- lib/wp/core.h | 4 + lib/wp/device.c | 3 +- lib/wp/endpoint.c | 1 - lib/wp/global-proxy.c | 12 +- lib/wp/iterator.c | 5 +- lib/wp/log.c | 20 +- lib/wp/metadata.c | 4 +- lib/wp/node.c | 1 - lib/wp/object-manager.c | 14 +- lib/wp/object.c | 41 +- lib/wp/object.h | 3 + lib/wp/private/pipewire-object-mixin.c | 33 +- lib/wp/private/pipewire-object-mixin.h | 6 +- lib/wp/proxy.c | 67 +-- lib/wp/proxy.h | 3 - lib/wp/spa-pod.c | 8 + lib/wp/transition.c | 2 +- lib/wp/wp.c | 3 + meson.build | 17 +- modules/module-default-nodes-api.c | 37 +- modules/module-default-nodes.c | 450 +++++++++++++---- modules/module-default-nodes/common.h | 14 +- modules/module-default-profile.c | 87 ++-- modules/module-device-activation.c | 293 +++++++++-- modules/module-lua-scripting.c | 1 + modules/module-mixer-api.c | 5 +- modules/module-reserve-device/plugin.c | 2 +- modules/module-route-settings-api.c | 5 +- modules/module-si-audio-adapter.c | 51 +- modules/module-si-node.c | 11 + modules/module-si-standard-link.c | 106 +++- src/config/bluetooth.conf | 22 + src/config/main.lua.d/40-device-defaults.lua | 3 + src/config/main.lua.d/50-alsa-config.lua | 15 + .../main.lua.d/50-default-access-config.lua | 8 + .../policy.lua.d/50-endpoints-config.lua | 84 +++- src/config/wireplumber.conf | 22 + src/main.c | 6 +- src/scripts/access/access-default.lua | 2 +- src/scripts/create-item.lua | 14 +- src/scripts/default-routes.lua | 46 +- src/scripts/monitors/alsa-midi.lua | 2 +- src/scripts/monitors/alsa.lua | 26 +- src/scripts/monitors/bluez.lua | 9 +- src/scripts/monitors/libcamera.lua | 12 +- src/scripts/monitors/v4l2.lua | 10 +- src/scripts/policy-endpoint-client-links.lua | 2 +- src/scripts/policy-endpoint-client.lua | 56 ++- src/scripts/policy-endpoint-device.lua | 52 +- src/scripts/policy-node.lua | 243 ++++++++-- src/scripts/suspend-node.lua | 6 +- 71 files changed, 2946 insertions(+), 1011 deletions(-) create mode 100644 docs/rst/configuration.rst create mode 100644 docs/rst/configuration/access.rst create mode 100644 docs/rst/configuration/alsa.rst create mode 100644 docs/rst/configuration/bluetooth.rst create mode 100644 docs/rst/configuration/config_lua.rst create mode 100644 docs/rst/configuration/locations.rst create mode 100644 docs/rst/configuration/main.rst create mode 100644 docs/rst/configuration/meson.build create mode 100644 docs/rst/configuration/multi_instance.rst create mode 100644 docs/rst/configuration/policy.rst delete mode 100644 docs/rst/daemon-configuration.rst diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f753ba99..8b1c81c3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -103,6 +103,11 @@ include: - export BUILD_ID="$CI_JOB_ID" - export PREFIX="$PWD/prefix-$BUILD_ID" - export PW_BUILD_DIR="$PWD/build-pipewire-$BUILD_ID" + - | + if [ -n "$FDO_CI_CONCURRENT" ]; then + NINJA_ARGS="-j$FDO_CI_CONCURRENT $NINJA_ARGS" + export NINJA_ARGS + fi # Build pipewire # Fedora also ships that, but without the test plugins that we need... - git clone --depth=1 --branch="$PIPEWIRE_HEAD" @@ -115,7 +120,7 @@ include: -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[] -Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled - - ninja -C "$PW_BUILD_DIR" install + - ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install # misc environment only for wireplumber - export WP_BUILD_DIR="$PWD/build-wireplumber-$BUILD_ID" - export PKG_CONFIG_PATH="$(dirname $(find "$PREFIX" -name 'libpipewire-*.pc')):$PKG_CONFIG_PATH" @@ -123,9 +128,9 @@ include: # Build wireplumber - meson "$WP_BUILD_DIR" . --prefix="$PREFIX" $BUILD_OPTIONS - cd "$WP_BUILD_DIR" - - ninja - - ninja test - - ninja install + - ninja $NINJA_ARGS + - ninja $NINJA_ARGS test + - ninja $NINJA_ARGS install artifacts: name: wireplumber-$CI_COMMIT_SHA when: always @@ -213,7 +218,7 @@ build_with_coverity: --comptype gcc --compiler cc --template --xml-option=append_arg@C:--ppp_translator --xml-option=append_arg@C:"replace/GLIB_(DEPRECATED|AVAILABLE)_ENUMERATOR_IN_\d_\d\d(_FOR\(\w+\)|)\s+=/ =" - - cov-build --dir cov-int --config coverity_conf.xml ninja -C "$WP_BUILD_DIR" + - cov-build --dir cov-int --config coverity_conf.xml ninja $NINJA_ARGS -C "$WP_BUILD_DIR" - tar caf wireplumber.tar.gz cov-int - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL diff --git a/NEWS.rst b/NEWS.rst index 0c5b8e9f..fd03c5cf 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,74 @@ -WirePlumber 0.4.5 +WirePlumber 0.4.7 ~~~~~~~~~~~~~~~~~ +Fixes: + + - Fixed a regression in 0.4.6 that caused the selection of the default audio + sources and sinks to be delayed until some event, which effectively caused + losing audio output in many circumstances (#148, #150, #151, #153) + + - Fixed a regression in 0.4.6 that caused the echo-cancellation pipewire + module (and possibly others) to not work + + - A default sink or source is now not selected if there is no available route + for it (#145) + + - Fixed an issue where some clients would wait for a bit while seeking (#146) + + - Fixed audio capture in the endpoints-based policy + + - Fixed an issue that would cause certain lua scripts to error out with older + configuration files (#158) + +Past releases +~~~~~~~~~~~~~ + +WirePlumber 0.4.6 +................. + +Changes: + + - Fixed a lot of race condition bugs that would cause strange crashes or + many log messages being printed when streaming clients would connect and + disconnect very fast (#128, #78, ...) + + - Improved the logic for selecting a default target device (#74) + + - Fixed switching to headphones when the wired headphones are plugged in (#98) + + - Fixed an issue where ``udevadm trigger`` would break wireplumber (#93) + + - Fixed an issue where switching profiles of a device could kill client nodes + + - Fixed briefly switching output to a secondary device when switching device + profiles (#85) + + - Fixed ``wpctl status`` showing default device selections when dealing with + module-loopback virtual sinks and sources (#130) + + - WirePlumber now ignores hidden files from the config directory (#104) + + - Fixed an interoperability issue with jackdbus (pipewire#1846) + + - Fixed an issue where pulseaudio tcp clients would not have permissions to + connect to PipeWire (pipewire#1863) + + - Fixed a crash in the journald logger with NULL debug messages (#124) + + - Enabled real-time priority for the bluetooth nodes to run in RT (#132) + + - Made the default stream volume configurable + + - Scripts are now also looked up in $XDG_CONFIG_HOME/wireplumber/scripts + + - Updated documentation on configuring WirePlumber and fixed some more + documentation issues (#68) + + - Added support for using strings as log level selectors in WIREPLUMBER_DEBUG + +WirePlumber 0.4.5 +................. + Fixes: - Fixed a crash that could happen after a node linking error (#76) @@ -34,9 +102,6 @@ API: - The file-monitor-api plugin can now watch files for changes in addition to directories -Past releases -~~~~~~~~~~~~~ - WirePlumber 0.4.4 ................. diff --git a/docs/rst/configuration.rst b/docs/rst/configuration.rst new file mode 100644 index 00000000..d4e051de --- /dev/null +++ b/docs/rst/configuration.rst @@ -0,0 +1,34 @@ + .. _configuration: + +Configuration +============= + +WirePlumber is a heavily modular daemon. By itself, it doesn't do anything +except load the configured modules. All the rest of the logic is implemented +inside those modules. + +Modular design ensures that it is possible to swap the implementation of +specific functionality without having to re-implement the rest of it, allowing +flexibility on target-sensitive parts, such as policy management and +making use of non-standard hardware. + +At startup, WirePlumber first reads its **main** configuration file. +This file configures the operation context (properties of the daemon, +modules to be loaded, etc). This file may also specify additional, secondary +configuration files which will be loaded as well at the time of parsing the +main file. + +All files and modules are specified relative to their standard search locations, +which are documented later in this chapter. + +.. toctree:: + :maxdepth: 1 + + configuration/locations.rst + configuration/main.rst + configuration/config_lua.rst + configuration/multi_instance.rst + configuration/alsa.rst + configuration/bluetooth.rst + configuration/policy.rst + configuration/access.rst diff --git a/docs/rst/configuration/access.rst b/docs/rst/configuration/access.rst new file mode 100644 index 00000000..d96d8e23 --- /dev/null +++ b/docs/rst/configuration/access.rst @@ -0,0 +1,63 @@ +.. _config_access: + +Access configuration +==================== + +main.lua.d/50-default-access-config.lua +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Using a similar format as the :ref:`ALSA monitor <config_alsa>`, this +configuration file is charged to configure the client objects created by +PipeWire. + +* *default_access.properties* + + A Lua object that contains generic client configuration properties in the + for of key pairs. + + Example: + + .. code-block:: lua + + default_access.properties = { + ["enable-flatpak-portal"] = true, + } + + The above example sets to ``true`` the ``enable-flatpak-portal`` property. + + The list of valid properties are: + + .. code-block:: lua + + ["enable-flatpak-portal"] = true, + + Whether to enable the flatpak portal or not. + +* *default_access.rules* + + This is a Lua array that can contain objects with rules for a client object. + Those Lua objects have 2 properties. Similar to the + :ref:`ALSA configuration <config_alsa>`, the first property is ``matches``, + which allow users to define rules to match a client object. + The second property is ``default_permissions``, and it is used to set + permissions on the matched client object. + + Example: + + .. code-block:: lua + + { + matches = { + { + { "pipewire.access", "=", "flatpak" }, + }, + }, + default_permissions = "rx", + } + + This grants read and execute permissions to all clients that have the + ``pipewire.access`` property set to ``flatpak``. + + Possible permissions are any combination of ``r``, ``w`` and ``x`` for read, + write and execute; or ``all`` for all kind of permissions. + diff --git a/docs/rst/configuration/alsa.rst b/docs/rst/configuration/alsa.rst new file mode 100644 index 00000000..122c8593 --- /dev/null +++ b/docs/rst/configuration/alsa.rst @@ -0,0 +1,456 @@ +.. _config_alsa: + +ALSA configuration +================== + +Modifying the default configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +ALSA devices are created and managed by the session manager with the *alsa.lua* +monitor script. In the default configuration, this script is loaded by +``main.lua.d/30-alsa-monitor.lua``, which also specifies an ``alsa_monitor`` +global table that can be filled in with properties and rules in subsequent +config files. By default, these are filled in ``main.lua.d/50-alsa-config.lua``. + +The ``alsa_monitor`` global table has 2 sub-tables: + +* *alsa_monitor.properties* + + This is a simple Lua table that has key value pairs used as properties. + + Example: + + .. code-block:: lua + + alsa_monitor.properties = { + ["alsa.jack-device"] = false, + ["alsa.reserve"] = true, + } + + The above example will configure the ALSA monitor to not enable the JACK + device, and do ALSA device reservation using the mentioned DBus interface. + + A list of valid properties are: + + .. code-block:: lua + + ["alsa.jack-device"] = false + + 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. + + .. code-block:: lua + + ["alsa.reserve"] = true + + Reserve ALSA devices via *org.freedesktop.ReserveDevice1* on D-Bus. + + .. code-block:: lua + + ["alsa.reserve.priority"] = -20 + + The used ALSA device reservation priority. + + .. code-block:: lua + + ["alsa.reserve.application-name"] = "WirePlumber" + + The used ALSA device reservation application name. + + +* *alsa_monitor.rules* + + This is a Lua array that can contain objects with rules for a device or node. + Those objects have 2 properties. The first one is ``matches``, which allow + users to define rules to match a device or node. The second property is + ``apply_properties``, and it is used to apply properties on the matched object. + + Example: + + .. code-block:: lua + + alsa_monitor.rules = { + matches = { + { + { "device.name", "matches", "alsa_card.*" }, + }, + }, + apply_properties = { + ["api.alsa.use-acp"] = true, + } + } + + This sets the API ALSA use ACP property to all devices with a name that + matches the ``alsa_card.*`` pattern. + + The ``matches`` section is an array of arrays. On the first level, the rules + are ORed together, so any rule match is going to apply the properties. On + the second level, the rules are merged with AND, so they must all match. + + Example: + + .. code-block:: lua + + matches = { + { + { "node.name", "matches", "alsa_input.*" }, + { "alsa.driver_name", "equals", "snd_hda_intel" }, + }, + { + { "node.name", "matches", "alsa_output.*" }, + }, + }, + + This is equivalent to the following logic, in pseudocode: + + .. code-block:: + + if ("node.name" MATCHES "alsa_input.*" AND "alsa.driver_name" EQUALS "snd_hda_intel" ) + OR + ("node.name" MATCHES "alsa_output.*") + then + ... apply the properties ... + end + + As you can notice, the individual rules are themselves also lua arrays. The + first element is a property name (ex "node.name"), the second element is a + verb and the third element is an expected value, which depends on the verb. + Internally, this uses the ``Constraint`` API, which is documented in the + :ref:`Object Interet API <lua_object_interest_api>` section. All the verbs + that you can use on ``Constraint`` are also allowed here. + + .. note:: + + When using the "matches" verb, the values are not complete regular expressions. + They are wildcard patterns, which means that '*' matches an arbitrary, + possibly empty, string and '?' matches an arbitrary character. + + All the possible properties that you can apply to devices and nodes of the + ALSA monitor are described in the sections below. + +Device properties +^^^^^^^^^^^^^^^^^ + +PipeWire devices correspond to the ALSA cards. +The following properties can be configured on devices created by the monitor: + +.. code-block:: lua + + ["api.alsa.use-acp"] = true + +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. + +.. code-block:: lua + + ["api.alsa.use-ucm"] = true + +By default, the UCM configuration is used when it is available for your device. +With this option you can disable this and use the ACP profiles instead. + +.. code-block:: lua + + ["api.alsa.soft-mixer"] = false + +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. + +.. code-block:: lua + + ["api.alsa.ignore-dB"] = false + +Setting this option to true will ignore the decibel setting configured by the +driver. Use this when the driver reports wrong settings. + +.. code-block:: lua + + ["device.profile-set"] = "profileset-name" + +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. + +.. code-block:: lua + + ["device.profile"] = "default profile name" + +The default active profile name. + +.. code-block:: lua + + ["api.acp.auto-profile"] = false + +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. + +.. code-block:: lua + + ["api.acp.auto-port"] = false + +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. + +Some of the other properties that might be configured on devices: + +.. code-block:: lua + + ["device.nick"] = "My Device", + ["device.description"] = "My Device" + +``device.description`` will show up in most apps when a device name is shown. + +Node Properties +^^^^^^^^^^^^^^^ + +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: + +.. code-block:: lua + + ["priority.driver"] = 2000 + +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. + +.. code-block:: lua + + ["priority.session"] = 100 + +This configures the priority of the device when selecting a default device. +Higher priority devices will be more likely candidates as a default device. + +.. code-block:: lua + + ["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:: lua + + ["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:: lua + + ["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:: lua + + ["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:: lua + + ["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:: lua + + ["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:: lua + + ["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:: lua + + ["channelmix.normalize"] = true + +Makes sure that during such mixing & resampling original 0 dB level is +preserved, so nothing sounds wildly quieter/louder. + +.. code-block:: lua + + ["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:: lua + + ["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 +device + +.. code-block:: lua + + ["api.alsa.period-size"] = 1024 + +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. + +.. code-block:: lua + + ["api.alsa.headroom"] = 0 + +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: + + + ============== ========================================== ========= + 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 + ============== ========================================== ========= + +It is possible to disable the batch device tweaks with: + +.. code-block:: lua + + ["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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Extra internal delay in the DAC and ADC converters of the device itself can be +set with the ``latency.internal.*`` properties: + +.. code-block:: lua + + ["latency.internal.rate"] = 256 + ["latency.internal.ns"] = 0 + +You can configure a latency in samples (relative to rate with +``latency.internal.rate``) or in nanoseconds (``latency.internal.ns``). +This value will be added to the total reported latency by the node of the device. + +You can use a tool like ``jack_iodelay`` to get the number of samples of +internal latency of your device. + +This property is also adjustable at runtime with the ``ProcessLatency`` param. +You will need to find the id of the Node you want to change. For example: +Query the current internal latency of an ALSA node with id 58: + +.. code-block:: console + + $ pw-cli e 58 ProcessLatency + Object: size 80, type Spa:Pod:Object:Param:ProcessLatency (262156), id Spa:Enum:ParamId:ProcessLatency (16) + Prop: key Spa:Pod:Object:Param:ProcessLatency:quantum (1), flags 00000000 + Float 0.000000 + Prop: key Spa:Pod:Object:Param:ProcessLatency:rate (2), flags 00000000 + Int 0 + Prop: key Spa:Pod:Object:Param:ProcessLatency:ns (3), flags 00000000 + Long 0 + +Set the internal latency to 256 samples: + +.. code-block:: console + + $ pw-cli s 58 ProcessLatency '{ rate = 256 }' + Object: size 32, type Spa:Pod:Object:Param:ProcessLatency (262156), id Spa:Enum:ParamId:ProcessLatency (16) + Prop: key Spa:Pod:Object:Param:ProcessLatency:rate (2), flags 00000000 + Int 256 + remote 0 node 58 changed + remote 0 port 70 changed + remote 0 port 72 changed + remote 0 port 74 changed + remote 0 port 76 changed + +Startup tweaks +^^^^^^^^^^^^^^ + +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: + +.. code-block:: lua + + ["api.alsa.start-delay"] = 0 + +It is unsure when this tunable should be used. + +IEC958 (S/PDIF) passthrough +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +S/PDIF passthrough will only be enabled when the accepted codecs are configured +on the ALSA device. + +This can be done in 3 different ways: + + 1. Use pavucontrol and toggle the codecs in the output advanced section + + 2. Modify the ``["iec958.codecs"] = "[ PCM DTS AC3 MPEG MPEG2-AAC EAC3 TrueHD DTS-HD ]"`` + node property to something. + + 3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify + the codecs at runtime. diff --git a/docs/rst/configuration/bluetooth.rst b/docs/rst/configuration/bluetooth.rst new file mode 100644 index 00000000..d9290204 --- /dev/null +++ b/docs/rst/configuration/bluetooth.rst @@ -0,0 +1,160 @@ +.. _config_bluetooth: + +Bluetooth configuration +======================= + +Using the same format as the :ref:`ALSA monitor <config_alsa>`, the +configuration file ``bluetooth.lua.d/50-bluez-config.lua`` is charged +to configure the Bluetooth devices and nodes created by WirePlumber. + +* *bluez_monitor.properties* + + A Lua object that contains generic client configuration properties in the + for of key pairs. + + Example: + + .. code-block:: lua + + bluez_monitor.properties = { + ["bluez5.enable-msbc"] = true, + } + + This example will enable the MSBC codec in connected Bluetooth devices that + support it. + + The list of valid properties are: + + .. code-block:: lua + + ["bluez5.enable-sbc-xq"] = true + + Enables the SBC-XQ codec in connected Blueooth devices that support it + + .. code-block:: lua + + ["bluez5.enable-msbc"] = true + + Enables the MSBC codec in connected Blueooth devices that support it + + .. code-block:: lua + + ["bluez5.enable-hw-volume"] = true + + Enables hardware volume controls in Bluetooth devices that support it + + .. code-block:: lua + + ["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:: lua + + ["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:: lua + + ["bluez5.hfphsp-backend"] = "native" + + HFP/HSP backend (default: native). Available values: ``any``, ``none``, + ``hsphfpd``, ``ofono`` or ``native``. + + .. code-block:: lua + + ["bluez5.default.rate"] = 48000 + + The bluetooth default audio rate. + + .. code-block:: lua + + ["bluez5.default.channels"] = 2 + + The bluetooth default number of channels. + +* *bluez_monitor.rules* + + Like in the :ref:`ALSA configuration <config_alsa>`, this is a Lua array that + can contain objects with rules for a Bluetooth device or node. + Those objects have 2 properties. The first one is ``matches``, which allows + users to define rules to match a Bluetooth device or node. + The second property is ``apply_properties``, and it is used to apply + properties on the matched Bluetooth device or node. + + Example: + + .. code-block:: lua + + { + matches = { + { + { "device.name", "matches", "bluez_card.*" }, + }, + }, + apply_properties = { + ["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink ]" + } + } + + 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. + + A list of valid properties are: + + .. code-block:: lua + + ["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink ]" + + 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``. + + .. code-block:: lua + + ["bluez5.hw-volume"] = "[ hfp_ag hsp_ag a2dp_source ]" + + Hardware volume controls (default: ``hfp_ag``, ``hsp_ag``, and ``a2dp_source``) + + Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``, + ``hsp_ag`` and ``a2dp_source``. + + .. code-block:: lua + + ["bluez5.a2dp.ldac.quality"] = "auto" + + 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). + + .. code-block:: lua + + ["bluez5.a2dp.aac.bitratemode"] = 0 + + AAC variable bitrate mode. + + Available values: 0 (cbr, default), 1-5 (quality level). + + .. code-block:: lua + + ["device.profile"] = "a2dp-sink" + + Profile connected first. + + Available values: ``a2dp-sink`` (default) or ``headset-head-unit``. diff --git a/docs/rst/configuration/config_lua.rst b/docs/rst/configuration/config_lua.rst new file mode 100644 index 00000000..d3558c88 --- /dev/null +++ b/docs/rst/configuration/config_lua.rst @@ -0,0 +1,170 @@ +.. _config_lua: + +Lua configuration files +======================= + +Lua configuration files are similar to the main configuration file, but they +leverage the lua language to enable advanced configuration of module arguments +and allow split-file configuration. + +There is only one global section that WirePlumber reads from these files: the +**components** table. This table is equivalent to the **context.components** +object on the main configuration file. Its purpose is to list components that +WirePlumber should load on startup. + +Every line on the **components** table should be another table that contains +information about the loaded component:: + + { + "component-name", + type = "component-type", + args = { additional arguments }, + optional = true/false, + } + +* **"component-name"** should be the name of the component to load + (ex. *"libwireplumber-module-mixer-api"*) + +* **"component-type"** should be the type of the component. + Valid component types include: + + * ``module``: A WirePlumber shared object module + * ``script/lua``: A WirePlumber Lua script + * ``pw_module``: A PipeWire shared object module (loaded on WirePlumber, + not on the PipeWire daemon) + +* **args** is an optional table that can contain additional arguments to be + passed down to the module or script. Scripts can retrieve these arguments + by declaring a line that reads ``local config = ...`` at the top of the script. + Modules receive these arguments as a GVariant ``a{sv}`` table. + +* **optional** is a boolean value that specifies whether loading of this + component is optional. The default value is ``false``. If set to ``true``, + then WirePlumber will not fail loading if the component is not found. + +Split-file configuration +------------------------ + +When a Lua configuration file is loaded, the engine also looks for additional +files in a directory that has the same name as the configuration file and a +``.d`` suffix. + +A Lua directory can contain a list of Lua configuration files. Those files are +loaded alphabetically by filename so that user can control the order in which +Lua configuration files are executed. + +Lua files in the directory are always loaded *after* the configuration file +that is out of the directory. However, it is perfectly valid to not have any +configuration file out of the directory. + +Example hierarchy with files both in and out of the directory +(in the order of loading):: + + config.lua + config.lua.d/00-functions.lua + config.lua.d/01-alsa.lua + config.lua.d/10-policy.lua + config.lua.d/99-misc.lua + +Example hierarchy with files only in the directory +(in the order of loading):: + + config.lua.d/00-functions.lua + config.lua.d/01-alsa.lua + config.lua.d/10-policy.lua + config.lua.d/99-misc.lua + +Multi-path merging +------------------ + +WirePlumber looks for configuration files in 3 different places, as described +in the :ref:`Locations of files <config_locations>` section. When a split-file +configuration scheme is used, files will be merged from these different locations. + +For example, consider these files exist on the filesystem:: + + /usr/share/wireplumber/config.lua.d/00-functions.lua + /usr/share/wireplumber/config.lua.d/01-alsa.lua + /usr/share/wireplumber/config.lua.d/10-policy.lua + /usr/share/wireplumber/config.lua.d/99-misc.lua + ... + /etc/wireplumber/config.lua.d/01-alsa.lua + ... + /home/user/.config/wireplumber/config.lua.d/11-policy-extras.lua + +In this case, loading ``config.lua`` will result in loading these files +(in this order):: + + /usr/share/wireplumber/config.lua.d/00-functions.lua + /etc/wireplumber/config.lua.d/01-alsa.lua + /usr/share/wireplumber/config.lua.d/10-policy.lua + /home/user/.config/wireplumber/config.lua.d/11-policy-extras.lua + /usr/share/wireplumber/config.lua.d/99-misc.lua + +This is useful to keep the default configuration in /usr and override it +with host-specific and user-specific parts in /etc and /home respectively. + +As an exception to this rule, if the configuration path is overridden with +the ``WIREPLUMBER_CONFIG_DIR`` environment variable, then configuration files +will only be loaded from this path and no merging will happen. + +Functions +--------- + +Because of the nature of these files (they are scripts!), it is more convenient +to manage the **components** table through functions. In the default +configuration files shipped with WirePlumber, there is a file called +``00-functions.lua`` that defines some helper functions to load components. + +When loading components through these functions, *duplicate calls are ignored*, +so it is possible to call a function to load a specific component multiple times +and it will only be loaded once. + +.. function:: load_module(module, args) + + Loads a WirePlumber shared object module. + + :param string module: the module name, without the "libwireplumber-module-" + prefix (ex specify "mixer-api" to load "libwireplumber-module-mixer-api") + :param table args: optional module arguments table + +.. function:: load_optional_module(module, args) + + Loads an optional WirePlumber shared object module. Optional in this case + means that if the module is not present on the filesystem, it will be ignored. + + :param string module: the module name, without the "libwireplumber-module-" + prefix (ex specify "mixer-api" to load "libwireplumber-module-mixer-api") + :param table args: optional module arguments table + +.. function:: load_pw_module(module) + + Loads a PipeWire shared object module + + :param string module: the module name, without the "libpipewire-module-" + prefix (ex specify "adapter" to load "libpipewire-module-adapter") + +.. function:: load_script(script, args) + + Loads a Lua script (a functionality script, not a lua configuration file) + + :param string script: the script's filename (ex. "policy-node.lua") + :param table args: optional script arguments table + +.. function:: load_monitor(monitor, args) + + Loads a Lua monitor script. Monitors are scripts found in the ``monitors/`` + directory and their purpose is to monitor and load devices. + + :param string monitor: the scripts's name without the directory or the .lua + extension (ex. "alsa" will load "monitors/alsa.lua") + :param table args: optional script arguments table + +.. function:: load_access(access, args) + + Loads a Lua access script. Access scripts are ones found in the ``access/`` + directory and their purpose is to manage application permissions. + + :param string access: the scripts's name without the directory or the .lua + extension (ex. "flatpak" will load "access/access-flatpak.lua") + :param table args: optional script arguments table diff --git a/docs/rst/configuration/locations.rst b/docs/rst/configuration/locations.rst new file mode 100644 index 00000000..15c53400 --- /dev/null +++ b/docs/rst/configuration/locations.rst @@ -0,0 +1,94 @@ +.. _config_locations: + +Locations of files +================== + +Location of configuration files +------------------------------- + +WirePlumber's default locations of its configuration files are determined at +compile time by the build system. Typically, those end up being +``$XDG_CONFIG_DIR/wireplumber``, ``/etc/wireplumber``, and +``/usr/share/wireplumber``, in that order of priority. + +In more detail, the latter two are controlled by the ``--sysconfdir`` and ``--datadir`` +meson options. When those are set to an absolute path, such as ``/etc``, the +location of the configuration files is set to be ``$sysconfdir/wireplumber``. +When set to a relative path, such as ``etc``, then the installation prefix (``--prefix``) +is prepended to the path: ``$prefix/$sysconfdir/wireplumber`` + +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 + +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. diff --git a/docs/rst/configuration/main.rst b/docs/rst/configuration/main.rst new file mode 100644 index 00000000..93f06596 --- /dev/null +++ b/docs/rst/configuration/main.rst @@ -0,0 +1,126 @@ +.. _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: + +* *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 <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. + +* *context.components* + + Used to load WirePlumber components. Components can be either WirePlumber + modules written in C, WirePlumber scripts or other configuration + files:: + + { name = <component-name>, type = <component-type> } + + Valid component types include: + + * ``module``: A WirePlumber shared object module + * ``script/lua``: A WirePlumber Lua script + (requires ``libwireplumber-module-lua-scripting``) + * ``config/lua``: A WirePlumber Lua configuration file + (requires ``libwireplumber-module-lua-scripting``) + + Example:: + + context.components = [ + { name = libwireplumber-module-lua-scripting, type = module } + { name = main.lua, type = config/lua } + ] + + This will load the WirePlumber lua-scripting module, dynamically, and then + it will also load any components specified in the ``main.lua`` file. + + .. note:: + + When loading lua configuration files, WirePlumber will also look for + additional files in the directory suffixed with ``.d`` and will load + all of them as well. For example, loading ``example.lua`` will also load + any ``.lua`` files under ``example.lua.d/``. In addition, the presence of the + main file is optional, so it is valid to specify ``example.lua`` in the + component name, while ``example.lua`` doesn't exist, but ``example.lua.d/`` + exists instead and has ``.lua`` files to load. + + For more information about lua configuration files, see the + :ref:`Lua configuration files <config_lua>` section. + diff --git a/docs/rst/configuration/meson.build b/docs/rst/configuration/meson.build new file mode 100644 index 00000000..a4c73e09 --- /dev/null +++ b/docs/rst/configuration/meson.build @@ -0,0 +1,11 @@ +# you need to add here any files you add to the toc directory as well +sphinx_files += files( + 'locations.rst', + 'main.rst', + 'config_lua.rst', + 'multi_instance.rst', + 'alsa.rst', + 'bluetooth.rst', + 'policy.rst', + 'access.rst', +) diff --git a/docs/rst/configuration/multi_instance.rst b/docs/rst/configuration/multi_instance.rst new file mode 100644 index 00000000..b8a0c8ae --- /dev/null +++ b/docs/rst/configuration/multi_instance.rst @@ -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. diff --git a/docs/rst/configuration/policy.rst b/docs/rst/configuration/policy.rst new file mode 100644 index 00000000..84cf49d3 --- /dev/null +++ b/docs/rst/configuration/policy.rst @@ -0,0 +1,116 @@ +.. _config_policy: + +Policy Configuration +==================== + +policy.lua.d/10-default-policy.lua +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This file contains generic default policy properties that can be configured. + +* *default_policy.policy* + + This is a Lua object that contains several properties that change the + behavior of the default WirePlumber policy. + + Example: + + .. code-block:: lua + + default_policy.policy = { + ["move"] = true, + } + + The above example will set the ``move`` policy property to ``true``. + + The list of supported properties are: + + .. code-block:: lua + + ["move"] = true + + Moves session items when metadata ``target.node`` changes. + + .. code-block:: lua + + ["follow"] = true + + Moves session items to the default device when it has changed. + + .. code-block:: lua + + ["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:: lua + + ["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). + +policy.lua.d/50-endpoints-config.lua +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Endpoints are objects that can group multiple clients into different groups or + roles. This is useful if a user wants to apply specific actions when a client + is connected to a particular role/endpoint. This configuration file allows + users to configure those endpoints and their actions. + +* *default_policy.policy.roles* + + This is a Lua array with objects defining the actions of each role. + + Example: + + .. code-block:: lua + + default_policy.policy.roles = { + ["Multimedia"] = { + ["alias"] = { "Movie", "Music", "Game" }, + ["priority"] = 10, + ["action.default"] = "mix", + } + ["Notification"] = { + ["priority"] = 20, + ["action.default"] = "duck", + ["action.Notification"] = "mix", + } + } + + The above example defines actions for both ``Multimedia`` and ``Notification`` + roles. Since the Notification role has more priority than the Multimedia + role, when a client connects to the Notification endpoint, it will ``duck`` + the volume of all Multimedia clients. If Multiple Notification clients want + to play audio, only the Notifications audio will be mixed. + + Possible values of actions are: ``mix`` (Mixes audio), + ``duck`` (Mixes and lowers the audio volume) or ``cork`` (Pauses audio). + +* *default_policy.policy.endpoints* + + This is a Lua array with objects defining the endpoints that the user wants + to create. + + Example: + + .. code-block:: lua + + default_policy.endpoints = { + ["endpoint.multimedia"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Multimedia", + } + }, + ["endpoint.notifications"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Notification", + } + + This example creates 2 endpoints, with names ``endpoint.multimedia`` and + ``endpoint.notifications``; and assigned roles ``Multimedia`` and ``Notification`` + respectively. Both endpoints have ``Audio/Sink`` media class, and so are only + used for playback. diff --git a/docs/rst/daemon-configuration.rst b/docs/rst/daemon-configuration.rst deleted file mode 100644 index fdb76af1..00000000 --- a/docs/rst/daemon-configuration.rst +++ /dev/null @@ -1,440 +0,0 @@ - .. _daemon-configuration: - -Configuration -============= - -WirePlumber is a heavily modular daemon. By itself, it doesn't do anything -except load the configured modules. All the rest of the logic is implemented -inside those modules. - -Modular design ensures that it is possible to swap the implementation of -specific functionality without having to re-implement the rest of it, allowing -flexibility on target-sensitive parts, such as policy management and -making use of non-standard hardware. - -*wireplumber.conf* ------------------- - -This is WirePlumber's main configuration file. It is read at startup, before -connecting to the PipeWire daemon. Its purpose is to list all the modules -that need to be loaded by WirePlumber. - -The format of this file is custom and resembles a script with commands:: - - *comment* - command parameter1 parameter2 ... - -Lines are executed in the order they appear and each of them executes an -action defined by the command. Lines starting with *#* are treated as comments -and ignored. Possible commands are - -* *add-spa-lib* - - Associates SPA plugin names with the names of the SPA modules that they - can be loaded from. This takes 2 parameters - a name pattern and a library name. - - This actually does not load the SPA plugin, it only calls *pw_core_add_spa_lib* - with the 2 paramteres given as arguments. As a consequence, it is safe to - call this even if the SPA module is not actually installed on the system. - - Example: - :: - - add-spa-lib api.alsa.* alsa/libspa-alsa - - In this example, we let *libpipewire* know that any SPA plugin whose name - starts with *api.alsa.* can be loaded from the SPA module - *alsa/libspa-alsa.so* (relative to the standard SPA modules directory). - -* *load-pipewire-module* - - Loads a *libpipewire* module. This is similar to the *load-module* commands - that would appear on *pipewire.conf*, the configuration file of the PipeWire - daemon. - - This takes at least 1 parameter, the module name, and optionally any module - arguments, in the format that they would be given in *pipewire.conf* - - Format: - :: - - load-pipewire-module module-name some-argument some-property=value - - Example: - :: - - load-pipewire-module libpipewire-module-client-device - - This command 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. - -* *load-module* - - Loads a WirePlumber module. This takes 2 arguments and an optional parameter - block. - - Format: - :: - - load-module ABI module-name { - "parameter" : <"value"> - } - - The *ABI* parameter specifies the binary interface that WirePlumber shall use - to load this module. Currently, the only supported ABI is *C*. It exists to - allow future expansion, writing modules in other languages. - - The *module-name* should be the name of the *.so* file without the *.so* - extension. - - Optionally, if the `load-module` line ends with a `{`, the next lines up to - and including the next matching `}` are treated as a parameter block. - This block essentially is a - `GVariant <https://developer.gnome.org/glib/stable/glib-GVariant.html>`_ - of type - `GVariant format strings <https://developer.gnome.org/glib/stable/gvariant-format-strings.html>`_ - in the - `GVariant Text Format <https://developer.gnome.org/glib/stable/gvariant-text.html>`_. - As a rule of thumb, parameter names in this block must always be strings - enclosed in double quotes, the separation between names and values is done - with the `:` character and values, regardless of their inner type, must always - be enclosed in `<` `>`. - - Note that starting the parameter block on the next line is an error. The - starting brace (`{`) must always be on the `load-module` line. - - Example: - :: - - load-module C libwireplumber-module-monitor { - "alsa": <{"factory": <"api.alsa.enum.udev">, "flags": <["use-adapter"]>}> - } - - Parameters are module-dependent. They are passed as a GVariant in the - module's initialization function and it is up to the module to interpret - their meaning. WirePlumber does not have any reserved parameters. - -Location of configuration files -------------------------------- - -WirePlumber's default locations of its configuration files are determined at -compile time by the build system. Typically, those end up being -`XDG_CONFIG_DIR/wireplumber`, `/etc/wireplumber`, and -`/usr/share/wireplumber`, in that order of priority. - -In more detail, the latter two are controlled by the `--sysconfdir` and `--datadir` -meson options. When those are set to an absolute path, such as `/etc`, the -location of the configuration files is set to be `$sysconfdir/wireplumber`. -When set to a relative path, such as `etc`, then the installation prefix (`--prefix`) -is prepended to the path: `$prefix/$sysconfdir/wireplumber` - -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 `wireplumber.conf` -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 possible to override the lookup path at runtime by passing the -`--config-file` or `-c` option:: - - wireplumber --config-file=src/config/wireplumber.conf - -It is also possible to override the whole configuration directory, so that -all other configuration files are being read from a different location as well, -by setting the `WIREPLUMBER_CONFIG_DIR` environment variable:: - - WIREPLUMBER_CONFIG_DIR=src/config wireplumber - -If `WIREPLUMBER_CONFIG_DIR` is set, the default locations are ignored. - -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.1` (or `/usr/lib/<arch-triplet>/wireplumber-0.1` 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. - -module-monitor -"""""""""""""" - -This module internally loads a SPA "device" object which enumerates all the -devices of a certain subsystem. Then it listens for "node" objects that are -being created by this device and exports them to PipeWire, after adjusting -their properties to provide enough context. - -`module-monitor` does not read any configuration files, however, it supports -configuration through parameters defined in the main `wireplumber.conf`. - -At the top level, each parameter is creating a monitor instance. The paramter -key is considered to be a friendly name for this instance and can be any string. -The value of each such parameter is meant to be a dictionary with parameters -for this instance. Possible instance parameters are - -* `factory` - - A string that specifies the name of the SPA factory that loads the intial - "device" object. - - Well-known factories are - - * "api.alsa.enum.udev" - Discovers ALSA devices via udev - * "api.v4l2.enum.udev" - Discovers V4L2 devices via udev - * "api.bluez5.enum.dbus" - Discovers bluetooth devices by calling bluez5 API via D-Bus - - * `flags` - - An array of strings that enable specific functionality in the monitor. - Possible flags include - - * "use-adapter" - - Instructs the monitor to wrap all the created nodes in an "adapter" - SPA node, which provides automatic port splitting/merging and format/rate - conversion. This should be always enabled for audio device nodes. - - * "local-nodes" - - Instructs the monitor to run all the created nodes locally in in the - WirePlumber process, instead of the default behavior which is to create - the nodes in the PipeWire process. This is useful for bluetooth nodes, - which should run outside of the main PipeWire process for performance - reasons. - - * "activate-devices" - - Instructs the monitor to automatically set the device profile to "On", - so that the nodes are created. If not specified, the profile must be - set externally by the user before any nodes appear. - -module-config-endpoint -"""""""""""""""""""""" - -This module creates endpoints when WirePlumber detects new nodes in the -pipewire graph. Nodes themselves can be created in two ways. -Device modes are being created by "monitors" that watch a specific subsystem -(udev, bluez, etc...) for devices. Client nodes are being created by client -applications that try to stream to/from pipewire. As soon as a node is created, -the `module-config-endpoint` iterates through all the `.endpoint` configuration -files, in the order that is determined by the filename, and tries to match the -node to the node description in the `[match-node]` table. Upon a successful -match, a new endpoint that follows the description in the `[endpoint]` table is -created. - -`*.endpoint` configuration files -"""""""""""""""""""""""""""""""" - -These files are TOML v0.5 files. At the top-level, they must contain exactly -2 tables: `[match-node]` and `[endpoint]` - -The `[match-node]` table contains properties that match a pipewire node that -exists on the graph. Possible fields of this table are - -* `properties` - - This is a TOML array of tables, where each table must contain two fields: - `name` and `value`, both being strings. Each table describes a match against - one of the pipewire properties of the node. For a successful node match, all - the described properties must match with the node. - - The value of the `name` field must match exactly the name of the pipewire - property, while the value of the `value` field can contain '*' (wildcard) - and '?' (joker), adhering to the rules of the - [GLib g_pattern_match() function](https://developer.gnome.org/glib/stable/glib-Glob-style-pattern-matching.html). - - When writing `.endpoint` files, a useful utility that you can use to list - device node properties is:: - - $ wireplumber-cli device-node-props - - Another way to figure out some of these properties *for ALSA nodes* is - by parsing the aplay/arecord output. For example, this line from `aplay -l` - is interpreted as follows:: - - card 0: PCH [HDA Intel PCH], device 2: ALC3246 [ALC3246 Analog] - - { name = "api.alsa.path", value = "hw:0,2" }, - { name = "api.alsa.card", value = "0" }, - { name = "api.alsa.card.id", value = "PCH" }, - { name = "api.alsa.card.name", value = "HDA Intel PCH" }, - { name = "api.alsa.pcm.device", value = "2" }, - { name = "api.alsa.pcm.id", value = "ALC3246" }, - { name = "api.alsa.pcm.name", value = "ALC3246 Analog" } - - The `[endpoint]` table contains a description of the endpoint to be created. - Possible fields of this table are - -* `session` - - Required. A String representing the session name to be used when exporting the - endpoint. - -* `type` - - Required. Specifies the factory to be used for construction. - The only well-known factories at the moment of writing is `si-adapter` and - `si-simple-node-edpoint`. - -* `streams` - - Optional. Specifies the name of a `.streams` file that contains the - descriptions of the streams to create for this endpoint. This currently - specific to the implementation of the `pw-audio-softdsp-endpoint` and might - change in the future. - -* `config` - - Optional. Specifies the configuration table used to configure the endpoint. - This table can have the following entries - - * `name` - - Optional. The name of the newly created endpoint. If not specified, the - endpoint is named after the node (from the `node.name` property of the - node). - - * `media_class` - - Optional. A string that specifies an override for the `media.class` - property of the node. It can be used in special circumstances to declare - that an endpoint is dealing with a different type of data. This is only - useful in combination with a policy implementation that is aware of this - media class. - - * `role` - - Optional. A string representing the role of the endpoint. - - * `priority` - - Optional. An unsigned integer that specifies the order in which endpoints - are chosen by the policy. - - If not specified, the default priority of an endpoint is equal to zero - (i.e. the lowest priority). - - * `enable-control-port` - - Optional. A boolean representing whether the control port should be - enabled on the endpoint or not. - - * `enable-monitor` - - Optional. A boolean representing whether the monitor ports should be - enabled on the endpoint or not. - - * `preferred-n-channels` - - Optional. An unsigned integer that specifies a preference in the number - of audio channels that an audio node should be configured with. Note that - if the node does not support this many channels, it will be configured - with the closest possible number of channels. This is only available - with the `si-adapter` factory. - -`*.streams` configuration file -"""""""""""""""""""""""""""""" - -These files contain lists of streams with their names and priorities. -They are TOML v0.5 files. - -Each `.streams` file must contain exactly one top-level array of tables, -called `streams`. Every table must contain a mandatory `name` field, and 2 -optional fields: `priority` and `enable_control_port`. - -The `name` of each stream is used to create the streams on new endpoints. - -The `priority` of each stream is being interpreted by the policy module to -apply restrictions on which app can use the stream at a given time. - -The `enable_control_port` is used to enable the control port of the stream. - -module-config-policy -"""""""""""""""""""" - -This module implements demo-quality policy management that is partly driven -by configuration files. The configuration files that this module reads are -described below: - -`*.endpoint-link` -""""""""""""""""" - -These files contain rules to link endpoints with each other. -They are TOML v0.5 files. - -Endpoints are normally created by another module, such -as `module-config-endpoint` which is described above. -As soon as an endpoint is created, the `module-config-policy` uses the -information gathered from the `.endpoint-link` files in order to create a -link to another endpoint. - -`.endpoint-link` files can contain 3 top-level tables -* `[match-endpoint]`, required -* `[target-endpoint]`, optional - -The `[match-endpoint]` table contains properties that match an endpoint that -exists on the graph. Possible fields of this table are - -* `name` - - Optional. The name of the endpoint. It is possible to use wildcards here to - match only parts of the name. - -* `media_class` - - Optional. A string that specifies the `media.class` that the endpoint - must have in order to match. - -* `properties` - - This is a TOML array of tables, where each table must contain two fields: - `name` and `value`, both being strings. Each table describes a match against - one of the pipewire properties of the endpoint. For a successful endpoint - match, all the described properties must match with the endpoint. - -The `[target-endpoint]` table contains properties that match an endpoint that -exists on the graph. The purpose of this table is to match a second endpoint -that the original matching endpoint from `[match-endpoint]` will be linked to. -If not specified, `module-config-policy` will look for the session "default" -endpoint for the type of media that the matching endpoint produces or consumes -and will use that as a target. Possible fields of this table are - -* `name`, `media_class`, `properties` - - All these fields are permitted and behave exactly as described above for the - `[match-endpoint]` table. - -* `stream` - - This field specifies a stream name that the link will use on the target - endpoint. If it is not specified, the stream name is acquired from the - `media.role` property of the matching endpoint. If specified, the value of - this field overrides the `media.role`. diff --git a/docs/rst/daemon-logging.rst b/docs/rst/daemon-logging.rst index fe01d974..76f1a496 100644 --- a/docs/rst/daemon-logging.rst +++ b/docs/rst/daemon-logging.rst @@ -10,7 +10,8 @@ Getting debug messages on the command line is a matter of setting the WIREPLUMBER_DEBUG=level:category1,category2,... -``level`` can be a number from 1 to 5 and defines the minimum debug level to show: +``level`` can be one of ``CEWMIDT`` or a numerical log level as listed below. +In either case it defines the minimum debug level to show: 0. critical warnings and fatal errors (``C`` & ``E`` in the log) 1. warnings (``W``) @@ -50,13 +51,13 @@ Show all messages: .. code:: - WIREPLUMBER_DEBUG=5 + WIREPLUMBER_DEBUG=D Show all messages up to the *debug* level (E, C, W, M, I & D), excluding *trace*: .. code:: - WIREPLUMBER_DEBUG=4 + WIREPLUMBER_DEBUG=M Show all messages up to the *message* level (E, C, W & M), excluding *info*, *debug* & *trace* @@ -70,13 +71,13 @@ Show all messages from the wireplumber library: .. code:: - WIREPLUMBER_DEBUG=5:wp-* + WIREPLUMBER_DEBUG=D:wp-* Show all messages from ``wp-registry``, libpipewire and all modules: .. code:: - WIREPLUMBER_DEBUG=5:wp-registry,pw,m-* + WIREPLUMBER_DEBUG=D:wp-registry,pw,m-* Relationship with the GLib log handler & G_MESSAGES_DEBUG --------------------------------------------------------- diff --git a/docs/rst/index.rst b/docs/rst/index.rst index 874f4d58..615a9abb 100644 --- a/docs/rst/index.rst +++ b/docs/rst/index.rst @@ -9,7 +9,7 @@ Table of Contents installing-wireplumber.rst running-wireplumber-daemon.rst - daemon-configuration.rst + configuration.rst daemon-logging.rst .. toctree:: diff --git a/docs/rst/installing-wireplumber.rst b/docs/rst/installing-wireplumber.rst index 55ca48dd..5fb76603 100644 --- a/docs/rst/installing-wireplumber.rst +++ b/docs/rst/installing-wireplumber.rst @@ -8,9 +8,9 @@ Dependencies In order to compile WirePlumber you will need: -* GLib >= 2.58 -* PipeWire 0.3 (>= 0.3.26) -* Lua 5.3 +* GLib >= 2.62 +* PipeWire 0.3 (>= 0.3.43) +* Lua 5.3 or 5.4 Lua is optional in the sense that if it is not found in the system, a bundled version will be built and linked statically with WirePlumber. This is controlled diff --git a/docs/rst/lua_api/lua_introduction.rst b/docs/rst/lua_api/lua_introduction.rst index 606cc816..44b749b4 100644 --- a/docs/rst/lua_api/lua_introduction.rst +++ b/docs/rst/lua_api/lua_introduction.rst @@ -6,15 +6,13 @@ Introduction `Lua <https://www.lua.org/>`_ is a powerful, efficient, lightweight, embeddable scripting language. -WirePlumber uses `Lua version 5.3 <https://www.lua.org/versions.html>`_ to -implement its engine. Another, more recent, version may be considered -in the future, but do note that different Lua versions are not API-compatible -and that will likely also affect WirePlumber's Lua API. +WirePlumber uses `Lua version 5.4 <https://www.lua.org/versions.html>`_ to +implement its engine. For older systems, Lua 5.3 is also supported. There are currently two uses for Lua in WirePlumber: - To implement the scripting engine - - To implement lua-based :ref:`config files <daemon-configuration>` + - To implement lua-based :ref:`config files <config_lua>` This section is only documenting the API of the **scripting engine** @@ -22,7 +20,7 @@ Lua Reference ------------- If you are not familiar with the Lua language and its API, please refer to -the `Lua 5.3 Reference Manual <https://www.lua.org/manual/5.3/manual.html>`_ +the `Lua 5.4 Reference Manual <https://www.lua.org/manual/5.4/manual.html>`_ Sandbox ------- diff --git a/docs/rst/meson.build b/docs/rst/meson.build index 5509d878..cf9a25b2 100644 --- a/docs/rst/meson.build +++ b/docs/rst/meson.build @@ -3,7 +3,7 @@ sphinx_files += files( 'index.rst', 'installing-wireplumber.rst', 'running-wireplumber-daemon.rst', - 'daemon-configuration.rst', + 'configuration.rst', 'daemon-logging.rst', 'contributing.rst', 'community.rst', @@ -15,3 +15,4 @@ sphinx_files += files( subdir('c_api') subdir('lua_api') +subdir('configuration') diff --git a/docs/rst/running-wireplumber-daemon.rst b/docs/rst/running-wireplumber-daemon.rst index 2d5dde7f..db500a15 100644 --- a/docs/rst/running-wireplumber-daemon.rst +++ b/docs/rst/running-wireplumber-daemon.rst @@ -3,11 +3,92 @@ Running the WirePlumber daemon ============================== +Systemd +------- + +WirePlumber comes with a systemd unit, ``wireplumber.service``, which should +be enabled on your user session: + +.. code:: console + + $ systemctl --user --now enable wireplumber + +.. note:: + + On non-systemd systems, you just need to ensure that wireplumber is started + after pipewire. + +Run from the PipeWire source tree +--------------------------------- + +PipeWire's build system comes with an option to build WirePlumber together +with PipeWire and allows executing them together without installing either of +them. + +To make this work, configure PipeWire with the +``-Dsession-managers="[ 'wireplumber' ]"`` option on the meson command line. + +When compiling PipeWire, the build system will now also clone and compile +WirePlumber as a subproject. + +To execute the whole stack without installing, simply execute ``make run`` +after compiling. + +Synopsis: + +.. code:: console + + $ meson -Dsession-managers="[ 'wireplumber' ]" build + $ ninja -C build + $ make run + +Run independently or without installing +--------------------------------------- + +If you wish to debug WirePlumber, it may be useful to run it separately from +PipeWire or run it directly from the source tree without installing. +To do so: + + 1. Ensure that neither *WirePlumber* nor *pipewire-media-session* + are running or started together with PipeWire + + - If any of those is started by systemd, + + - Stop the relevant systemd service, ``wireplumber.service`` + or ``pipewire-media-session.service`` + - Disable that service as well if you intend to restart PipeWire + (so that the session manager is not restarted with it) + + - If any of those is started from pipewire.conf, + + - Kill it, in order to stop it temporarily: ``killall wireplumber`` + or ``killall pipewire-media-session`` + - Comment out with ``#`` the relevant ``{ path = "..." args = "" }`` + line from the ``context.exec`` section in ``pipewire.conf``, + if you intend to restart PipeWire + + 2. Ensure that PipeWire is running + + 3. Without stopping PipeWire, run WirePlumber. + + - if it is installed, execute ``wireplumber`` + - if it is **not** installed, execute ``make run`` in the source tree, + or use the ``wp-uninstalled.sh`` script: + + .. code:: console + + $ ./wp-uninstalled.sh wireplumber + Replacing pipewire-media-session -------------------------------- -PipeWire 0.3 comes with an example session manager (pipewire-media-session) -that you will need to disable and replace with WirePlumber. +Older versions of PipeWire used to be distributed with an example session +manager (pipewire-media-session) that you needed to disable and replace with +WirePlumber. + +.. warning:: + + These instructions are only relevant to older versions of PipeWire systemd ^^^^^^^ @@ -69,40 +150,3 @@ and change the appropriate line in the ``exec`` section: # but it is better to start it as a systemd service. This setup assumes that WirePlumber is *installed* on the target system. - -Run independently or without installing ---------------------------------------- - -If you wish to debug WirePlumber, it may be useful to run it separately from -PipeWire or run it directly from the source tree without installing. -To do so: - - 1. Ensure that neither *WirePlumber* nor *pipewire-media-session* - are running or started together with PipeWire - - - If any of those is started by systemd, - - - Stop the relevant systemd service, ``wireplumber.service`` - or ``pipewire-media-session.service`` - - Disable that service as well if you intend to restart PipeWire - (so that the session manager is not restarted with it) - - - If any of those is started from pipewire.conf, - - - Kill it, in order to stop it temporarily: ``killall wireplumber`` - or ``killall pipewire-media-session`` - - Comment out with ``#`` the relevant ``{ path = "..." args = "" }`` - line from the ``exec`` section in ``pipewire.conf``, - if you intend to restart PipeWire - - 2. Ensure that PipeWire is running - - 3. Without stopping PipeWire, run WirePlumber. - - - if it is installed, execute ``wireplumber`` - - if it is **not** installed, execute ``make run`` in the source tree, - or use the ``wp-uninstalled.sh`` script: - - .. code:: console - - $ ./wp-uninstalled.sh wireplumber diff --git a/lib/wp/core.c b/lib/wp/core.c index 27467067..23f6f35f 100644 --- a/lib/wp/core.c +++ b/lib/wp/core.c @@ -881,13 +881,62 @@ wp_core_timeout_add_closure (WpCore * self, GSource **source, guint timeout_ms, gboolean wp_core_sync (WpCore * self, GCancellable * cancellable, GAsyncReadyCallback callback, gpointer user_data) +{ + return wp_core_sync_closure (self, cancellable, + g_cclosure_new (G_CALLBACK (callback), user_data, NULL)); +} + +static void +invoke_closure (GObject * obj, GAsyncResult * res, gpointer data) +{ + GClosure *closure = data; + GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT }; + g_value_init (&values[0], G_TYPE_OBJECT); + g_value_init (&values[1], G_TYPE_OBJECT); + g_value_set_object (&values[0], obj); + g_value_set_object (&values[1], res); + g_closure_invoke (closure, NULL, 2, values, NULL); + g_value_unset (&values[0]); + g_value_unset (&values[1]); + g_closure_unref (closure); +} + +/*! + * \brief Asks the PipeWire server to invoke the \a closure via an event. + * + * Since methods are handled in-order and events are delivered + * in-order, this can be used as a barrier to ensure all previous + * methods and the resulting events have been handled. + * + * In both success and error cases, \a closure is always invoked. + * Use wp_core_sync_finish() from within the \a closure to determine whether + * the operation completed successfully or if an error occurred. + * + * \ingroup wpcore + * \since 0.4.6 + * \param self the core + * \param cancellable (nullable): a GCancellable to cancel the operation + * \param closure (transfer floating): a closure to invoke when the operation + * is done + * \returns TRUE if the sync operation was started, FALSE if an error + * occurred before returning from this function + */ +gboolean +wp_core_sync_closure (WpCore * self, GCancellable * cancellable, + GClosure * closure) { g_autoptr (GTask) task = NULL; int seq; g_return_val_if_fail (WP_IS_CORE (self), FALSE); + g_return_val_if_fail (closure, FALSE); + + closure = g_closure_ref (closure); + g_closure_sink (closure); + if (G_CLOSURE_NEEDS_MARSHAL (closure)) + g_closure_set_marshal (closure, g_cclosure_marshal_VOID__OBJECT); - task = g_task_new (self, cancellable, callback, user_data); + task = g_task_new (self, cancellable, invoke_closure, closure); if (G_UNLIKELY (!self->pw_core)) { g_warn_if_reached (); diff --git a/lib/wp/core.h b/lib/wp/core.h index 903b54a3..07a6125b 100644 --- a/lib/wp/core.h +++ b/lib/wp/core.h @@ -107,6 +107,10 @@ WP_API gboolean wp_core_sync (WpCore * self, GCancellable * cancellable, GAsyncReadyCallback callback, gpointer user_data); +WP_API +gboolean wp_core_sync_closure (WpCore * self, GCancellable * cancellable, + GClosure * closure); + WP_API gboolean wp_core_sync_finish (WpCore * self, GAsyncResult * res, GError ** error); diff --git a/lib/wp/device.c b/lib/wp/device.c index 9a0b995f..680e4371 100644 --- a/lib/wp/device.c +++ b/lib/wp/device.c @@ -414,7 +414,6 @@ wp_spa_device_activate_execute_step (WpObject * object, struct pw_core *pw_core = wp_core_get_pw_core (core); g_return_if_fail (pw_core); - wp_proxy_watch_bind_error (WP_PROXY (self), WP_TRANSITION (transition)); wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core, SPA_TYPE_INTERFACE_Device, wp_properties_peek_dict (self->properties), @@ -507,7 +506,7 @@ wp_spa_device_deactivate (WpObject * object, WpObjectFeatures features) * Flags: G_SIGNAL_RUN_FIRST * \endparblock * - * \par remove-object + * \par object-removed * \parblock * \code * void diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c index cf39fe1e..07ab47c6 100644 --- a/lib/wp/endpoint.c +++ b/lib/wp/endpoint.c @@ -559,7 +559,6 @@ wp_impl_endpoint_activate_execute_step (WpObject * object, } /* bind */ - wp_proxy_watch_bind_error (WP_PROXY (self), WP_TRANSITION (transition)); wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core, PW_TYPE_INTERFACE_Endpoint, wp_properties_peek_dict (self->immutable_props), diff --git a/lib/wp/global-proxy.c b/lib/wp/global-proxy.c index 33ae03d9..e32a75d1 100644 --- a/lib/wp/global-proxy.c +++ b/lib/wp/global-proxy.c @@ -162,8 +162,6 @@ wp_global_proxy_step_bind (WpObject * object, WpGlobalProxy *self = WP_GLOBAL_PROXY (object); WpGlobalProxyPrivate *priv = wp_global_proxy_get_instance_private (self); - wp_proxy_watch_bind_error (WP_PROXY (self), WP_TRANSITION (transition)); - /* Create the pipewire object if global is NULL */ if (priv->global == NULL && priv->factory_name[0] != '\0') { g_autoptr (WpCore) core = NULL; @@ -390,6 +388,16 @@ wp_global_proxy_bind (WpGlobalProxy * self) if (!priv->global || !priv->global->proxy) return FALSE; + /* + * We can bind only if the WpGlobal will unbind this proxy on removal, so + * assert here that this is so. Why: pw_registry_bind can race with the global + * id being replaced with a different object on server side, so we must rely + * on the registry_global messages to know if the object was replaced. If the + * race occurs, and a wrong object is going to be bound here, what will then + * happen is that WpGlobal will dispose this proxy and no problem arises. + */ + g_return_val_if_fail (priv->global->proxy == self, FALSE); + p = wp_global_bind (priv->global); if (!p) return FALSE; diff --git a/lib/wp/iterator.c b/lib/wp/iterator.c index 6cbbd12d..f86f72d7 100644 --- a/lib/wp/iterator.c +++ b/lib/wp/iterator.c @@ -96,7 +96,6 @@ wp_iterator_new (const WpIteratorMethods *methods, size_t user_size) * \brief Gets the implementation-specific storage of an iterator * \note this only for use by implementations of WpIterator * - * \protected * \ingroup wpiterator * \param self an iterator object * \returns a pointer to the implementation-specific storage area @@ -174,7 +173,7 @@ wp_iterator_next (WpIterator *self, GValue *item) } /*! - * \brief Iterates over all items of the iterator calling a function. + * \brief Fold a function over the items of the iterator. * * \ingroup wpiterator * \param self the iterator @@ -196,7 +195,7 @@ wp_iterator_fold (WpIterator *self, WpIteratorFoldFunc func, GValue *ret, } /*! - * \brief Fold a function over the items of the iterator. + * \brief Iterates over all items of the iterator calling a function. * * \ingroup wpiterator * \param self the iterator diff --git a/lib/wp/log.c b/lib/wp/log.c index 4b9a1509..96d192db 100644 --- a/lib/wp/log.c +++ b/lib/wp/log.c @@ -379,6 +379,19 @@ wp_log_level_is_enabled (GLogLevelFlags log_level) return log_level_index (log_level) <= enabled_level; } +static gint +level_index_from_string (const char *str) +{ + g_return_val_if_fail (str != NULL, 0); + + for (guint i = 0; i < G_N_ELEMENTS (log_level_info); i++) { + if (g_str_equal (str, log_level_info[i].name)) + return i; + } + + return level_index_from_spa (atoi (str)); +} + /*! * \brief Configures the log level and enabled categories * \ingroup wplog @@ -406,7 +419,7 @@ wp_log_set_level (const gchar * level_str) tokens = pw_split_strv (level_str, ":", 2, &n_tokens); /* set the log level */ - enabled_level = level_index_from_spa (atoi (tokens[0])); + enabled_level = level_index_from_string (tokens[0]); /* enable filtering of debug categories */ if (n_tokens > 1) { @@ -496,8 +509,11 @@ wp_log_writer_default (GLogLevelFlags log_level, if (!is_category_enabled(cf.log_domain)) return G_LOG_WRITER_UNHANDLED; + if (G_UNLIKELY (!cf.message)) + cf.message_field->value = cf.message = "(null)"; + /* format the message to include the object */ - if (cf.object_type && cf.message) { + if (cf.object_type) { cf.message_field->value = cf.message = full_message = format_message (&cf); } diff --git a/lib/wp/metadata.c b/lib/wp/metadata.c index 41095875..fc1dd7bb 100644 --- a/lib/wp/metadata.c +++ b/lib/wp/metadata.c @@ -35,7 +35,7 @@ * \code * void * changed_callback (WpMetadata * self, - * guint object, + * guint subject, * gchar * key, * gchar * type, * gchar * value, @@ -472,6 +472,7 @@ wp_metadata_find (WpMetadata * self, guint32 subject, const gchar * key, if (g_strcmp0 (k, key) == 0) { if (type) *type = t; + g_value_unset (&val); return v; } } @@ -663,7 +664,6 @@ wp_impl_metadata_activate_execute_step (WpObject * object, prop_impl = SPA_DICT_INIT_ARRAY(items); props = &prop_impl; } - wp_proxy_watch_bind_error (WP_PROXY (self), WP_TRANSITION (transition)); wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core, PW_TYPE_INTERFACE_Metadata, props, priv->iface, 0) ); diff --git a/lib/wp/node.c b/lib/wp/node.c index e052596f..03163948 100644 --- a/lib/wp/node.c +++ b/lib/wp/node.c @@ -763,7 +763,6 @@ wp_impl_node_activate_execute_step (WpObject * object, struct pw_core *pw_core = wp_core_get_pw_core (core); g_return_if_fail (pw_core); - wp_proxy_watch_bind_error (WP_PROXY (self), WP_TRANSITION (transition)); wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core, PW_TYPE_INTERFACE_Node, NULL, self->pw_impl_node, 0)); diff --git a/lib/wp/object-manager.c b/lib/wp/object-manager.c index 0f859582..bcb34533 100644 --- a/lib/wp/object-manager.c +++ b/lib/wp/object-manager.c @@ -46,6 +46,11 @@ * that match the interests of this WpObjectManager will immediately become * available to get through wp_object_manager_new_iterator() and the * WpObjectManager \c object-added signal will be emitted for all of them. + * However, note that if these objects need to be prepared (to activate some + * features on them), the emission of \c object-added will be delayed. To know + * when it is safe to access the initial set of objects, wait until the + * \c installed signal has been emitted. That signal is emitted asynchronously + * after all the initial objects have been prepared. * * \gproperties * @@ -106,11 +111,11 @@ * Flags: G_SIGNAL_RUN_FIRST * \endparblock * - * \par object-changed + * \par objects-changed * \parblock * \code * void - * object_changed_callback (WpObjectManager * self, + * objects_changed_callback (WpObjectManager * self, * gpointer user_data) * \endcode * @@ -1094,7 +1099,7 @@ expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self) WpGlobal *g = g_ptr_array_index (tmp_globals, i); /* if global was already removed, drop it */ - if (g->flags == 0) + if (g->flags == 0 || g->id == SPA_ID_INVALID) continue; wp_object_manager_add_global (om, g); @@ -1347,6 +1352,9 @@ wp_global_rm_flag (WpGlobal *global, guint rm_flag) /* 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); diff --git a/lib/wp/object.c b/lib/wp/object.c index 3b56d6bf..396fff36 100644 --- a/lib/wp/object.c +++ b/lib/wp/object.c @@ -11,6 +11,7 @@ #include "object.h" #include "log.h" #include "core.h" +#include "error.h" /*! \defgroup wpfeatureactivationtransition WpFeatureActivationTransition */ /*! @@ -466,6 +467,45 @@ wp_object_deactivate (WpObject * self, WpObjectFeatures features) WP_OBJECT_GET_CLASS (self)->deactivate (self, features & priv->ft_active); } +/*! + * \brief Aborts the current object activation by returning a transition error + * if any transitions are pending. + * + * This is usually used to stop any pending activation if an error happened. + * + * \ingroup wpobject + * \param self the object + * \param msg the message used in the transition error + * \since 0.4.6 + */ +void +wp_object_abort_activation (WpObject * self, const gchar *msg) +{ + WpObjectPrivate *priv; + g_autoptr (WpTransition) t = NULL; + + g_return_if_fail (WP_IS_OBJECT (self)); + + priv = wp_object_get_instance_private (self); + + g_clear_pointer (&priv->idle_advnc_source, g_source_unref); + + /* abort ongoing transition if any */ + t = g_weak_ref_get (&priv->ongoing_transition); + if (t) + wp_transition_return_error (t, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_OPERATION_FAILED, + "Object activation aborted: %s", msg)); + + /* abort queued transitions */ + while (!g_queue_is_empty (priv->transitions)) { + WpTransition *next = g_queue_pop_head (priv->transitions); + wp_transition_return_error (next, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_OPERATION_FAILED, + "Object activation aborted: %s", msg)); + } +} + /*! * \brief Allows subclasses to update the currently active features. * @@ -475,7 +515,6 @@ wp_object_deactivate (WpObject * self, WpObjectFeatures features) * * \remark Private method to be called by subclasses only. * - * \protected * \ingroup wpobject * \param self the object * \param activated the features that were activated, or 0 diff --git a/lib/wp/object.h b/lib/wp/object.h index 18173f0e..21d60660 100644 --- a/lib/wp/object.h +++ b/lib/wp/object.h @@ -106,6 +106,9 @@ void wp_object_deactivate (WpObject * self, WpObjectFeatures features); /* for subclasses only */ +WP_API +void wp_object_abort_activation (WpObject * self, const gchar *msg); + WP_API void wp_object_update_features (WpObject * self, WpObjectFeatures activated, WpObjectFeatures deactivated); diff --git a/lib/wp/private/pipewire-object-mixin.c b/lib/wp/private/pipewire-object-mixin.c index 25dbc957..16041802 100644 --- a/lib/wp/private/pipewire-object-mixin.c +++ b/lib/wp/private/pipewire-object-mixin.c @@ -647,8 +647,9 @@ wp_pw_object_mixin_cache_params (WpObject * object, WpObjectFeatures missing) g_object_set_qdata (G_OBJECT (object), activated_features_quark (), GUINT_TO_POINTER (activated)); - wp_core_sync (core, NULL, - (GAsyncReadyCallback) param_cache_features_enabled, object); + wp_core_sync_closure (core, NULL, g_cclosure_new_object ( + G_CALLBACK (param_cache_features_enabled), + G_OBJECT (object))); } void @@ -722,13 +723,14 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update) WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance); WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance); - guint32 change_mask = - G_STRUCT_MEMBER (guint32, update, iface->change_mask_offset); - guint32 process_info_change_mask = + guint64 change_mask = + G_STRUCT_MEMBER (guint64, update, iface->change_mask_offset); + guint64 process_info_change_mask = change_mask & ~(iface->CHANGE_MASK_PROPS | iface->CHANGE_MASK_PARAMS); gpointer old_info = NULL; - wp_debug_object (instance, "info, change_mask:0x%x [%s%s]", change_mask, + wp_debug_object (instance, "info, change_mask:0x%"G_GINT64_MODIFIER"x [%s%s]", + change_mask, (change_mask & iface->CHANGE_MASK_PROPS) ? "props," : "", (change_mask & iface->CHANGE_MASK_PARAMS) ? "params," : ""); @@ -736,7 +738,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update) if (iface->process_info && d->info && process_info_change_mask) { /* copy everything that changed except props and params, for efficiency; process_info() is only interested in variables that are not PROPS & PARAMS */ - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = process_info_change_mask; old_info = iface->update_info (NULL, d->info); } @@ -772,8 +774,6 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update) /* update our info struct */ d->info = iface->update_info (d->info, update); - wp_object_update_features (WP_OBJECT (instance), - WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0); /* update properties */ if (change_mask & iface->CHANGE_MASK_PROPS) { @@ -794,6 +794,9 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update) iface->process_info (instance, old_info, d->info); g_clear_pointer (&old_info, iface->free_info); } + + wp_object_update_features (WP_OBJECT (instance), + WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0); } static gint @@ -844,9 +847,9 @@ wp_pw_object_mixin_impl_add_listener (gpointer instance, spa_hook_list_isolate (&d->hooks, &save, listener, events, data); - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = iface->CHANGE_MASK_ALL; + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = iface->CHANGE_MASK_ALL; iface->emit_info (&d->hooks, d->info); - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = 0; + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = 0; spa_hook_list_join (&d->hooks, &save); return 0; @@ -947,10 +950,10 @@ wp_pw_object_mixin_notify_info (gpointer instance, guint32 change_mask) (change_mask & iface->CHANGE_MASK_PROPS) ? "props," : "", (change_mask & iface->CHANGE_MASK_PARAMS) ? "params," : ""); - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = (change_mask & iface->CHANGE_MASK_ALL); iface->emit_info (&d->hooks, d->info); - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = 0; + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = 0; if (change_mask & iface->CHANGE_MASK_PROPS) g_object_notify (G_OBJECT (instance), "properties"); @@ -988,10 +991,10 @@ wp_pw_object_mixin_notify_params_changed (gpointer instance, guint32 id) /* toggle the serial flag; this notifies that there is a data change */ info->flags ^= SPA_PARAM_INFO_SERIAL; - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = iface->CHANGE_MASK_PARAMS; iface->emit_info (&d->hooks, d->info); - G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = 0; + G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = 0; if (subscribed) wp_pw_object_mixin_impl_enum_params (instance, 1, id, 0, -1, NULL); diff --git a/lib/wp/private/pipewire-object-mixin.h b/lib/wp/private/pipewire-object-mixin.h index 202a5b28..c1761242 100644 --- a/lib/wp/private/pipewire-object-mixin.h +++ b/lib/wp/private/pipewire-object-mixin.h @@ -54,9 +54,9 @@ struct _WpPwObjectMixinPrivInterface gsize param_info_offset; gsize n_params_offset; - guint32 CHANGE_MASK_ALL; - guint32 CHANGE_MASK_PROPS; - guint32 CHANGE_MASK_PARAMS; + guint64 CHANGE_MASK_ALL; + guint64 CHANGE_MASK_PROPS; + guint64 CHANGE_MASK_PARAMS; gpointer (*update_info) (gpointer info, gconstpointer update); void (*free_info) (gpointer info); diff --git a/lib/wp/proxy.c b/lib/wp/proxy.c index eebffd90..899d81e5 100644 --- a/lib/wp/proxy.c +++ b/lib/wp/proxy.c @@ -133,6 +133,9 @@ proxy_event_destroy (void *data) spa_hook_remove (&priv->listener); priv->pw_proxy = NULL; wp_object_update_features (WP_OBJECT (self), 0, WP_PROXY_FEATURE_BOUND); + + wp_object_abort_activation (WP_OBJECT (self), "PipeWire proxy destroyed"); + g_signal_emit (self, signals[SIGNAL_PW_PROXY_DESTROYED], 0); } @@ -157,9 +160,18 @@ static void proxy_event_error (void *data, int seq, int res, const char *message) { WpProxy *self = WP_PROXY (data); + WpProxyPrivate *priv = wp_proxy_get_instance_private (self); wp_trace_object (self, "error seq:%d res:%d (%s) %s", seq, res, spa_strerror(res), message); + + /* we destroy the proxy on error if feature bound is still not enabled */ + if (priv->pw_proxy && + !(wp_object_get_active_features (WP_OBJECT (self)) & WP_PROXY_FEATURE_BOUND)) + pw_proxy_destroy (priv->pw_proxy); + + wp_object_abort_activation (WP_OBJECT (self), message); + g_signal_emit (self, signals[SIGNAL_ERROR], 0, seq, res, message); } @@ -176,6 +188,17 @@ wp_proxy_init (WpProxy * self) { } +static void +wp_proxy_dispose (GObject * object) +{ + WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY (object)); + + if (priv->pw_proxy) + pw_proxy_destroy (priv->pw_proxy); + + G_OBJECT_CLASS (wp_proxy_parent_class)->dispose (object); +} + static void wp_proxy_get_property (GObject * object, guint property_id, GValue * value, GParamSpec * pspec) @@ -213,6 +236,7 @@ wp_proxy_class_init (WpProxyClass * klass) WpObjectClass *wpobject_class = (WpObjectClass *) klass; object_class->get_property = wp_proxy_get_property; + object_class->dispose = wp_proxy_dispose; wpobject_class->deactivate = wp_proxy_deactivate; @@ -338,46 +362,3 @@ wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy) /* inform subclasses and listeners */ g_signal_emit (self, signals[SIGNAL_PW_PROXY_CREATED], 0, priv->pw_proxy); } - -static void -bind_error (WpProxy * proxy, int seq, int res, const gchar *msg, - WpTransition * transition) -{ - WpProxyPrivate *priv = wp_proxy_get_instance_private (proxy); - - if (priv->pw_proxy) - pw_proxy_destroy (priv->pw_proxy); - - wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY, - WP_LIBRARY_ERROR_OPERATION_FAILED, "%s", msg)); -} - -static void -bind_success (WpProxy * proxy, uint32_t global_id, WpTransition * transition) -{ - g_signal_handlers_disconnect_by_func (proxy, bind_error, transition); - g_signal_handlers_disconnect_by_func (proxy, bind_success, transition); -} - -/*! - * \brief Private method to be used by subclasses to watch for errors - * during a \a transition that leads the proxy to become bound - * - * If an error is signaled on the proxy before the \c bound signal is emitted, - * the transition will return an error. - * - * \param proxy the proxy - * \param transition the transition that binds the proxy - * \ingroup wpproxy - */ -void -wp_proxy_watch_bind_error (WpProxy * proxy, WpTransition * transition) -{ - g_return_if_fail (WP_IS_PROXY (proxy)); - g_return_if_fail (WP_IS_TRANSITION (transition)); - - g_signal_connect_object (proxy, "error", G_CALLBACK (bind_error), - transition, 0); - g_signal_connect_object (proxy, "bound", G_CALLBACK (bind_success), - transition, 0); -} diff --git a/lib/wp/proxy.h b/lib/wp/proxy.h index 17187012..f6a6c43d 100644 --- a/lib/wp/proxy.h +++ b/lib/wp/proxy.h @@ -100,9 +100,6 @@ struct pw_proxy * wp_proxy_get_pw_proxy (WpProxy * self); WP_API void wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy); -WP_API -void wp_proxy_watch_bind_error (WpProxy * proxy, WpTransition * transition); - G_END_DECLS #endif diff --git a/lib/wp/spa-pod.c b/lib/wp/spa-pod.c index c1c22572..bd7b8876 100644 --- a/lib/wp/spa-pod.c +++ b/lib/wp/spa-pod.c @@ -2332,6 +2332,10 @@ wp_spa_pod_builder_add_valist (WpSpaPodBuilder *self, va_list args) } break; } + case 'b': + spa_pod_builder_bool(&self->builder, + va_arg(args, gboolean) ? true : false); + break; default: SPA_POD_BUILDER_COLLECT(&self->builder, *format, args); break; @@ -2778,6 +2782,10 @@ wp_spa_pod_parser_get_valist (WpSpaPodParser *self, va_list args) } break; } + case 'b': + *va_arg(args, gboolean*) = + SPA_POD_VALUE(struct spa_pod_bool, pod) ? TRUE : FALSE; + break; default: SPA_POD_PARSER_COLLECT (pod, *format, args); break; diff --git a/lib/wp/transition.c b/lib/wp/transition.c index 97bd455f..a8571640 100644 --- a/lib/wp/transition.c +++ b/lib/wp/transition.c @@ -33,7 +33,7 @@ * possible, the WpTransition base class does not expect * _WpTransitionClass::execute_step() to call wp_transition_advance() directly. * Instead, it is expected that wp_transition_advance() will be called from - * the callback that the step's asyncrhonous operation will call when it is + * the callback that the step's asynchronous operation will call when it is * completed. * * \gproperties diff --git a/lib/wp/wp.c b/lib/wp/wp.c index ffdff1a2..c356133e 100644 --- a/lib/wp/wp.c +++ b/lib/wp/wp.c @@ -315,6 +315,9 @@ wp_new_files_iterator (WpLookupDirs dirs, const gchar *subdir, if (dir) { const gchar *filename; while ((filename = g_dir_read_name (dir))) { + if (filename[0] == '.') + continue; + if (suffix && !g_str_has_suffix (filename, suffix)) continue; diff --git a/meson.build b/meson.build index 3acaa312..1f9c9d8d 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('wireplumber', ['c'], - version : '0.4.5', + version : '0.4.7', license : 'MIT', meson_version : '>= 0.56.0', default_options : [ @@ -33,7 +33,7 @@ gmodule_dep = dependency('gmodule-2.0', version : glib_req_version) gio_dep = dependency('gio-2.0', version : glib_req_version) giounix_dep = dependency('gio-unix-2.0', version : glib_req_version) spa_dep = dependency('libspa-0.2', version: '>= 0.2') -pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.3.37') +pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.3.43') mathlib = cc.find_library('m') threads_dep = dependency('threads') @@ -138,3 +138,16 @@ wireplumber_uninstalled = custom_target('wp-uninstalled', build_by_default : true, command : ['cp', '@INPUT@', '@OUTPUT@'], ) + +if meson.version().version_compare('>= 0.58') + builddir = meson.project_build_root() + srcdir = meson.project_source_root() + + devenv = environment({ + 'WIREPLUMBER_MODULE_DIR': builddir / 'modules', + 'WIREPLUMBER_CONFIG_DIR': srcdir / 'src' / 'config', + 'WIREPLUMBER_DATA_DIR': srcdir / 'src', + }) + + meson.add_devenv(devenv) +endif diff --git a/modules/module-default-nodes-api.c b/modules/module-default-nodes-api.c index ef5f4f86..16b74c8d 100644 --- a/modules/module-default-nodes-api.c +++ b/modules/module-default-nodes-api.c @@ -16,7 +16,6 @@ struct _WpDefaultNodesApi gchar *defaults[N_DEFAULT_NODES]; WpObjectManager *om; - GSource *idle_source; }; enum { @@ -36,15 +35,18 @@ wp_default_nodes_api_init (WpDefaultNodesApi * self) { } -static gboolean -emit_changed_cb (gpointer data) +static void +sync_changed_notification (WpCore * core, GAsyncResult * res, + WpDefaultNodesApi * self) { - WpDefaultNodesApi *self = data; + g_autoptr (GError) error = NULL; + if (!wp_core_sync_finish (core, res, &error)) { + wp_warning_object (self, "core sync error: %s", error->message); + return; + } g_signal_emit (self, signals[SIGNAL_CHANGED], 0); - - g_clear_pointer (&self->idle_source, g_source_unref); - return G_SOURCE_REMOVE; + return; } static void @@ -52,7 +54,8 @@ schedule_changed_notification (WpDefaultNodesApi *self) { g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); g_return_if_fail (core); - wp_core_idle_add (core, &self->idle_source, emit_changed_cb, self, NULL); + wp_core_sync_closure (core, NULL, g_cclosure_new_object ( + G_CALLBACK (sync_changed_notification), G_OBJECT (self))); } static void @@ -141,10 +144,6 @@ wp_default_nodes_api_disable (WpPlugin * plugin) { WpDefaultNodesApi * self = WP_DEFAULT_NODES_API (plugin); - if (self->idle_source) - g_source_destroy (self->idle_source); - g_clear_pointer (&self->idle_source, g_source_unref); - for (guint i = 0; i < N_DEFAULT_NODES; i++) g_clear_pointer (&self->defaults[i], g_free); g_clear_object (&self->om); @@ -161,14 +160,22 @@ wp_default_nodes_api_get_default_node (WpDefaultNodesApi * self, break; } } + if (node_t != -1 && self->defaults[node_t]) { - g_autoptr (WpNode) node = wp_object_manager_lookup (self->om, + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) val = G_VALUE_INIT; + it = wp_object_manager_new_filtered_iterator (self->om, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_NAME, "=s", self->defaults[node_t], NULL); - if (node) - return wp_proxy_get_bound_id (WP_PROXY (node)); + for (; wp_iterator_next (it, &val); g_value_unset (&val)) { + WpNode *node = g_value_get_object (&val); + const gchar *mc = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), PW_KEY_MEDIA_CLASS); + if (!g_str_has_prefix (mc, "Stream/")) + return wp_proxy_get_bound_id (WP_PROXY (node)); + } } return SPA_ID_INVALID; } diff --git a/modules/module-default-nodes.c b/modules/module-default-nodes.c index ce619d25..ac9a87bd 100644 --- a/modules/module-default-nodes.c +++ b/modules/module-default-nodes.c @@ -8,6 +8,7 @@ #include <wp/wp.h> #include <errno.h> +#include <pipewire/pipewire.h> #include <pipewire/keys.h> #define COMPILING_MODULE_DEFAULT_NODES 1 @@ -37,9 +38,8 @@ struct _WpDefaultNodes WpState *state; WpDefaultNode defaults[N_DEFAULT_NODES]; WpObjectManager *metadata_om; - WpObjectManager *nodes_om; + WpObjectManager *rescan_om; GSource *timeout_source; - GSource *idle_source; /* properties */ guint save_interval_ms; @@ -98,57 +98,200 @@ timer_start (WpDefaultNodes *self) } } +static gboolean +node_has_available_routes (WpDefaultNodes * self, WpNode *node) +{ + const gchar *dev_id_str = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), PW_KEY_DEVICE_ID); + const gchar *cpd_str = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), "card.profile.device"); + gint dev_id = dev_id_str ? atoi (dev_id_str) : -1; + gint cpd = cpd_str ? atoi (cpd_str) : -1; + g_autoptr (WpDevice) device = NULL; + + if (dev_id == -1 || cpd == -1) + return TRUE; + + /* Get the device */ + device = wp_object_manager_lookup (self->rescan_om, WP_TYPE_DEVICE, + WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=i", dev_id, NULL); + if (!device) + return TRUE; + + /* Check if the current device route supports the node card device profile */ + { + g_autoptr (WpIterator) routes = NULL; + g_auto (GValue) val = G_VALUE_INIT; + routes = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (device), + "Route", NULL); + for (; wp_iterator_next (routes, &val); g_value_unset (&val)) { + WpSpaPod *route = g_value_get_boxed (&val); + gint route_device = -1; + guint32 route_avail = SPA_PARAM_AVAILABILITY_unknown; + + if (!wp_spa_pod_get_object (route, NULL, + "device", "i", &route_device, + "available", "?I", &route_avail, + NULL)) + continue; + + if (route_device != cpd) + continue; + + if (route_avail == SPA_PARAM_AVAILABILITY_no) + return FALSE; + + return TRUE; + } + } + + /* Check if available routes support the node card device profile */ + { + g_autoptr (WpIterator) routes = NULL; + g_auto (GValue) val = G_VALUE_INIT; + routes = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (device), + "EnumRoute", NULL); + for (; wp_iterator_next (routes, &val); g_value_unset (&val)) { + WpSpaPod *route = g_value_get_boxed (&val); + guint32 route_avail = SPA_PARAM_AVAILABILITY_unknown; + g_autoptr (WpSpaPod) route_devices = NULL; + + if (!wp_spa_pod_get_object (route, NULL, + "available", "?I", &route_avail, + "devices", "?P", &route_devices, + NULL)) + continue; + + { + g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (route_devices); + g_auto (GValue) v = G_VALUE_INIT; + for (; wp_iterator_next (it, &v); g_value_unset (&v)) { + gint32 *d = (gint32 *)g_value_get_pointer (&v); + if (d && *d == cpd) { + if (route_avail != SPA_PARAM_AVAILABILITY_no) + return TRUE; + } + } + } + } + } + + return FALSE; +} + static WpNode * -find_highest_priority_node (WpDefaultNodes * self, gint node_t) +find_best_media_class_node (WpDefaultNodes * self, const gchar *media_class, + const gchar *node_name, WpDirection direction, gint *priority) { g_autoptr (WpIterator) it = NULL; g_auto (GValue) val = G_VALUE_INIT; gint highest_prio = 0; WpNode *res = NULL; - it = wp_object_manager_new_filtered_iterator (self->nodes_om, WP_TYPE_NODE, - WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", MEDIA_CLASS_MATCH[node_t], - WP_CONSTRAINT_TYPE_G_PROPERTY, N_PORTS_KEY[node_t], "!u", 0, + g_return_val_if_fail (media_class, NULL); + + it = wp_object_manager_new_filtered_iterator (self->rescan_om, WP_TYPE_NODE, + WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "=s", media_class, NULL); for (; wp_iterator_next (it, &val); g_value_unset (&val)) { WpNode *node = g_value_get_object (&val); - const gchar *prio_str = wp_pipewire_object_get_property ( - WP_PIPEWIRE_OBJECT (node), PW_KEY_PRIORITY_SESSION); - gint prio = prio_str ? atoi (prio_str) : -1; + gint n_ports = direction == WP_DIRECTION_INPUT ? + wp_node_get_n_input_ports (node, NULL) : + wp_node_get_n_output_ports (node, NULL); + if (n_ports > 0) { + const gchar *name = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME); + const gchar *prio_str = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), PW_KEY_PRIORITY_SESSION); + gint prio = prio_str ? atoi (prio_str) : -1; + + if (!node_has_available_routes (self, node)) + continue; + + if (name && node_name && g_strcmp0 (name, node_name) == 0) + prio += 10000; + + if (prio > highest_prio || res == NULL) { + highest_prio = prio; + res = node; + } + } + } + + if (priority) + *priority = highest_prio; + return res; +} - if (prio > highest_prio || res == NULL) { +static WpNode * +find_best_media_classes_node (WpDefaultNodes * self, + const gchar **media_classes, const gchar *node_name, WpDirection direction) +{ + gint highest_prio = -1; + WpNode *res = NULL; + for (guint i = 0; media_classes[i]; i++) { + gint prio = -1; + WpNode *node = find_best_media_class_node (self, media_classes[i], + node_name, direction, &prio); + if (node && (!res || prio > highest_prio)) { highest_prio = prio; res = node; } } - return res ? g_object_ref (res) : NULL; + return res; +} + +static WpNode * +find_best_node (WpDefaultNodes * self, gint node_t) +{ + const gchar *name = self->defaults[node_t].config_value; + + switch (node_t) { + case AUDIO_SINK: { + const gchar *media_classes[] = { + "Audio/Sink", + "Audio/Duplex", + NULL}; + return find_best_media_classes_node (self, media_classes, name, + WP_DIRECTION_INPUT); + } + case AUDIO_SOURCE: { + const gchar *media_classes[] = { + "Audio/Source", + "Audio/Source/Virtual", + "Audio/Duplex", + "Audio/Sink", + NULL}; + return find_best_media_classes_node (self, media_classes, name, + WP_DIRECTION_OUTPUT); + } + case VIDEO_SOURCE: { + const gchar *media_classes[] = { + "Video/Source", + "Video/Source/Virtual", + NULL}; + return find_best_media_classes_node (self, media_classes, name, + WP_DIRECTION_OUTPUT); + } + default: + break; + } + + return NULL; } static void reevaluate_default_node (WpDefaultNodes * self, WpMetadata *m, gint node_t) { - g_autoptr (WpNode) node = NULL; + WpNode *node = NULL; const gchar *node_name = NULL; gchar buf[1024]; - /* Find the configured default node */ - node_name = self->defaults[node_t].config_value; - if (node_name) { - node = wp_object_manager_lookup (self->nodes_om, WP_TYPE_NODE, - WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_NAME, "=s", node_name, - WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", MEDIA_CLASS_MATCH[node_t], - WP_CONSTRAINT_TYPE_G_PROPERTY, N_PORTS_KEY[node_t], "!u", 0, - NULL); - } - - /* If not found, get the highest priority one */ - if (!node) { - node = find_highest_priority_node (self, node_t); - if (node) - node_name = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node), - PW_KEY_NODE_NAME); - } + node = find_best_node (self, node_t); + if (node) + node_name = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node), + PW_KEY_NODE_NAME); /* store it in the metadata if it was changed */ if (node && node_name && @@ -162,89 +305,215 @@ reevaluate_default_node (WpDefaultNodes * self, WpMetadata *m, gint node_t) g_snprintf (buf, sizeof(buf), "{ \"name\": \"%s\" }", node_name); wp_metadata_set (m, 0, DEFAULT_KEY[node_t], "Spa:String:JSON", buf); + } else if (!node && self->defaults[node_t].value) { + g_clear_pointer (&self->defaults[node_t].value, g_free); + wp_info_object (self, "unset default node for %s", NODE_TYPE_STR[node_t]); + wp_metadata_set (m, 0, DEFAULT_KEY[node_t], NULL, NULL); } } -static void -on_metadata_changed (WpMetadata *m, guint32 subject, - const gchar *key, const gchar *type, const gchar *value, gpointer d) +static guint +get_device_total_nodes (WpPipewireObject * proxy) { - WpDefaultNodes * self = WP_DEFAULT_NODES (d); - gint node_t = -1; - gchar name[1024]; - - if (subject == 0) { - for (gint i = 0; i < N_DEFAULT_NODES; i++) { - if (!g_strcmp0 (key, DEFAULT_CONFIG_KEY[i])) { - node_t = i; - break; + g_autoptr (WpIterator) profiles = NULL; + g_auto (GValue) item = G_VALUE_INIT; + + profiles = wp_pipewire_object_enum_params_sync (proxy, "Profile", NULL); + if (!profiles) + return 0; + + for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { + WpSpaPod *pod = g_value_get_boxed (&item); + gint idx = -1; + const gchar *name = NULL; + g_autoptr (WpSpaPod) classes = NULL; + + /* Parse */ + if (!wp_spa_pod_get_object (pod, NULL, + "index", "i", &idx, + "name", "s", &name, + "classes", "?P", &classes, + NULL)) + continue; + if (!classes) + continue; + + /* Parse profile classes */ + { + g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (classes); + g_auto (GValue) v = G_VALUE_INIT; + gint total_nodes = 0; + for (; wp_iterator_next (it, &v); g_value_unset (&v)) { + WpSpaPod *entry = g_value_get_boxed (&v); + g_autoptr (WpSpaPodParser) pp = NULL; + const gchar *media_class = NULL; + gint n_nodes = 0; + g_return_val_if_fail (entry, 0); + if (!wp_spa_pod_is_struct (entry)) + continue; + pp = wp_spa_pod_parser_new_struct (entry); + g_return_val_if_fail (pp, 0); + g_return_val_if_fail (wp_spa_pod_parser_get_string (pp, &media_class), 0); + g_return_val_if_fail (wp_spa_pod_parser_get_int (pp, &n_nodes), 0); + wp_spa_pod_parser_end (pp); + + total_nodes += n_nodes; } + + if (total_nodes > 0) + return total_nodes; } } - if (node_t != -1) { - g_clear_pointer (&self->defaults[node_t].config_value, g_free); + return 0; +} - if (value && !g_strcmp0 (type, "Spa:String:JSON") && - json_object_find (value, "name", name, sizeof(name)) == 0) - { - self->defaults[node_t].config_value = g_strdup (name); - } +static gboolean +nodes_ready (WpDefaultNodes * self) +{ + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) val = G_VALUE_INIT; - wp_debug_object (m, "changed '%s' -> '%s'", key, - self->defaults[node_t].config_value); + /* Get the total number of nodes for each device and make sure they exist + * and have at least 1 port */ + it = wp_object_manager_new_filtered_iterator (self->rescan_om, + WP_TYPE_DEVICE, NULL); + for (; wp_iterator_next (it, &val); g_value_unset (&val)) { + WpPipewireObject *device = g_value_get_object (&val); + guint total_nodes = get_device_total_nodes (device); + if (total_nodes > 0) { + guint32 device_id = wp_proxy_get_bound_id (WP_PROXY (device)); + g_autoptr (WpIterator) node_it = NULL; + g_auto (GValue) node_val = G_VALUE_INIT; + guint ready_nodes = 0; + + node_it = wp_object_manager_new_filtered_iterator (self->rescan_om, + WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + PW_KEY_DEVICE_ID, "=i", device_id, NULL); + for (; wp_iterator_next (node_it, &node_val); g_value_unset (&node_val)) { + WpPipewireObject *node = g_value_get_object (&node_val); + g_autoptr (WpPort) port = + wp_object_manager_lookup (self->rescan_om, + WP_TYPE_PORT, WP_CONSTRAINT_TYPE_PW_PROPERTY, + PW_KEY_NODE_ID, "=u", wp_proxy_get_bound_id (WP_PROXY (node)), + NULL); + if (port) + ready_nodes++; + } - /* re-evaluate the default, taking into account the new configured default; - block recursive calls to this handler as an optimization */ - g_signal_handlers_block_by_func (m, on_metadata_changed, d); - reevaluate_default_node (self, m, node_t); - g_signal_handlers_unblock_by_func (m, on_metadata_changed, d); + if (ready_nodes < total_nodes) { + const gchar *device_name = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (device), PW_KEY_DEVICE_NAME); + wp_debug_object (self, "device '%s' is not ready (%d/%d)", device_name, + ready_nodes, total_nodes); + return FALSE; + } + } + } - /* Save state after specific interval */ - timer_start (self); + /* Make sure Audio and Video virtual sources have ports */ + { + g_autoptr (WpIterator) node_it = NULL; + g_auto (GValue) node_val = G_VALUE_INIT; + node_it = wp_object_manager_new_filtered_iterator (self->rescan_om, + WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_DEVICE_ID, "-", + NULL); + for (; wp_iterator_next (node_it, &node_val); g_value_unset (&node_val)) { + WpPipewireObject *node = g_value_get_object (&node_val); + const gchar *media_class = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), PW_KEY_MEDIA_CLASS); + g_autoptr (WpPort) port = + wp_object_manager_lookup (self->rescan_om, + WP_TYPE_PORT, WP_CONSTRAINT_TYPE_PW_PROPERTY, + PW_KEY_NODE_ID, "=u", wp_proxy_get_bound_id (WP_PROXY (node)), + NULL); + if (!port && + (g_strcmp0 ("Audio/Source/Virtual", media_class) == 0 || + g_strcmp0 ("Video/Source/Virtual", media_class) == 0)) { + const gchar *node_name = wp_pipewire_object_get_property ( + WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME); + wp_debug_object (self, "virtual node '%s' is not ready", node_name); + return FALSE; + } + } } + + return TRUE; } -static gboolean -rescan (WpDefaultNodes * self) +static void +sync_rescan (WpCore * core, GAsyncResult * res, WpDefaultNodes * self) { g_autoptr (WpMetadata) metadata = NULL; + g_autoptr (GError) error = NULL; - g_clear_pointer (&self->idle_source, g_source_unref); + if (!wp_core_sync_finish (core, res, &error)) { + wp_warning_object (self, "core sync error: %s", error->message); + return; + } /* Get the metadata */ metadata = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA, NULL); if (!metadata) - return G_SOURCE_REMOVE; + return; + + /* Make sure nodes are ready for current profile */ + if (!nodes_ready (self)) + return; - wp_trace_object (self, "nodes changed, re-evaluating defaults"); + wp_trace_object (self, "re-evaluating defaults"); reevaluate_default_node (self, metadata, AUDIO_SINK); reevaluate_default_node (self, metadata, AUDIO_SOURCE); reevaluate_default_node (self, metadata, VIDEO_SOURCE); - - return G_SOURCE_REMOVE; } static void schedule_rescan (WpDefaultNodes * self) { - if (!self->idle_source) { - g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); - g_return_if_fail (core); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + g_return_if_fail (core); - wp_core_idle_add_closure (core, &self->idle_source, - g_cclosure_new_object (G_CALLBACK (rescan), G_OBJECT (self))); - } + wp_debug_object (self, "scheduling default nodes rescan"); + wp_core_sync_closure (core, NULL, g_cclosure_new_object ( + G_CALLBACK (sync_rescan), G_OBJECT (self))); } static void -on_node_added (WpObjectManager * om, WpNode * node, WpDefaultNodes * self) +on_metadata_changed (WpMetadata *m, guint32 subject, + const gchar *key, const gchar *type, const gchar *value, gpointer d) { - g_signal_connect_object (node, "notify::n-input-ports", - G_CALLBACK (schedule_rescan), self, G_CONNECT_SWAPPED); - g_signal_connect_object (node, "notify::n-output-ports", - G_CALLBACK (schedule_rescan), self, G_CONNECT_SWAPPED); + WpDefaultNodes * self = WP_DEFAULT_NODES (d); + gint node_t = -1; + gchar name[1024]; + + if (subject == 0) { + for (gint i = 0; i < N_DEFAULT_NODES; i++) { + if (!g_strcmp0 (key, DEFAULT_CONFIG_KEY[i])) { + node_t = i; + break; + } + } + } + + if (node_t != -1) { + g_clear_pointer (&self->defaults[node_t].config_value, g_free); + + if (value && !g_strcmp0 (type, "Spa:String:JSON") && + json_object_find (value, "name", name, sizeof(name)) == 0) + { + self->defaults[node_t].config_value = g_strdup (name); + } + + wp_debug_object (m, "changed '%s' -> '%s'", key, + self->defaults[node_t].config_value); + + /* schedule rescan */ + schedule_rescan (self); + + /* Save state after specific interval */ + timer_start (self); + } } static void @@ -268,18 +537,20 @@ on_metadata_added (WpObjectManager *om, WpMetadata *metadata, gpointer d) g_signal_connect_object (metadata, "changed", G_CALLBACK (on_metadata_changed), self, 0); - /* Create the nodes object manager */ - self->nodes_om = wp_object_manager_new (); - wp_object_manager_add_interest (self->nodes_om, WP_TYPE_NODE, NULL); - wp_object_manager_request_object_features (self->nodes_om, WP_TYPE_NODE, - WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL); - g_signal_connect_object (self->nodes_om, "object-added", - G_CALLBACK (on_node_added), self, 0); - g_signal_connect_object (self->nodes_om, "object-added", - G_CALLBACK (schedule_rescan), self, G_CONNECT_SWAPPED); - g_signal_connect_object (self->nodes_om, "object-removed", + /* Create the rescan object manager */ + self->rescan_om = wp_object_manager_new (); + wp_object_manager_add_interest (self->rescan_om, WP_TYPE_DEVICE, NULL); + wp_object_manager_add_interest (self->rescan_om, WP_TYPE_NODE, NULL); + wp_object_manager_add_interest (self->rescan_om, WP_TYPE_PORT, NULL); + wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_DEVICE, + WP_OBJECT_FEATURES_ALL); + wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_NODE, + WP_OBJECT_FEATURES_ALL); + wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_PORT, + WP_OBJECT_FEATURES_ALL); + g_signal_connect_object (self->rescan_om, "objects-changed", G_CALLBACK (schedule_rescan), self, G_CONNECT_SWAPPED); - wp_core_install_object_manager (core, self->nodes_om); + wp_core_install_object_manager (core, self->rescan_om); } static void @@ -313,11 +584,6 @@ wp_default_nodes_disable (WpPlugin * plugin) { WpDefaultNodes * self = WP_DEFAULT_NODES (plugin); - /* Clear the current rescan callback */ - if (self->idle_source) - g_source_destroy (self->idle_source); - g_clear_pointer (&self->idle_source, g_source_unref); - /* Clear the current timeout callback */ if (self->timeout_source) g_source_destroy (self->timeout_source); @@ -329,7 +595,7 @@ wp_default_nodes_disable (WpPlugin * plugin) } g_clear_object (&self->metadata_om); - g_clear_object (&self->nodes_om); + g_clear_object (&self->rescan_om); g_clear_object (&self->state); } diff --git a/modules/module-default-nodes/common.h b/modules/module-default-nodes/common.h index 35faa011..20ab4cfc 100644 --- a/modules/module-default-nodes/common.h +++ b/modules/module-default-nodes/common.h @@ -37,18 +37,6 @@ static const gchar * DEFAULT_CONFIG_KEY[N_DEFAULT_NODES] = { [VIDEO_SOURCE] = "default.configured.video.source", }; -static const gchar * MEDIA_CLASS_MATCH[N_DEFAULT_NODES] = { - [AUDIO_SINK] = "Audio/*", - [AUDIO_SOURCE] = "Audio/*", - [VIDEO_SOURCE] = "Video/*", -}; - -static const gchar * N_PORTS_KEY[N_DEFAULT_NODES] = { - [AUDIO_SINK] = "n-input-ports", - [AUDIO_SOURCE] = "n-output-ports", - [VIDEO_SOURCE] = "n-output-ports", -}; - #endif static int @@ -62,7 +50,7 @@ json_object_find (const char *obj, const char *key, char *value, size_t len) if (spa_json_enter_object(&it[0], &it[1]) <= 0) return -EINVAL; - while (spa_json_get_string(&it[1], k, sizeof(k)-1) > 0) { + while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) { if (strcmp(k, key) == 0) { if (spa_json_get_string(&it[1], value, len) <= 0) continue; diff --git a/modules/module-default-profile.c b/modules/module-default-profile.c index 54f585a6..03bf9e8b 100644 --- a/modules/module-default-profile.c +++ b/modules/module-default-profile.c @@ -66,12 +66,13 @@ find_device_profile (WpPipewireObject *device, const gchar *lookup_name) if (!wp_spa_pod_get_object (pod, NULL, "index", "i", &index, "name", "s", &name, - NULL)) { + NULL)) continue; - } - if (g_strcmp0 (name, lookup_name) == 0) + if (g_strcmp0 (name, lookup_name) == 0) { + g_value_unset (&item); return index; + } } return -1; @@ -169,50 +170,45 @@ update_profile (WpDefaultProfile *self, WpPipewireObject *device, } static void -on_device_profile_notified (WpPipewireObject *device, GAsyncResult *res, - WpDefaultProfile *self) +handle_profile (WpDefaultProfile *self, WpPipewireObject * device, + WpIterator *profiles) { - g_autoptr (WpIterator) profiles = NULL; - g_autoptr (GError) error = NULL; g_auto (GValue) item = G_VALUE_INIT; - const gchar *name = NULL; - gint index = 0; - - /* Finish */ - profiles = wp_pipewire_object_enum_params_finish (device, res, &error); - if (error) { - wp_warning_object (self, "failed to get current profile on device: %s", - error->message); - return; - } - /* Ignore empty profile notifications */ - if (!wp_iterator_next (profiles, &item)) - return; - - /* Parse the profile */ - WpSpaPod *pod = g_value_get_boxed (&item); - if (!wp_spa_pod_get_object (pod, NULL, - "index", "i", &index, - "name", "s", &name, - NULL)) { - wp_warning_object (self, "failed to parse current profile"); - return; - } + for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { + WpSpaPod *pod = g_value_get_boxed (&item); + const gchar *name = NULL; + gint index = 0; + gboolean save = FALSE; - g_value_unset (&item); + if (!wp_spa_pod_get_object (pod, NULL, + "index", "i", &index, + "name", "s", &name, + "save", "?b", &save, + NULL)) + continue; - /* Update the profile */ - update_profile (self, device, name); + if (save) + update_profile (self, device, name); + } } static void -on_device_param_info_notified (WpPipewireObject * device, GParamSpec * param, +on_device_params_changed (WpPipewireObject * proxy, const gchar *param_name, WpDefaultProfile *self) { - /* Check the profile every time the params have changed */ - wp_pipewire_object_enum_params (device, "Profile", NULL, NULL, - (GAsyncReadyCallback) on_device_profile_notified, self); + g_autoptr (WpIterator) profiles = NULL; + + if (g_strcmp0 (param_name, "Profile") == 0) { + profiles = wp_pipewire_object_enum_params_sync (proxy, "Profile", NULL); + if (profiles) + handle_profile (self, proxy, profiles); + } else if (g_strcmp0 (param_name, "EnumProfile") == 0) { + profiles = wp_pipewire_object_enum_params_sync (proxy, "EnumProfile", NULL); + if (profiles) + g_object_set_qdata_full (G_OBJECT (proxy), profiles_quark (), + g_steal_pointer (&profiles), (GDestroyNotify) wp_iterator_unref); + } } static void @@ -221,21 +217,10 @@ on_device_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d) WpDefaultProfile *self = WP_DEFAULT_PROFILE (d); g_autoptr (WpIterator) profiles = NULL; - wp_debug_object (self, "device " WP_OBJECT_FORMAT " added", - WP_OBJECT_ARGS (proxy)); - - /* Enum available profiles */ - profiles = wp_pipewire_object_enum_params_sync (proxy, "EnumProfile", NULL); - if (!profiles) - return; - - /* Keep a reference of the profiles in the device object */ - g_object_set_qdata_full (G_OBJECT (proxy), profiles_quark (), - g_steal_pointer (&profiles), (GDestroyNotify) wp_iterator_unref); + g_signal_connect_object (proxy, "params-changed", + G_CALLBACK (on_device_params_changed), self, 0); - /* Watch for param info changes */ - g_signal_connect_object (proxy, "notify::param-info", - G_CALLBACK (on_device_param_info_notified), self, 0); + on_device_params_changed (proxy, "EnumProfile", self); } static void diff --git a/modules/module-device-activation.c b/modules/module-device-activation.c index 68aea2c9..98978a27 100644 --- a/modules/module-device-activation.c +++ b/modules/module-device-activation.c @@ -12,6 +12,9 @@ #include <spa/utils/keys.h> #include <spa/utils/names.h> +G_DEFINE_QUARK (wp-module-device-activation-best-profile, best_profile); +G_DEFINE_QUARK (wp-module-device-activation-active-profile, active_profile); + struct _WpDeviceActivation { WpPlugin parent; @@ -28,8 +31,18 @@ G_DEFINE_TYPE (WpDeviceActivation, wp_device_activation, WP_TYPE_PLUGIN) static void set_device_profile (WpDeviceActivation *self, WpPipewireObject *device, gint index) { + const gchar *dn = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME); + gpointer active_ptr = NULL; + g_return_if_fail (device); + /* Make sure the profile we want to set is not active */ + active_ptr = g_object_get_qdata (G_OBJECT (device), active_profile_quark ()); + if (active_ptr && GPOINTER_TO_INT (active_ptr) - 1 == index) { + wp_info_object (self, "profile %d is already active in %s", index, dn); + return; + } + /* Set profile */ wp_pipewire_object_set_param (device, "Profile", 0, wp_spa_pod_new_object ( @@ -37,66 +50,248 @@ set_device_profile (WpDeviceActivation *self, WpPipewireObject *device, gint ind "index", "i", index, NULL)); - wp_info_object (self, "profile %d set on device " WP_OBJECT_FORMAT, index, - WP_OBJECT_ARGS (device)); + wp_info_object (self, "profile %d set on device %s", index, dn); } -static void -handle_device_profiles (WpDeviceActivation *self, WpPipewireObject *proxy, - WpIterator *profiles) +static gint +find_active_profile (WpPipewireObject *proxy, gboolean *off) +{ + g_autoptr (WpIterator) profiles = NULL; + g_auto (GValue) item = G_VALUE_INIT; + gint idx = -1, prio = 0; + guint32 avail = SPA_PARAM_AVAILABILITY_unknown; + const gchar *name; + + /* Get current profile */ + profiles = wp_pipewire_object_enum_params_sync (proxy, "Profile", NULL); + if (!profiles) + return idx; + + for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { + WpSpaPod *pod = g_value_get_boxed (&item); + if (!wp_spa_pod_get_object (pod, NULL, + "index", "i", &idx, + "name", "s", &name, + "priority", "?i", &prio, + "available", "?I", &avail, + NULL)) + continue; + + g_value_unset (&item); + break; + } + + if (off) + *off = idx >= 0 && g_strcmp0 (name, "off") == 0; + + return idx; +} + +static gint +find_best_profile (WpIterator *profiles) +{ + g_auto (GValue) item = G_VALUE_INIT; + gint best_idx = -1, unk_idx = -1, off_idx = -1; + gint best_prio = 0, unk_prio = 0; + + wp_iterator_reset (profiles); + for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { + WpSpaPod *pod = g_value_get_boxed (&item); + gint idx, prio = 0; + guint32 avail = SPA_PARAM_AVAILABILITY_unknown; + const gchar *name; + + if (!wp_spa_pod_get_object (pod, NULL, + "index", "i", &idx, + "name", "s", &name, + "priority", "?i", &prio, + "available", "?I", &avail, + NULL)) { + continue; + } + + if (g_strcmp0 (name, "pro-audio") == 0) + continue; + + if (g_strcmp0 (name, "off") == 0) { + off_idx = idx; + } else if (avail == SPA_PARAM_AVAILABILITY_yes) { + if (best_idx == -1 || prio > best_prio) { + best_prio = prio; + best_idx = idx; + } + } else if (avail != SPA_PARAM_AVAILABILITY_no) { + if (unk_idx == -1 || prio > unk_prio) { + unk_prio = prio; + unk_idx = idx; + } + } + } + + if (best_idx != -1) + return best_idx; + else if (unk_idx != -1) + return unk_idx; + else if (off_idx != -1) + return off_idx; + return -1; +} + +static gint +find_default_profile (WpDeviceActivation *self, WpPipewireObject *proxy, + WpIterator *profiles, gboolean *available) { g_autoptr (WpPlugin) dp = g_weak_ref_get (&self->default_profile); - const gchar *name = NULL; - gint index = -1; + g_auto (GValue) item = G_VALUE_INIT; + const gchar *def_name = NULL; /* Get the default profile name if default-profile module is loaded */ if (dp) - g_signal_emit_by_name (dp, "get-profile", WP_DEVICE (proxy), &name); - - /* Find the profile index */ - if (name) { - g_auto (GValue) item = G_VALUE_INIT; - for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { - WpSpaPod *pod = g_value_get_boxed (&item); - gint i = 0; - const gchar *n = NULL; - - /* Parse */ - if (!wp_spa_pod_get_object (pod, NULL, - "index", "i", &i, - "name", "s", &n, - NULL)) { - continue; - } + g_signal_emit_by_name (dp, "get-profile", WP_DEVICE (proxy), &def_name); + if (!def_name) + return -1; - if (g_strcmp0 (name, n) == 0) { - index = i; - break; - } + /* Find the best profile index */ + wp_iterator_reset (profiles); + for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { + WpSpaPod *pod = g_value_get_boxed (&item); + gint idx = -1, prio = 0; + guint32 avail = SPA_PARAM_AVAILABILITY_unknown; + const gchar *name = NULL; + + /* Parse */ + if (!wp_spa_pod_get_object (pod, NULL, + "index", "i", &idx, + "name", "s", &name, + "priority", "?i", &prio, + "available", "?I", &avail, + NULL)) + continue; + + /* Check if the profile name is the default one */ + if (g_strcmp0 (def_name, name) == 0) { + if (available) + *available = avail; + g_value_unset (&item); + return idx; } } - /* If not profile was found, use index 1 for ALSA (no ACP) and Bluez5 */ - if (index < 0) { - /* Alsa */ - const gchar *api = - wp_pipewire_object_get_property (proxy, PW_KEY_DEVICE_API); - if (api && g_str_has_prefix (api, "alsa")) { - const gchar *acp = - wp_pipewire_object_get_property (proxy, "device.api.alsa.acp"); - if (!acp || !atoi (acp)) - index = 1; - } + return -1; +} + +static gint +handle_active_profile (WpDeviceActivation *self, WpPipewireObject *proxy, + WpIterator *profiles, gboolean *changed, gboolean *off) +{ + const gchar *dn = wp_pipewire_object_get_property (proxy, PW_KEY_DEVICE_NAME); + gpointer active_ptr = NULL; + gint new_active = -1; + gint local_changed = FALSE; + + /* Find the new active profile */ + new_active = find_active_profile (proxy, off); + if (new_active < 0) { + wp_info_object (self, "cannot find active profile in %s", dn); + return new_active; + } + + /* Update active profile if changed */ + active_ptr = g_object_get_qdata (G_OBJECT (proxy), active_profile_quark ()); + local_changed = !active_ptr || GPOINTER_TO_INT (active_ptr) - 1 != new_active; + if (local_changed) { + wp_info_object (self, "active profile changed to %d in %s", new_active, dn); + g_object_set_qdata (G_OBJECT (proxy), active_profile_quark (), + GINT_TO_POINTER (new_active + 1)); + } - /* Bluez5 */ - else if (api && g_str_has_prefix (api, "bluez5")) { - index = 1; + if (changed) + *changed = local_changed; + + return new_active; +} + +static gint +handle_best_profile (WpDeviceActivation *self, WpPipewireObject *proxy, + WpIterator *profiles, gboolean *changed) +{ + const gchar *dn = wp_pipewire_object_get_property (proxy, PW_KEY_DEVICE_NAME); + gpointer best_ptr = NULL; + gint new_best = -1; + gboolean local_changed = FALSE; + + /* Get the new best profile index */ + new_best = find_best_profile (profiles); + if (new_best < 0) { + wp_info_object (self, "cannot find best profile in %s", dn); + return new_best; + } + + /* Update best profile if changed */ + best_ptr = g_object_get_qdata (G_OBJECT (proxy), best_profile_quark ()); + local_changed = !best_ptr || GPOINTER_TO_INT (best_ptr) - 1 != new_best; + if (local_changed) { + wp_info_object (self, "best profile changed to %d in %s", new_best, dn); + g_object_set_qdata (G_OBJECT (proxy), best_profile_quark (), + GINT_TO_POINTER (new_best + 1)); + } + + if (changed) + *changed = local_changed; + + return new_best; +} + +static void +handle_enum_profiles (WpDeviceActivation *self, WpPipewireObject *proxy, + WpIterator *profiles) +{ + const gchar *dn = wp_pipewire_object_get_property (proxy, PW_KEY_DEVICE_NAME); + gint active_idx = FALSE, best_idx = FALSE; + gboolean active_changed = FALSE, best_changed = FALSE, active_off = FALSE; + + /* Set default device if active profile changed to off */ + active_idx = handle_active_profile (self, proxy, profiles, &active_changed, + &active_off); + if (active_idx >= 0 && active_changed && active_off) { + gboolean default_avail = FALSE; + gint default_idx = -1; + default_idx = find_default_profile (self, proxy, profiles, &default_avail); + if (default_idx >= 0) { + if (default_avail == SPA_PARAM_AVAILABILITY_no) { + wp_info_object (self, "default profile %d unavailable for %s", + default_idx, dn); + } else { + wp_info_object (self, "found default profile %d for %s", default_idx, + dn); + set_device_profile (self, proxy, default_idx); + return; + } + } else { + wp_info_object (self, "cannot find default profile for %s", dn); } } - /* Set the profile */ - if (index >= 0) - set_device_profile (self, proxy, index); + /* Otherwise just set the best profile if changed */ + best_idx = handle_best_profile (self, proxy, profiles, &best_changed); + if (best_idx >= 0 && best_changed) + set_device_profile (self, proxy, best_idx); + else if (best_idx >= 0) + wp_info_object (self, "best profile %d already set in %s", best_idx, dn); + else + wp_info_object (self, "best profile not found in %s", dn); +} + +static void +on_device_params_changed (WpPipewireObject * proxy, const gchar *param_name, + WpDeviceActivation *self) +{ + if (g_strcmp0 (param_name, "EnumProfile") == 0) { + g_autoptr (WpIterator) profiles = NULL; + profiles = wp_pipewire_object_enum_params_sync (proxy, "EnumProfile", NULL); + if (profiles) + handle_enum_profiles (self, proxy, profiles); + } } static void @@ -105,12 +300,10 @@ on_device_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d) WpDeviceActivation *self = WP_DEVICE_ACTIVATION (d); g_autoptr (WpIterator) profiles = NULL; - /* Enum available profiles */ - profiles = wp_pipewire_object_enum_params_sync (proxy, "EnumProfile", NULL); - if (!profiles) - return; + g_signal_connect_object (proxy, "params-changed", + G_CALLBACK (on_device_params_changed), self, 0); - handle_device_profiles (self, proxy, profiles); + on_device_params_changed (proxy, "EnumProfile", self); } static void diff --git a/modules/module-lua-scripting.c b/modules/module-lua-scripting.c index c143d3d8..a81038aa 100644 --- a/modules/module-lua-scripting.c +++ b/modules/module-lua-scripting.c @@ -132,6 +132,7 @@ find_script (const gchar * script, gboolean daemon) return g_strdup (script); return wp_find_file (WP_LOOKUP_DIR_ENV_DATA | + WP_LOOKUP_DIR_XDG_CONFIG_HOME | WP_LOOKUP_DIR_ETC | WP_LOOKUP_DIR_PREFIX_SHARE, script, "scripts"); diff --git a/modules/module-mixer-api.c b/modules/module-mixer-api.c index b410a5ce..af6de20a 100644 --- a/modules/module-mixer-api.c +++ b/modules/module-mixer-api.c @@ -210,6 +210,7 @@ collect_node_info (WpMixerApi * self, struct node_info *info, info->route_index = r_index; info->route_device = r_device; have_volume = TRUE; + g_value_unset (&val); break; } } @@ -222,8 +223,10 @@ collect_node_info (WpMixerApi * self, struct node_info *info, it = wp_pipewire_object_enum_params_sync (node, "Props", NULL); for (; it && wp_iterator_next (it, &val); g_value_unset (&val)) { WpSpaPod *param = g_value_get_boxed (&val); - if (node_info_fill (info, param)) + if (node_info_fill (info, param)) { + g_value_unset (&val); break; + } } } } diff --git a/modules/module-reserve-device/plugin.c b/modules/module-reserve-device/plugin.c index d8e4e092..68df6ee8 100644 --- a/modules/module-reserve-device/plugin.c +++ b/modules/module-reserve-device/plugin.c @@ -221,7 +221,7 @@ wp_reserve_device_plugin_create_reservation (WpReserveDevicePlugin *self, NULL); /* use rd->name to avoid copying @em name again */ - g_hash_table_insert (self->reserve_devices, rd->name, rd); + g_hash_table_replace (self->reserve_devices, rd->name, rd); return g_object_ref (rd); } diff --git a/modules/module-route-settings-api.c b/modules/module-route-settings-api.c index eae4a802..65f2161b 100644 --- a/modules/module-route-settings-api.c +++ b/modules/module-route-settings-api.c @@ -80,7 +80,7 @@ wp_route_settings_api_convert (WpRouteSettingsApi * self, if (spa_json_enter_object(&it[0], &it[1]) <= 0) return NULL; - while (spa_json_get_string(&it[1], k, sizeof(k)-1) > 0) { + while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) { int len; const char *value; @@ -98,9 +98,8 @@ wp_route_settings_api_convert (WpRouteSettingsApi * self, str = g_string_new(""); while ((len = spa_json_next(&it[2], &value)) > 0) { char v[1024]; - if (len > 1023) + if (spa_json_parse_stringn(value, len, v, sizeof(v)) < 0) continue; - spa_json_parse_string(value, len, v); g_string_append_printf(str, "%s;", v); } return g_string_free(str, false); diff --git a/modules/module-si-audio-adapter.c b/modules/module-si-audio-adapter.c index 00870248..f1f62185 100644 --- a/modules/module-si-audio-adapter.c +++ b/modules/module-si-audio-adapter.c @@ -205,6 +205,15 @@ si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node) return have_format; } +static void +on_proxy_destroyed (WpNode * proxy, WpSiAudioAdapter * self) +{ + if (self->node == proxy) { + wp_object_abort_activation (WP_OBJECT (self), "proxy destroyed"); + si_audio_adapter_reset (WP_SESSION_ITEM (self)); + } +} + static gboolean si_audio_adapter_configure (WpSessionItem * item, WpProperties *p) { @@ -255,6 +264,8 @@ si_audio_adapter_configure (WpSessionItem * item, WpProperties *p) self->is_autoconnect = str && pw_properties_parse_bool (str); self->node = g_object_ref (node); + g_signal_connect_object (self->node, "pw-proxy-destroyed", + G_CALLBACK (on_proxy_destroyed), self, 0); wp_properties_set (si_props, "item.node.supports-encoded-fmts", self->have_encoded ? "true" : "false"); @@ -419,10 +430,13 @@ build_adapter_default_format (WpSiAudioAdapter * self, const gchar *mode) static void on_format_set (GObject *obj, GAsyncResult * res, gpointer p) { - WpTransition *transition = p; + g_autoptr(WpTransition) transition = p; WpSiAudioAdapter *self = wp_transition_get_source_object (transition); g_autoptr (GError) error = NULL; + if (wp_transition_get_completed (transition)) + return; + wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (self), res, &error); if (error) { wp_transition_return_error (transition, g_steal_pointer (&error)); @@ -463,7 +477,7 @@ si_audio_adapter_configure_node (WpSiAudioAdapter *self, /* set chosen format in the ports */ wp_si_adapter_set_ports_format (WP_SI_ADAPTER (self), - wp_spa_pod_ref (ports_format), mode, on_format_set, transition); + g_steal_pointer (&ports_format), mode, on_format_set, g_object_ref (transition)); } static void @@ -591,12 +605,10 @@ si_audio_adapter_set_ports_format (WpSiAdapter * item, WpSpaPod *f, const gchar *mode, GAsyncReadyCallback callback, gpointer data) { WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item); - g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); g_autoptr (WpSpaPod) format = f; + g_autoptr (GTask) task = g_task_new (self, NULL, callback, data); guint32 active = 0; - g_return_if_fail (core); - /* cancel previous task if any */ if (self->format_task) { g_autoptr (GTask) t = g_steal_pointer (&self->format_task); @@ -607,17 +619,18 @@ si_audio_adapter_set_ports_format (WpSiAdapter * item, WpSpaPod *f, /* build default format if NULL was given */ if (!format && !g_strcmp0 (mode, "dsp")) { format = build_adapter_default_format (self, mode); - g_return_if_fail (format); + if (!format) { + g_task_return_new_error (task, WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_OPERATION_FAILED, + "failed to build default format, aborting set format operation"); + return; + } } - /* create the new task */ - g_return_if_fail (!self->format_task); - self->format_task = g_task_new (self, NULL, callback, data); - + /* make sure the node has WP_NODE_FEATURE_PORTS */ active = wp_object_get_active_features (WP_OBJECT (self->node)); if (G_UNLIKELY (!(active & WP_NODE_FEATURE_PORTS))) { - g_autoptr (GTask) t = g_steal_pointer (&self->format_task); - g_task_return_new_error (t, WP_DOMAIN_LIBRARY, + g_task_return_new_error (task, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, "node feature ports is not enabled, aborting set format operation"); return; @@ -627,20 +640,20 @@ si_audio_adapter_set_ports_format (WpSiAdapter * item, WpSpaPod *f, if (!g_strcmp0 (mode, self->mode) && ((format == NULL && self->format == NULL) || wp_spa_pod_equal (format, self->format))) { - g_autoptr (GTask) t = g_steal_pointer (&self->format_task); - g_task_return_boolean (t, TRUE); + g_task_return_boolean (task, TRUE); return; } - /* set format and mode */ - g_clear_pointer (&self->format, wp_spa_pod_unref); - self->format = g_steal_pointer (&format); - strncpy (self->mode, mode ? mode : "dsp", sizeof (self->mode) - 1); - /* ensure the node is suspended */ if (wp_node_get_state (self->node, NULL) >= WP_NODE_STATE_IDLE) wp_node_send_command (self->node, "Suspend"); + /* set task, format and mode */ + self->format_task = g_steal_pointer (&task); + g_clear_pointer (&self->format, wp_spa_pod_unref); + self->format = g_steal_pointer (&format); + strncpy (self->mode, mode ? mode : "dsp", sizeof (self->mode) - 1); + /* configure DSP with chosen format */ wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node), "PortConfig", 0, wp_spa_pod_new_object ( diff --git a/modules/module-si-node.c b/modules/module-si-node.c index 35075a9e..2aa078ac 100644 --- a/modules/module-si-node.c +++ b/modules/module-si-node.c @@ -46,6 +46,15 @@ si_node_reset (WpSessionItem * item) WP_SESSION_ITEM_CLASS (si_node_parent_class)->reset (item); } +static void +on_proxy_destroyed (WpNode * proxy, WpSiNode * self) +{ + if (self->node == proxy) { + wp_object_abort_activation (WP_OBJECT (self), "proxy destroyed"); + si_node_reset (WP_SESSION_ITEM (self)); + } +} + static gboolean si_node_configure (WpSessionItem * item, WpProperties *p) { @@ -62,6 +71,8 @@ si_node_configure (WpSessionItem * item, WpProperties *p) return FALSE; self->node = g_object_ref (node); + g_signal_connect_object (self->node, "pw-proxy-destroyed", + G_CALLBACK (on_proxy_destroyed), self, 0); wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME); wp_session_item_set_properties (WP_SESSION_ITEM (self), diff --git a/modules/module-si-standard-link.c b/modules/module-si-standard-link.c index 95ec7ca5..a90eeb04 100644 --- a/modules/module-si-standard-link.c +++ b/modules/module-si-standard-link.c @@ -27,6 +27,8 @@ struct _WpSiStandardLink /* activate */ GPtrArray *node_links; + guint n_active_links; + guint n_failed_links; guint n_async_ops_wait; }; @@ -153,6 +155,8 @@ si_standard_link_disable_active (WpSessionItem *si) } g_clear_pointer (&self->node_links, g_ptr_array_unref); + self->n_active_links = 0; + self->n_failed_links = 0; self->n_async_ops_wait = 0; wp_object_update_features (WP_OBJECT (self), 0, @@ -164,17 +168,29 @@ on_link_activated (WpObject * proxy, GAsyncResult * res, WpTransition * transition) { WpSiStandardLink *self = wp_transition_get_source_object (transition); - g_autoptr (GError) error = NULL; + guint len = self->node_links->len; - if (!wp_object_activate_finish (proxy, res, &error)) { - wp_transition_return_error (transition, g_steal_pointer (&error)); + /* Count the number of failed and active links */ + if (wp_object_activate_finish (proxy, res, NULL)) + self->n_active_links++; + else + self->n_failed_links++; + + /* Wait for all links to finish activation */ + if (self->n_failed_links + self->n_active_links != len) return; - } - self->n_async_ops_wait--; - if (self->n_async_ops_wait == 0) + /* We only active feature if all links activated successfully */ + if (self->n_failed_links > 0) { + g_clear_pointer (&self->node_links, g_ptr_array_unref); + wp_transition_return_error (transition, g_error_new ( + WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "%d of %d PipeWire links failed to activate", + self->n_failed_links, len)); + } else { wp_object_update_features (WP_OBJECT (self), WP_SESSION_ITEM_FEATURE_ACTIVE, 0); + } } struct port @@ -225,33 +241,34 @@ static gboolean create_links (WpSiStandardLink * self, WpTransition * transition, GVariant * out_ports, GVariant * in_ports) { + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); g_autoptr (GArray) in_ports_arr = NULL; - g_autoptr (WpCore) core = NULL; struct port out_port = {0}; struct port *in_port; GVariantIter *iter = NULL; guint i; + /* Clear old links if any */ + self->n_active_links = 0; + self->n_failed_links = 0; + g_clear_pointer (&self->node_links, g_ptr_array_unref); + /* tuple format: uint32 node_id; uint32 port_id; uint32 channel; // enum spa_audio_channel */ - if (!out_ports || !g_variant_is_of_type (out_ports, G_VARIANT_TYPE("a(uuu)"))) + if (!g_variant_is_of_type (out_ports, G_VARIANT_TYPE("a(uuu)"))) return FALSE; - if (!in_ports || !g_variant_is_of_type (in_ports, G_VARIANT_TYPE("a(uuu)"))) + if (!g_variant_is_of_type (in_ports, G_VARIANT_TYPE("a(uuu)"))) return FALSE; - core = wp_object_get_core (WP_OBJECT (self)); - g_return_val_if_fail (core, FALSE); - - self->n_async_ops_wait = 0; - self->node_links = g_ptr_array_new_with_free_func (g_object_unref); - i = g_variant_n_children (in_ports); if (i == 0) return FALSE; + self->node_links = g_ptr_array_new_with_free_func (g_object_unref); + /* transfer the in ports to an array so that we can mark them when they are linked */ in_ports_arr = g_array_sized_new (FALSE, TRUE, sizeof (struct port), i + 1); @@ -309,7 +326,6 @@ create_links (WpSiStandardLink * self, WpTransition * transition, g_ptr_array_add (self->node_links, link); /* activate to ensure it is created without errors */ - self->n_async_ops_wait++; wp_object_activate_closure (WP_OBJECT (link), WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL, NULL, g_cclosure_new_object ( @@ -330,11 +346,23 @@ get_ports_and_create_links (WpSiStandardLink *self, WpTransition *transition) si_out = WP_SI_LINKABLE (g_weak_ref_get (&self->out_item)); si_in = WP_SI_LINKABLE (g_weak_ref_get (&self->in_item)); - g_return_if_fail (si_out); - g_return_if_fail (si_in); + if (!si_out || !si_in || + !wp_session_item_is_configured (WP_SESSION_ITEM (si_out)) || + !wp_session_item_is_configured (WP_SESSION_ITEM (si_in))) { + wp_transition_return_error (transition, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "si-standard-link: in/out items are not valid anymore")); + return; + } out_ports = wp_si_linkable_get_ports (si_out, self->out_item_port_context); in_ports = wp_si_linkable_get_ports (si_in, self->in_item_port_context); + if (!out_ports || !in_ports) { + wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_INVARIANT, + "Failed to create links because one of the nodes has no ports")); + return; + } if (!create_links (self, transition, out_ports, in_ports)) wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY, @@ -426,6 +454,14 @@ on_main_adapter_ready (GObject *obj, GAsyncResult * res, gpointer p) main = g_object_get_data (G_OBJECT (transition), "adapter_main"); other = g_object_get_data (G_OBJECT (transition), "adapter_other"); + if (!wp_session_item_is_configured (WP_SESSION_ITEM (main->si)) || + !wp_session_item_is_configured (WP_SESSION_ITEM (other->si))) { + wp_transition_return_error (transition, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "si-standard-link: in/out items are not valid anymore")); + return; + } + if (self->passthrough) { wp_si_adapter_set_ports_format (other->si, NULL, "passthrough", on_adapters_ready, transition); @@ -444,15 +480,26 @@ on_main_adapter_ready (GObject *obj, GAsyncResult * res, gpointer p) static void configure_and_link_adapters (WpSiStandardLink *self, WpTransition *transition) { + g_autoptr (WpSiAdapter) si_out = + WP_SI_ADAPTER (g_weak_ref_get (&self->out_item)); + g_autoptr (WpSiAdapter) si_in = + WP_SI_ADAPTER (g_weak_ref_get (&self->in_item)); struct adapter *out, *in, *main, *other; const gchar *str = NULL; + if (!si_out || !si_in || + !wp_session_item_is_configured (WP_SESSION_ITEM (si_out)) || + !wp_session_item_is_configured (WP_SESSION_ITEM (si_in))) { + wp_transition_return_error (transition, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "si-standard-link: in/out items are not valid anymore")); + return; + } + out = g_slice_new0 (struct adapter); in = g_slice_new0 (struct adapter); - out->si = WP_SI_ADAPTER (g_weak_ref_get (&self->out_item)); - in->si = WP_SI_ADAPTER (g_weak_ref_get (&self->in_item)); - g_return_if_fail (out->si); - g_return_if_fail (in->si); + out->si = g_steal_pointer (&si_out); + in->si = g_steal_pointer (&si_in); str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.node.type"); out->is_device = !g_strcmp0 (str, "device"); @@ -537,6 +584,15 @@ si_standard_link_do_link (WpSiStandardLink *self, WpTransition *transition) g_autoptr (WpSessionItem) si_out = g_weak_ref_get (&self->out_item); g_autoptr (WpSessionItem) si_in = g_weak_ref_get (&self->in_item); + if (!si_out || !si_in || + !wp_session_item_is_configured (si_out) || + !wp_session_item_is_configured (si_in)) { + wp_transition_return_error (transition, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "si-standard-link: in/out items are not valid anymore")); + return; + } + if (WP_IS_SI_ADAPTER (si_out) && WP_IS_SI_ADAPTER (si_in)) configure_and_link_adapters (self, transition); else if (!WP_IS_SI_ADAPTER (si_out) && !WP_IS_SI_ADAPTER (si_in)) @@ -582,9 +638,11 @@ si_standard_link_enable_active (WpSessionItem *si, WpTransition *transition) /* make sure in/out items are valid */ si_out = g_weak_ref_get (&self->out_item); si_in = g_weak_ref_get (&self->in_item); - if (!si_out || !si_in) { + if (!si_out || !si_in || + !wp_session_item_is_configured (si_out) || + !wp_session_item_is_configured (si_in)) { wp_transition_return_error (transition, - g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, "si-standard-link: in/out items are not valid anymore")); return; } diff --git a/src/config/bluetooth.conf b/src/config/bluetooth.conf index ebd0b600..d7d2764e 100644 --- a/src/config/bluetooth.conf +++ b/src/config/bluetooth.conf @@ -35,6 +35,28 @@ context.modules = [ # If nofail is given, module initialization failures are ignored. # + # Uses RTKit to boost the data thread priority. + { name = libpipewire-module-rtkit + args = { + #nice.level = -11 + #rt.prio = 88 + #rt.time.soft = 2000000 + #rt.time.hard = 2000000 + } + flags = [ ifexists nofail ] + } + + # Set thread priorities without using RTKit. + #{ name = libpipewire-module-rt + # args = { + # nice.level = -11 + # rt.prio = 88 + # rt.time.soft = 2000000 + # rt.time.hard = 2000000 + # } + # flags = [ ifexists nofail ] + #} + # The native communication protocol. { name = libpipewire-module-protocol-native } diff --git a/src/config/main.lua.d/40-device-defaults.lua b/src/config/main.lua.d/40-device-defaults.lua index 4986db96..569ffc33 100644 --- a/src/config/main.lua.d/40-device-defaults.lua +++ b/src/config/main.lua.d/40-device-defaults.lua @@ -5,6 +5,9 @@ device_defaults.properties = { -- when set to false, default nodes and routes are selected based on -- their priorities and any runtime changes do not persist after restart ["use-persistent-storage"] = true, + + -- the default volume to apply to ACP device nodes, in the linear scale + --["default-volume"] = 0.4, } function device_defaults.enable() diff --git a/src/config/main.lua.d/50-alsa-config.lua b/src/config/main.lua.d/50-alsa-config.lua index 43bfc114..23f8ca1b 100644 --- a/src/config/main.lua.d/50-alsa-config.lua +++ b/src/config/main.lua.d/50-alsa-config.lua @@ -17,6 +17,19 @@ alsa_monitor.properties = { alsa_monitor.rules = { -- An array of matches/actions to evaluate. + -- + -- If you want to disable some devices or nodes, you can apply properties per device as the following example. + -- The name can be found by running pw-cli ls Device, or pw-cli dump Device + --{ + -- matches = { + -- { + -- { "device.name", "matches", "name_of_some_disabled_card" }, + -- }, + -- }, + -- apply_properties = { + -- ["device.disabled"] = true, + -- }, + --} { -- Rules for matching a device or node. It is an array of -- properties that all need to match the regexp. If any of the @@ -89,11 +102,13 @@ alsa_monitor.rules = { --["audio.channels"] = 2, --["audio.format"] = "S16LE", --["audio.rate"] = 44100, + --["audio.allowed-rates"] = "32000,96000" --["audio.position"] = "FL,FR", --["api.alsa.period-size"] = 1024, --["api.alsa.headroom"] = 0, --["api.alsa.disable-mmap"] = false, --["api.alsa.disable-batch"] = false, + --["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend }, }, } diff --git a/src/config/main.lua.d/50-default-access-config.lua b/src/config/main.lua.d/50-default-access-config.lua index 0282d4aa..6cf18bed 100644 --- a/src/config/main.lua.d/50-default-access-config.lua +++ b/src/config/main.lua.d/50-default-access-config.lua @@ -20,4 +20,12 @@ default_access.rules = { }, default_permissions = "rx", }, + { + matches = { + { + { "pipewire.access", "=", "restricted" }, + }, + }, + default_permissions = "rx", + }, } diff --git a/src/config/policy.lua.d/50-endpoints-config.lua b/src/config/policy.lua.d/50-endpoints-config.lua index 4bdf82ab..62415094 100644 --- a/src/config/policy.lua.d/50-endpoints-config.lua +++ b/src/config/policy.lua.d/50-endpoints-config.lua @@ -4,36 +4,92 @@ --[[ default_policy.policy.roles = { + ["Capture"] = { + ["alias"] = { "Multimedia", "Music", "Voice", "Capture" }, + ["priority"] = 25, + ["action.default"] = "cork", + ["action.capture"] = "mix", + ["media.class"] = "Audio/Source", + }, ["Multimedia"] = { ["alias"] = { "Movie", "Music", "Game" }, - ["priority"] = 10, - ["action.default"] = "mix", + ["priority"] = 25, + ["action.default"] = "cork", + }, + ["Speech-Low"] = { + ["priority"] = 30, + ["action.default"] = "cork", + ["action.Speech-Low"] = "mix", + }, + ["Custom-Low"] = { + ["priority"] = 35, + ["action.default"] = "cork", + ["action.Custom-Low"] = "mix", }, - ["Notification"] = { - ["priority"] = 20, + ["Navigation"] = { + ["priority"] = 50, ["action.default"] = "duck", - ["action.Notification"] = "mix", + ["action.Navigation"] = "mix", }, - ["Alert"] = { - ["priority"] = 30, + ["Speech-High"] = { + ["priority"] = 60, + ["action.default"] = "cork", + ["action.Speech-High"] = "mix", + }, + ["Custom-High"] = { + ["priority"] = 65, ["action.default"] = "cork", - ["action.Alert"] = "mix", + ["action.Custom-High"] = "mix", + }, + ["Communication"] = { + ["priority"] = 75, + ["action.default"] = "cork", + ["action.Communication"] = "mix", + }, + ["Emergency"] = { + ["alias"] = { "Alert" }, + ["priority"] = 99, + ["action.default"] = "cork", + ["action.Emergency"] = "mix", }, } default_policy.endpoints = { + ["endpoint.capture"] = { + ["media.class"] = "Audio/Source", + ["role"] = "Capture", + }, ["endpoint.multimedia"] = { ["media.class"] = "Audio/Sink", ["role"] = "Multimedia", }, - ["endpoint.notifications"] = { + ["endpoint.speech_low"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Speech-Low", + }, + ["endpoint.custom_low"] = { ["media.class"] = "Audio/Sink", - ["role"] = "Notification", + ["role"] = "Custom-Low", }, - ["endpoint.alert"] = { + ["endpoint.navigation"] = { ["media.class"] = "Audio/Sink", - ["role"] = "Alert", + ["role"] = "Navigation", + }, + ["endpoint.speech_high"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Speech-High", + }, + ["endpoint.custom_high"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Custom-High", + }, + ["endpoint.communication"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Communication", + }, + ["endpoint.emergency"] = { + ["media.class"] = "Audio/Sink", + ["role"] = "Emergency", }, } - -]]-- +]]-- \ No newline at end of file diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index 0ac65c73..5658c961 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -38,6 +38,28 @@ context.modules = [ # If nofail is given, module initialization failures are ignored. # + # Uses RTKit to boost the data thread priority. + { name = libpipewire-module-rtkit + args = { + #nice.level = -11 + #rt.prio = 88 + #rt.time.soft = 2000000 + #rt.time.hard = 2000000 + } + flags = [ ifexists nofail ] + } + + # Set thread priorities without using RTKit. + #{ name = libpipewire-module-rt + # args = { + # nice.level = -11 + # rt.prio = 88 + # rt.time.soft = 2000000 + # rt.time.hard = 2000000 + # } + # flags = [ ifexists nofail ] + #} + # The native communication protocol. { name = libpipewire-module-protocol-native } diff --git a/src/main.c b/src/main.c index 8fefa886..e1970b69 100644 --- a/src/main.c +++ b/src/main.c @@ -151,7 +151,7 @@ wp_init_transition_execute_step (WpTransition * transition, guint step) while (spa_json_enter_object(&it[1], &it[2]) > 0) { char *name = NULL, *type = NULL; - while (spa_json_get_string(&it[2], key, sizeof(key)-1) > 0) { + while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) { const char *val; int len; @@ -160,10 +160,10 @@ wp_init_transition_execute_step (WpTransition * transition, guint step) if (strcmp(key, "name") == 0) { name = (char*)val; - spa_json_parse_string(val, len, name); + spa_json_parse_stringn(val, len, name, len+1); } else if (strcmp(key, "type") == 0) { type = (char*)val; - spa_json_parse_string(val, len, type); + spa_json_parse_stringn(val, len, type, len+1); } } if (name == NULL || type == NULL) { diff --git a/src/scripts/access/access-default.lua b/src/scripts/access/access-default.lua index 3c27e90a..0fac87b7 100644 --- a/src/scripts/access/access-default.lua +++ b/src/scripts/access/access-default.lua @@ -5,7 +5,7 @@ -- -- SPDX-License-Identifier: MIT -local config = ... +local config = ... or {} -- preprocess rules and create Interest objects for _, r in ipairs(config.rules or {}) do diff --git a/src/scripts/create-item.lua b/src/scripts/create-item.lua index 9f2bc862..f2c75623 100644 --- a/src/scripts/create-item.lua +++ b/src/scripts/create-item.lua @@ -6,7 +6,7 @@ -- SPDX-License-Identifier: MIT -- Receive script arguments from config.lua -local config = ... +local config = ... or {} items = {} @@ -22,6 +22,8 @@ function configProperties(node) ["client.id"] = np["client.id"], ["object.path"] = np["object.path"], ["priority.session"] = np["priority.session"], + ["device.id"] = np["device.id"], + ["card.profile.device"] = np["card.profile.device"], } for k, v in pairs(np) do @@ -67,9 +69,13 @@ function addItem (node, item_type) end -- activate item - items[id]:activate (Features.ALL, function (item) - Log.info(item, "activated item for node " .. tostring(id)) - item:register () + items[id]:activate (Features.ALL, function (item, e) + if e then + Log.message(item, "failed to activate item: " .. tostring(e)); + else + Log.info(item, "activated item for node " .. tostring(id)) + item:register () + end end) end diff --git a/src/scripts/default-routes.lua b/src/scripts/default-routes.lua index b55ae4e0..075f5d63 100644 --- a/src/scripts/default-routes.lua +++ b/src/scripts/default-routes.lua @@ -8,11 +8,14 @@ -- -- SPDX-License-Identifier: MIT -local config = ... +local config = ... or {} -- whether to store state on the file system use_persistent_storage = config["use-persistent-storage"] or false +-- the default volume to apply +default_volume = tonumber(config["default-volume"] or 0.4) + -- table of device info dev_infos = {} @@ -130,7 +133,7 @@ function restoreRoute(device, dev_info, device_id, route) -- default props local props = { "Spa:Pod:Object:Param:Props", "Route", - channelVolumes = { 0.4 }, + channelVolumes = { default_volume }, mute = false, } @@ -349,6 +352,12 @@ function handleDevice(device) local dev_info = dev_infos[device["bound-id"]] local new_route_infos = {} local avail_routes_changed = false + local profile = nil + + -- get current profile + for p in device:iterate_params("Profile") do + profile = parseParam(p, "Profile") + end -- look at all the routes and update/reset cached information for p in device:iterate_params("EnumRoute") do @@ -367,7 +376,9 @@ function handleDevice(device) Log.info(device, "route " .. route.name .. " available changed " .. route_info.available .. " -> " .. route.available) route_info.available = route.available - avail_routes_changed = true + if profile and arrayContains(route.profiles, profile.index) then + avail_routes_changed = true + end end route_info.prev_active = route_info.active route_info.active = false @@ -416,23 +427,20 @@ function handleDevice(device) ::skip_route:: end - -- now get the profile and restore routes for it - for p in device:iterate_params("Profile") do - local profile = parseParam(p, "Profile") - if profile then - local profile_changed = (dev_info.active_profile ~= profile.index) - - -- if the profile changed, restore routes for that profile - -- if any of the routes of the current profile changed in availability, - -- then try to select a new "best" route for each device and ignore - -- what was stored - if profile_changed or avail_routes_changed then - dev_info.active_profile = profile.index - restoreProfileRoutes(device, dev_info, profile, profile_changed) - end - - saveProfile(dev_info, profile.name) + -- restore routes for profile + if profile then + local profile_changed = (dev_info.active_profile ~= profile.index) + + -- if the profile changed, restore routes for that profile + -- if any of the routes of the current profile changed in availability, + -- then try to select a new "best" route for each device and ignore + -- what was stored + if profile_changed or avail_routes_changed then + dev_info.active_profile = profile.index + restoreProfileRoutes(device, dev_info, profile, profile_changed) end + + saveProfile(dev_info, profile.name) end end diff --git a/src/scripts/monitors/alsa-midi.lua b/src/scripts/monitors/alsa-midi.lua index bf4fde47..0efee345 100644 --- a/src/scripts/monitors/alsa-midi.lua +++ b/src/scripts/monitors/alsa-midi.lua @@ -6,7 +6,7 @@ -- SPDX-License-Identifier: MIT -- Receive script arguments from config.lua -local config = ... +local config = ... or {} -- ensure config.properties is not nil config.properties = config.properties or {} diff --git a/src/scripts/monitors/alsa.lua b/src/scripts/monitors/alsa.lua index 18ed6145..3e8eda83 100644 --- a/src/scripts/monitors/alsa.lua +++ b/src/scripts/monitors/alsa.lua @@ -6,7 +6,7 @@ -- SPDX-License-Identifier: MIT -- Receive script arguments from config.lua -local config = ... +local config = ... or {} -- ensure config.properties is not nil config.properties = config.properties or {} @@ -168,6 +168,9 @@ function createNode(parent, id, type, factory, properties) -- apply properties from config.rules rulesApplyProperties(properties) + if properties["node.disabled"] then + return + end -- create the node local node = Node("adapter", properties) @@ -177,9 +180,13 @@ end function createDevice(parent, id, factory, properties) local device = SpaDevice(factory, properties) - device:connect("create-object", createNode) - device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) - parent:store_managed_object(id, device) + if device then + device:connect("create-object", createNode) + device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) + parent:store_managed_object(id, device) + else + Log.warning ("Failed to create '" .. factory .. "' device") + end end function prepareDevice(parent, id, type, factory, properties) @@ -250,6 +257,9 @@ function prepareDevice(parent, id, type, factory, properties) -- apply properties from config.rules rulesApplyProperties(properties) + if properties["device.disabled"] then + return + end -- override the device factory to use ACP if properties["api.alsa.use-acp"] then @@ -287,6 +297,12 @@ function prepareDevice(parent, id, type, factory, properties) end end) + rd:connect("release-requested", function (rd) + Log.info("release requested") + parent:store_managed_object(id, nil) + rd:call("release") + end) + if jack_device then rd:connect("notify::owner-name-changed", function (rd, pspec) if rd["state"] == "busy" and @@ -309,7 +325,7 @@ function createMonitor () local m = SpaDevice("api.alsa.enum.udev", config.properties) if m == nil then Log.message("PipeWire's SPA ALSA udev plugin(\"api.alsa.enum.udev\")" - .. "missing or broken. Sound Cards Cannot be enumerated") + .. "missing or broken. Sound Cards cannot be enumerated") return nil end diff --git a/src/scripts/monitors/bluez.lua b/src/scripts/monitors/bluez.lua index 172e013d..4a54a7b2 100644 --- a/src/scripts/monitors/bluez.lua +++ b/src/scripts/monitors/bluez.lua @@ -109,8 +109,13 @@ function createDevice(parent, id, type, factory, properties) -- create the device device = SpaDevice(factory, properties) - device:connect("create-object", createNode) - parent:store_managed_object(id, device) + if device then + device:connect("create-object", createNode) + parent:store_managed_object(id, device) + else + Log.warning ("Failed to create '" .. factory .. "' device") + return + end end Log.info(parent, string.format("%d, %s (%s): %s", diff --git a/src/scripts/monitors/libcamera.lua b/src/scripts/monitors/libcamera.lua index 7209c620..27c74597 100644 --- a/src/scripts/monitors/libcamera.lua +++ b/src/scripts/monitors/libcamera.lua @@ -125,12 +125,16 @@ function createDevice(parent, id, type, factory, properties) -- create the device local device = SpaDevice(factory, properties) - device:connect("create-object", createNode) - device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) - parent:store_managed_object(id, device) + if device then + device:connect("create-object", createNode) + device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) + parent:store_managed_object(id, device) + else + Log.warning ("Failed to create '" .. factory .. "' device") + end end -monitor = SpaDevice("api.libcamera.enum.client", config.properties or {}) +monitor = SpaDevice("api.libcamera.enum.manager", config.properties or {}) if monitor then monitor:connect("create-object", createDevice) monitor:activate(Feature.SpaDevice.ENABLED) diff --git a/src/scripts/monitors/v4l2.lua b/src/scripts/monitors/v4l2.lua index fd9a20db..102eb810 100644 --- a/src/scripts/monitors/v4l2.lua +++ b/src/scripts/monitors/v4l2.lua @@ -125,9 +125,13 @@ function createDevice(parent, id, type, factory, properties) -- create the device local device = SpaDevice(factory, properties) - device:connect("create-object", createNode) - device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) - parent:store_managed_object(id, device) + if device then + device:connect("create-object", createNode) + device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND) + parent:store_managed_object(id, device) + else + Log.warning ("Failed to create '" .. factory .. "' device") + end end monitor = SpaDevice("api.v4l2.enum.udev", config.properties or {}) diff --git a/src/scripts/policy-endpoint-client-links.lua b/src/scripts/policy-endpoint-client-links.lua index 1a932940..eaa1c088 100644 --- a/src/scripts/policy-endpoint-client-links.lua +++ b/src/scripts/policy-endpoint-client-links.lua @@ -5,7 +5,7 @@ -- -- SPDX-License-Identifier: MIT -local config = ... +local config = ... or {} config.roles = config.roles or {} config["duck.level"] = config["duck.level"] or 0.3 diff --git a/src/scripts/policy-endpoint-client.lua b/src/scripts/policy-endpoint-client.lua index d32de5bb..487d4b4a 100644 --- a/src/scripts/policy-endpoint-client.lua +++ b/src/scripts/policy-endpoint-client.lua @@ -6,15 +6,43 @@ -- SPDX-License-Identifier: MIT -- Receive script arguments from config.lua -local config = ... +local config = ... or {} config.roles = config.roles or {} -local pending_rescan = false +local self = {} +self.scanning = false +self.pending_rescan = false -function findRole(role) +function rescan () + for si in linkables_om:iterate() do + handleLinkable (si) + end +end + +function scheduleRescan () + if self.scanning then + self.pending_rescan = true + return + end + + self.scanning = true + rescan () + self.scanning = false + + if self.pending_rescan then + self.pending_rescan = false + Core.sync(function () + scheduleRescan () + end) + end +end + +function findRole(role, tmc) if role and not config.roles[role] then for r, p in pairs(config.roles) do - if type(p.alias) == "table" then + -- default media class can be overridden in the role config data + mc = p["media.class"] or "Audio/Sink" + if (type(p.alias) == "table" and tmc == mc) then for i = 1, #(p.alias), 1 do if role == p.alias[i] then return r @@ -43,7 +71,7 @@ function findTargetEndpoint (node, media_class) end -- find highest priority endpoint by role - media_role = findRole(node.properties["media.role"]) + media_role = findRole(node.properties["media.role"], target_media_class) for si_target_ep in endpoints_om:iterate { Constraint { "role", "=", media_role, type = "pw-global" }, Constraint { "media.class", "=", target_media_class, type = "pw-global" }, @@ -158,7 +186,7 @@ function handleLinkable (si) link:remove () Log.info (si, "... moving to new target") else - pending_rescan = true + scheduleRescan () Log.info (si, "... scheduled rescan") return end @@ -191,20 +219,6 @@ function unhandleLinkable (si) end end -function rescan () - for si in linkables_om:iterate() do - handleLinkable (si) - end - - -- if pending_rescan, re-evaluate after sync - if pending_rescan then - pending_rescan = false - Core.sync (function (c) - rescan() - end) - end -end - endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }} linkables_om = ObjectManager { Interest { type = "SiLinkable", -- only handle si-audio-adapter and si-node @@ -218,7 +232,7 @@ links_om = ObjectManager { Interest { type = "SiLink", } } linkables_om:connect("objects-changed", function (om) - rescan () + scheduleRescan () end) linkables_om:connect("object-removed", function (om, si) diff --git a/src/scripts/policy-endpoint-device.lua b/src/scripts/policy-endpoint-device.lua index 04e3bc62..71f77727 100644 --- a/src/scripts/policy-endpoint-device.lua +++ b/src/scripts/policy-endpoint-device.lua @@ -6,13 +6,40 @@ -- SPDX-License-Identifier: MIT -- Receive script arguments from config.lua -local config = ... +local config = ... or {} -- ensure config.move and config.follow are not nil config.move = config.move or false config.follow = config.follow or false -local pending_rescan = false +local self = {} +self.scanning = false +self.pending_rescan = false + +function rescan () + -- check endpoints and register new links + for si_ep in endpoints_om:iterate() do + handleEndpoint (si_ep) + end +end + +function scheduleRescan () + if self.scanning then + self.pending_rescan = true + return + end + + self.scanning = true + rescan () + self.scanning = false + + if self.pending_rescan then + self.pending_rescan = false + Core.sync(function () + scheduleRescan () + end) + end +end function findTargetByDefaultNode (target_media_class) local def_id = default_nodes:call("get-default-node", target_media_class) @@ -133,7 +160,7 @@ function handleEndpoint (si_ep) link:remove () Log.info (si_ep, "... moving to new target") else - pending_rescan = true + scheduleRescan () Log.info (si_ep, "... scheduled rescan") return end @@ -164,21 +191,6 @@ function unhandleLinkable (si) end end -function rescan () - -- check endpoints and register new links - for si_ep in endpoints_om:iterate() do - handleEndpoint (si_ep) - end - - -- if pending_rescan, re-evaluate after sync - if pending_rescan then - pending_rescan = false - Core.sync (function (c) - rescan() - end) - end -end - default_nodes = Plugin.find("default-nodes-api") endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }} linkables_om = ObjectManager { @@ -200,12 +212,12 @@ links_om = ObjectManager { -- listen for default node changes if config.follow is enabled if config.follow then default_nodes:connect("changed", function (p) - rescan() + scheduleRescan () end) end linkables_om:connect("objects-changed", function (om) - rescan() + scheduleRescan () end) linkables_om:connect("object-removed", function (om, si) diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua index b2e1e143..51a55490 100644 --- a/src/scripts/policy-node.lua +++ b/src/scripts/policy-node.lua @@ -6,13 +6,39 @@ -- SPDX-License-Identifier: MIT -- Receive script arguments from config.lua -local config = ... +local config = ... or {} -- ensure config.move and config.follow are not nil config.move = config.move or false config.follow = config.follow or false -local pending_rescan = false +local self = {} +self.scanning = false +self.pending_rescan = false + +function rescan() + for si in linkables_om:iterate() do + handleLinkable (si) + end +end + +function scheduleRescan () + if self.scanning then + self.pending_rescan = true + return + end + + self.scanning = true + rescan () + self.scanning = false + + if self.pending_rescan then + self.pending_rescan = false + Core.sync(function () + scheduleRescan () + end) + end +end function parseBool(var) return var and (var:lower() == "true" or var == "1") @@ -82,12 +108,16 @@ function createLink (si, si_target, passthrough, exclusive) -- activate si_link:activate (Feature.SessionItem.ACTIVE, function (l, e) if e then - Log.warning (l, "failed to activate si-standard-link: " .. tostring(e)) - si_flags[si_id].peer_id = nil + Log.info (l, "failed to activate si-standard-link: " .. tostring(e)) + if si_flags[si_id] ~= nil then + si_flags[si_id].peer_id = nil + end l:remove () else - si_flags[si_id].failed_peer_id = nil - si_flags[si_id].failed_count = 0 + if si_flags[si_id] ~= nil then + si_flags[si_id].failed_peer_id = nil + si_flags[si_id].failed_count = 0 + end Log.info (l, "activated si-standard-link") end end) @@ -262,9 +292,112 @@ function findDefinedTarget (properties) return nil end --- User or client defined target is not available, Loop through all the valid --- linkables(nodes) and pick an appropriate one. -function findUndefinedTarget (si) +function parseParam(param, id) + local route = param:parse() + if route.pod_type == "Object" and route.object_id == id then + return route.properties + else + return nil + end +end + +function arrayContains(a, value) + for _, v in ipairs(a) do + if v == value then + return true + end + end + return false +end + + +-- Does the target device have any active/available paths/routes to +-- the physical device(spkr/mic/cam)? +function haveAvailableRoutes (si_props) + local card_profile_device = si_props["card.profile.device"] + local device_id = si_props["device.id"] + local device = device_id and devices_om:lookup { + Constraint { "bound-id", "=", device_id, type = "gobject"}, + } + + if not card_profile_device or not device then + return true + end + + local found = 0 + local avail = 0 + + -- First check "SPA_PARAM_Route" if there are any active devices + -- in an active profile. + for p in device:iterate_params("Route") do + local route = parseParam(p, "Route") + if not route then + goto skip_route + end + + if (route.device ~= tonumber(card_profile_device)) then + goto skip_route + end + + if (route.available == "no") then + return false + end + + do return true end + + ::skip_route:: + end + + -- Second check "SPA_PARAM_EnumRoute" if there is any route that + -- is available if not active. + for p in device:iterate_params("EnumRoute") do + local route = parseParam(p, "EnumRoute") + if not route then + goto skip_enum_route + end + + if not arrayContains(route.devices, tonumber(card_profile_device)) then + goto skip_enum_route + end + found = found + 1; + if (route.available ~= "no") then + avail = avail +1 + end + ::skip_enum_route:: + end + + if found == 0 then + return true + end + if avail > 0 then + return true + end + + return false + +end + +function findDefaultlinkable (si) + local si_props = si.properties + local target_direction = getTargetDirection(si_props) + local def_node_id = getDefaultNode(si_props, target_direction) + local si_target = linkables_om:lookup { + Constraint { "node.id", "=", tostring(def_node_id) } + } + + if si_target ~= nil then + local can_passthrough = canPassthrough(si, si_target) + Log.info(string.format("... default target picked: %s (%s), can_passthrough:%s", + tostring(si_target.properties["node.name"]), + tostring(si_target.properties["node.id"]), + tostring(can_passthrough))) + return si_target, can_passthrough + end + + return nil, nil +end + +function findBestLinkable (si) local si_props = si.properties local target_direction = getTargetDirection(si_props) local target_picked = nil @@ -279,6 +412,7 @@ function findUndefinedTarget (si) } do local si_target_props = si_target.properties local si_target_node_id = si_target_props["node.id"] + local priority = tonumber(si_target_props["priority.session"]) or 0 Log.debug(string.format("Looking at: %s (%s)", tostring(si_target_props["node.name"]), @@ -289,14 +423,9 @@ function findUndefinedTarget (si) goto skip_linkable end - local priority = tonumber(si_target_props["priority.session"]) or 0 - - -- Is this linkable(node) a default one? - local def_node_id = getDefaultNode(si_props, target_direction) - if tostring(def_node_id) == si_target_node_id then - Log.debug(string.format("... this (%s) is the default node for %s", - def_node_id, target_direction)) - priority = priority + 10000 + if not haveAvailableRoutes(si_target_props) then + Log.debug("... does not have routes, skip linkable") + goto skip_linkable end -- todo:check if this linkable(node/device) have valid routes. @@ -336,7 +465,7 @@ function findUndefinedTarget (si) end if target_picked then - Log.info(string.format("... target: %s (%s), can_passthrough:%s", + Log.info(string.format("... best target picked: %s (%s), can_passthrough:%s", tostring(target_picked.properties["node.name"]), tostring(target_picked.properties["node.id"]), tostring(target_can_passthrough))) @@ -347,6 +476,25 @@ function findUndefinedTarget (si) end +function findUndefinedTarget (si) + -- Just find the best linkable if default nodes module is not loaded + if default_nodes == nil then + return findBestLinkable (si) + end + + -- Otherwise find the default linkable. If the default linkabke cannot link, + -- we find the best one instead. We return nil if default does not exist. + local si_target, can_passthrough = findDefaultlinkable (si) + if si_target then + if canLink (si.properties, si_target) then + return si_target, can_passthrough + else + return findBestLinkable (si) + end + end + return nil, nil +end + function lookupLink (si_id, si_target_id) local link = links_om:lookup { Constraint { "out.item.id", "=", si_id }, @@ -413,25 +561,16 @@ function handleLinkable (si) si_target = nil end - -- wait up to 2 seconds for the requested target to become available - -- this is because the client may have already "seen" a target that we haven't - -- yet prepared, which leads to a race condition + -- if the client has seen a target that we haven't yet prepared, schedule + -- a rescan one more time and hope for the best local si_id = si.id if si_props["node.target"] and si_props["node.target"] ~= "-1" and not si_target and not si_flags[si_id].was_handled and not si_flags[si_id].done_waiting then - if not si_flags[si_id].timeout_source then - si_flags[si_id].timeout_source = Core.timeout_add(2000, function() - if si_flags[si_id] then - si_flags[si_id].done_waiting = true - si_flags[si_id].timeout_source = nil - rescan() - end - return false - end) - end Log.info (si, "... waiting for target") + si_flags[si_id].done_waiting = true + scheduleRescan() return end @@ -455,7 +594,7 @@ function handleLinkable (si) link:remove () Log.info (si, "... moving to new target") else - pending_rescan = true + scheduleRescan() Log.info (si, "... scheduled rescan") return end @@ -495,6 +634,9 @@ function handleLinkable (si) if not reconnect then Log.info (si, "... destroy node") node:request_destroy() + elseif si_flags[si.id].was_handled then + Log.info (si, "... waiting reconnect") + return end local client_id = node.properties["client.id"] @@ -541,20 +683,6 @@ function unhandleLinkable (si) si_flags[si.id] = nil end -function rescan() - for si in linkables_om:iterate() do - handleLinkable (si) - end - - -- if pending_rescan, re-evaluate after sync - if pending_rescan then - pending_rescan = false - Core.sync (function (c) - rescan() - end) - end -end - default_nodes = Plugin.find("default-nodes-api") metadata_om = ObjectManager { @@ -568,6 +696,9 @@ endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } } clients_om = ObjectManager { Interest { type = "client" } } +devices_om = ObjectManager { Interest { type = "device" } } + + linkables_om = ObjectManager { Interest { type = "SiLinkable", @@ -586,7 +717,7 @@ links_om = ObjectManager { function cleanupTargetNodeMetadata() local metadata = metadata_om:lookup() - if metadata then + if metadata and default_nodes ~= nil then local to_remove = {} for s, k, t, v in metadata:iterate(Id.ANY) do if k == "target.node" then @@ -613,10 +744,10 @@ function cleanupTargetNodeMetadata() end -- listen for default node changes if config.follow is enabled -if config.follow then +if config.follow and default_nodes ~= nil then default_nodes:connect("changed", function () cleanupTargetNodeMetadata() - rescan() + scheduleRescan () end) end @@ -625,18 +756,25 @@ if config.move then metadata_om:connect("object-added", function (om, metadata) metadata:connect("changed", function (m, subject, key, t, value) if key == "target.node" then - rescan() + scheduleRescan () end end) end) end -linkables_om:connect("objects-changed", function (om) - rescan() +linkables_om:connect("object-added", function (om, si) + if si.properties["item.node.type"] ~= "stream" then + scheduleRescan () + else + handleLinkable (si) + end end) linkables_om:connect("object-removed", function (om, si) unhandleLinkable (si) + if si.properties["item.node.type"] ~= "stream" then + scheduleRescan () + end end) metadata_om:activate() @@ -644,3 +782,4 @@ endpoints_om:activate() clients_om:activate() linkables_om:activate() links_om:activate() +devices_om:activate() diff --git a/src/scripts/suspend-node.lua b/src/scripts/suspend-node.lua index 90689300..b051d46f 100644 --- a/src/scripts/suspend-node.lua +++ b/src/scripts/suspend-node.lua @@ -25,11 +25,11 @@ om:connect("object-added", function (om, node) sources[id] = nil end - -- Add a timeout source if idle for at least 3 seconds + -- Add a timeout source if idle for at least 5 seconds if cur_state == "idle" then -- honor "session.suspend-timeout-seconds" if specified local timeout = - tonumber(node.properties["session.suspend-timeout-seconds"]) or 3 + tonumber(node.properties["session.suspend-timeout-seconds"]) or 5 if timeout == 0 then return @@ -45,7 +45,7 @@ om:connect("object-added", function (om, node) sources[id] = nil -- false (== G_SOURCE_REMOVE) destroys the source so that this - -- function does not get fired again after 3 seconds + -- function does not get fired again after 5 seconds return false end) end -- GitLab