Compare commits

...

54 commits

Author SHA1 Message Date
filmsi
80478e7548 Update Slovenian translation (sl.po) 2025-12-16 16:03:57 +02:00
Barnabás Pőcze
3fb5b775ee m-modem-manager: Unref WpCore
When getting the "core" property from a `WpObject`, a strong reference is
returned to the `WpCore` object. This has to be unref-d when not needed
anymore. `wp_modem_manager_enable()` fails to do so, so fix it.

Fixes: 2794764d5a ("m-modem-manager: add module for tracking status of voice calls")
2025-12-16 15:59:07 +02:00
lumingzh
a5538f4167 update Chinese translation 2025-12-16 10:14:56 +08:00
Julian Bouzas
bec20fc054 create-item: Only configure audio device sink nodes in MONO
This allows multi channel mixing in the graph, even if MONO is enabled.
2025-12-08 09:43:56 -05:00
George Kiagiadakis
beded0214d meson: define SPA_AUDIO_MAX_CHANNELS only on newer spa headers
Older spa headers define this without an #ifndef check,
which could lead to compilation issues.
2025-12-02 12:59:58 +02:00
George Kiagiadakis
2286152c07 ci: adapt pipewire build options based on the pw version we are building 2025-12-02 12:32:01 +02:00
George Kiagiadakis
94fe1cbfbd ci: add builds with older versions of libpipewire 2025-12-02 11:59:45 +02:00
Torkel Niklasson
6ebf81453c rescan: Optimize linking for simple stream nodes
Add immediate linking for common cases to reduce latency when new
audio/video streams are added. The full rescan mechanism is preserved
and still triggers for all session-item-added events via the
rescan-trigger hook.
2025-11-28 11:05:34 +00:00
Wim Taymans
ceed5dca7c meson: bump max channels to 128
Recompiling with the SPA_AUDIO_MAX_CHANNELS=128u defined, will create
larger audio_info structures that can hold more channels. Because
PipeWire is now using 128 channels, do the same in wireplumber so that
the generated PortConfig param can hold all channel positions.

See pipewire#4995
2025-11-26 10:35:00 +01:00
Evangelos Ribeiro Tzaras
84e4752f1a m-modem-manager: Prefer automatic cleanup
Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
9e390f1121 m-modem-manager: Avoid memory allocations unpacking string types
Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
133b82e61a m-modem-manager: Don't leak path
Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
9045d2439a m-modem-manager: Don't leak error
And include the error message while we're at it.

Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
278541f637 m-modem-manager: Set GDBusConnection before trying to use it
Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Evangelos Ribeiro Tzaras
27b6027649 m-modem-manager: Use correct error clearing function
We segfault otherwise on error:

  Thread 1 "wireplumber" received signal SIGSEGV, Segmentation fault.
  g_type_check_instance_is_fundamentally_a (type_instance=type_instance@entry=0x7fffec008120,
      fundamental_type=fundamental_type@entry=0x50 [GObject]) at ../../../gobject/gtype.c:3917
  warning: 3917	../../../gobject/gtype.c: No such file or directory
  (gdb) bt
  #0  g_type_check_instance_is_fundamentally_a
      (type_instance=type_instance@entry=0x7fffec008120, fundamental_type=fundamental_type@entry=0x50 [GObject])
      at ../../../gobject/gtype.c:3917
  #1  0x00007ffff7eafe3d in g_object_unref (_object=0x7fffec008120) at ../../../gobject/gobject.c:4743
  #2  0x00007ffff4784ec1 in list_calls_done (obj=<optimized out>, res=<optimized out>, data=0x5555556ab1b0)
      at ../modules/module-modem-manager.c:209

Fixes: 2794764d5a (m-modem-manager: add module for tracking status of voice calls)

Signed-off-by: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
2025-11-25 14:41:42 +02:00
Guido Günther
6398bf1bce docs: Switch to build directory for run invocation
There's no `Makefile` in the top level build directory so
switch to the build dir.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-11-24 23:23:51 +01:00
Julian Bouzas
f196d10e87 linking/rescan.lua: Clean a bit the code using the new properties API 2025-11-24 19:42:07 +02:00
Julian Bouzas
5071a85997 scripts: Fix compatibility issues with new Lua Properties API
We need to explicitly use Properties() in those cases as {} construct tables.
2025-11-24 19:42:07 +02:00
Julian Bouzas
15d98f59e5 state-routes: use get_count() instead of next()
We cannot use next() as the properties are not a Lua table anymore.
2025-11-24 19:42:07 +02:00
Julian Bouzas
be6f2b2926 m-lua-scripting: Handle both Properties and Lua tables in all Lua APIs 2025-11-24 19:42:07 +02:00
Julian Bouzas
01eb206460 m-lua-scripting: Add get_property() API for pipewire objects
This can be faster if we only want to get one property.
2025-11-24 19:42:07 +02:00
Julian Bouzas
5a4ecceee6 m-lua-scripting: Add get_property() API for session items
This can be faster if we only want to get one property.
2025-11-24 19:42:07 +02:00
Julian Bouzas
a35e40c1d2 m-lua-scripting: Add WpProperties API
Similar to Pod and Json, this API allows handling WpProperties references.
2025-11-24 19:42:07 +02:00
Julian Bouzas
2712cbb5a9 pipewire-object-mixin: Copy the props instead of wrapping them
This allows users to have full control of the properties when they get them.
2025-11-24 19:42:07 +02:00
Julian Bouzas
c68eb59017 device: Copy the props instead of wrapping them before emitting create-device signal
This allows the signal handlers to have full control of the properties.
2025-11-24 19:42:07 +02:00
Julian Bouzas
c0e047c241 apply-default-node: Make sure the metadata is valid
This fixes some warnings in the Lua tests scripts from tests/scripts/scripts/*
2025-11-24 19:42:07 +02:00
Julian Bouzas
238fd3c067 event-dispatcher: Sort hooks when registering them
This avoids sorting them constantly when collecting them. If a hook has a
circular dependency, a warning will be logged and the hook won't be registered.

See #824
2025-11-24 08:01:06 -05:00
Julian Bouzas
b80a0975c7 event-dispatcher: Register hooks for defined events in a hash table
Since all the current hooks are defined specifically for a particular event
type, we can register the hooks in a hash table using the event type as key
for faster event hook collection.

Also, hooks that are not specific to a particular event type, like constraints
such as 'event.type=*', will be registered in both the undefined hook list,
and also in all the hash table defined hook lists so they are always evaluated.

Even though 'wp_event_dispatcher_new_hooks_iterator()' can still be used, it is
now marked as deprecated because it is slower. The event hook collection uses
'wp_event_dispatcher_new_hooks_for_event_type_iterator()' now because it is
much faster.

Previously, the more hooks we were registering, the slower WirePlumber would
process events as all hooks needed to be evaluated for all events constantly.
This is not the case anymore with this patch. We can register thousands of
hooks, and if only 1 of those runs for a particular event, only 1 will be
evaluated instead of all of them.

See #824
2025-11-24 08:01:00 -05:00
qaqland
7ca21699a9 wpctl: add bash completions 2025-11-17 18:00:48 +08:00
Julian Bouzas
551353482a monitor/alsa: Also include alsa.* device properties for rule matching
UCM alsa nodes don't seem to have the 'alsa.*' properties from the device
included, which make it harder to match those nodes with alsa rules. This
patch adds all the 'alsa.*' properties in the UCM node to solve this.
2025-11-13 11:09:41 -05:00
George Kiagiadakis
f0b224b210 po: update Turkish translation
Closes: #871
2025-11-10 12:45:20 +02:00
George Kiagiadakis
0dad52f774 event-hook: simplify interest matching
We never use the feature of matching the subject type, so we can make
this simpler by not specifying CHECK_ALL, which also allows the
match_full() function to return early if some constraint doesn't match
instead of checking them all and wasting time.
2025-10-31 18:52:03 +02:00
Charles
27f97f6c45 monitors/alsa: Increase headroom for VMware and VirtualBox
Ubuntu received reports of very bad audio stuttering when running
25.10 in VMware & Virtualbox on Windows 11 hosts.

ac0d8ee4a8 dropped the headroom from
8192 samples to 2048. This seems fine for QEMU+KVM, but for unknown
reasons, the system emulation on Windows with these VMware/Virtualbox
is much slower, and xruns become frequent.

See:
https://bugs.launchpad.net/ubuntu/+source/wireplumber/+bug/2127250
https://discourse.ubuntu.com/t/vm-ubuntu2025-10-on-vmware-workstation-and-sound-problem/71066/3
2025-10-28 04:49:03 +00:00
filmsi
285230af67 Update Slovenian translation 2025-10-26 08:28:03 +00:00
Guido Günther
c095ae5254 role-based-policy: Allow to set target sink for media role loopbacks
There are streams that should go to a speaker rather than a headphone
or earpiece by default. Examples are alarms and emergency alerts on
phones. Allow to set a preference via
`policy.role-based.preferred-target` which then looks up the target
via `node.name` and `node.nick`.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-25 16:12:01 +03:00
Guido Günther
d2a49e8bc5 docs/linking: Fix typo
Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-25 16:12:01 +03:00
Guido Günther
b2c4993ab5 doc: Fix role based policy name
The role-priority-system doesn't exist anymore

Fixes: 0d995c56 ("wireplumber.conf: improve standard policy definition")
Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-25 16:12:01 +03:00
Julian Bouzas
ae30b4f022 state-profile: Handle new 'session.dont-restore-off-profile' property
This avoids restoring the Off profile if it was saved before. The property can
be set using the 'monitor.alsa.rules' section of the configuration.
2025-10-25 15:36:00 +03:00
Guido Günther
962be34a2b docs: Update rescan-for-linking priority
`rescan-for-linking` is the lowest priority linking event but the
`rescan-for-media-role-volume` event is lower overall priority.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:24:25 +02:00
Guido Günther
6f5ca5a79d wireplumber.conf: Enable default volume control tracking
Track a suitable default volume for media role based policies.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:20:33 +02:00
Guido Günther
5ecfe9f555 scripts: Add script to find a suitable volume control
When using role based priorities a volume slider presented to the
user should adjust the volume of the currently playing role.

E.g. when a phone has an incoming call and is ringing the default volume
slider should adjust the ringing volume and once the call has been
picked up it should adjust the call volume and once the call ended it
should adjust the media volume again.

It's currently hard for e.g. desktop shells to find out what a suitable
sink for volume control is so add a script to find a suitable target for
volume control (based on media role priority) by storing it's name in
the metadata.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:20:33 +02:00
Guido Günther
93377a8b4f m-std-event-source: Add rescan-for-media-role-volume
This allows us to do reduce the number of default volume updates.

Signed-off-by: Guido Günther <agx@sigxcpu.org>
2025-10-24 12:18:56 +02:00
Wim Taymans
ee72196500 m-si-audio-adapter: don't overread the position array
Limit the amount of channels we read from and write to the position
array with SPA_N_ELEMENTS(). The number of channels might be larger than
what we have positions for.
2025-10-21 16:31:00 +02:00
Julian Bouzas
e30c2a7cd9 state-routes.lua: Use the returned iterator from enum_params() to update routes info
This does not really fix anything but it saves some CPU cycles as we don't need
to get the route params from the cache anymore.
2025-10-17 09:05:27 -04:00
Julian Bouzas
4239055454 m-lua-scripting: Pass returned itrator to the closure when finishing enum_param()
We should not ignore the returned iterator as it allows users to get the exact
returned params after enumerating them, which might be useful in some cases.
2025-10-17 09:05:27 -04:00
Julian Bouzas
f188ddfb34 m-lua-scripting: Add WpIterator API
We skip the fold() API as the same thing can be done just using Lua closures.
2025-10-17 09:05:22 -04:00
Julian Bouzas
fb1738932b audio-group.lua: Demote creation of audio group log to info
This was never meant to be a warning message.
2025-10-17 15:49:02 +03:00
Julian Bouzas
f8be5a76e6 proc-utils: Make sure '/proc/<pid>/*' files exist before opening them
This avoids warnings if the specific file does not exist, especially when the
process has being removed quickly.

Fixes #816
2025-10-17 15:49:02 +03:00
Julian Bouzas
5c6a72e3cf m-si-standard-link: log error message when link activation fails
This makes debugging easier if a link fails to activate.
2025-10-17 08:41:48 -04:00
lumingzh
7b78078ed2 update Chinese translation 2025-10-14 10:01:43 +08:00
Julian Bouzas
6cfaf3f70d automute-alsa-routes.lua: Don't register/remove hooks if never registered/removed before
This avoids avent dispatcher errors in the log.
2025-10-13 16:15:34 +03:00
Julian Bouzas
2942903d0e scripts: Add node/filter-graph.lua 2025-10-13 15:48:41 +03:00
Arun Raghavan
38a21ea191 monitor/alsa: Add a setting to use HDMI channel detection
This allows us to set up the device to use HDMI ELD information for
channels. Not yet documented while we experiment with different ways to
make this work.
2025-10-10 15:44:48 -07:00
George Kiagiadakis
41b310c2d5 Add AGENTS.md
This helps steer LLMs when operating on this codebase - https://agents.md/

Mostly to help me with making releases for now
2025-10-10 18:03:07 +03:00
55 changed files with 2166 additions and 657 deletions

View file

@ -154,13 +154,22 @@ include:
# Fedora also ships that, but without the test plugins that we need...
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
https://gitlab.freedesktop.org/pipewire/pipewire.git
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
-Dvulkan=disabled -Dgstreamer=disabled -Dlibsystemd=disabled
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
-Decho-cancel-webrtc=disabled -Dsession-managers=[]
# Set build options based on PipeWire version
- |
case "$PIPEWIRE_HEAD" in
1.0|1.2|1.4)
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
;;
*)
export PIPEWIRE_BUILD_OPTIONS="-Dlibsystemd=disabled"
;;
esac
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" $PIPEWIRE_BUILD_OPTIONS
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled
-Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled
-Dgstreamer=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled
-Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled
-Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[]
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
# misc environment only for wireplumber
@ -236,6 +245,9 @@ build_on_fedora_no_docs:
stage: build
variables:
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
parallel:
matrix:
- PIPEWIRE_HEAD: ['master', '1.4', '1.2', '1.0']
build_on_ubuntu_with_gir:
extends:

37
AGENTS.md Normal file
View file

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

View file

@ -140,9 +140,9 @@ Policies
for enabling devices, linking streams, granting permissions to clients,
etc, as appropriate for a desktop system.
.. describe:: policy.role-priority-system
.. describe:: policy.role-based
Enables the role priority system policy. This system creates virtual sinks
Enables the role based priority system policy. This system creates virtual sinks
that group streams based on their ``media.role`` property, and assigns a
priority to each role. Depending on the priority configuration, lower
priority roles may be corked or ducked when a higher priority role stream

View file

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

View file

@ -42,7 +42,7 @@ assignments:
}
After that, once the media class of a device node has been select for a
After that, once the media class of a device node has been selected for a
particular stream node, and there are more than 1 device node matching such
media class, WirePlumber will select one based on a set of priorities:

View file

@ -438,7 +438,7 @@ spa_device_event_object_info (void *data, uint32_t id,
g_autoptr (WpProperties) props = NULL;
type = spa_debug_type_short_name (info->type);
props = wp_properties_new_wrap_dict (info->props);
props = wp_properties_new_copy_dict (info->props);
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
id, type, info->factory_name);

View file

@ -15,6 +15,161 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
typedef struct _HookData HookData;
struct _HookData
{
struct spa_list link;
WpEventHook *hook;
GPtrArray *dependencies;
};
static inline HookData *
hook_data_new (WpEventHook * hook)
{
HookData *hook_data = g_new0 (HookData, 1);
spa_list_init (&hook_data->link);
hook_data->hook = g_object_ref (hook);
hook_data->dependencies = g_ptr_array_new ();
return hook_data;
}
static void
hook_data_free (HookData *self)
{
g_clear_object (&self->hook);
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
g_free (self);
}
static inline void
record_dependency (struct spa_list *list, const gchar *target,
const gchar *dependency)
{
HookData *hook_data;
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
break;
}
}
}
static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
return TRUE;
}
}
}
return FALSE;
}
static gboolean
sort_hooks (GPtrArray *hooks)
{
struct spa_list collected, result, remaining;
HookData *sorted_hook_data = NULL;
spa_list_init (&collected);
spa_list_init (&result);
spa_list_init (&remaining);
for (guint i = 0; i < hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (hooks, i);
HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
}
if (!spa_list_is_empty (&collected)) {
HookData *hook_data;
/* convert "before" dependencies into "after" dependencies */
spa_list_for_each (hook_data, &collected, link) {
const gchar * const * strv =
wp_event_hook_get_runs_before_hooks (hook_data->hook);
while (strv && *strv) {
/* record hook_data->hook as a dependency of the *strv hook */
record_dependency (&collected, *strv,
wp_event_hook_get_name (hook_data->hook));
strv++;
}
}
/* sort */
while (!spa_list_is_empty (&collected)) {
gboolean made_progress = FALSE;
/* examine each hook to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
spa_list_consume (hook_data, &collected, link) {
guint deps_satisfied = 0;
spa_list_remove (&hook_data->link);
for (guint i = 0; i < hook_data->dependencies->len; i++) {
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
/* if the dependency is already in the sorted result list or if
it doesn't exist at all, we consider it satisfied */
if (hook_exists_in (dep, &result) ||
!(hook_exists_in (dep, &collected) ||
hook_exists_in (dep, &remaining))) {
deps_satisfied++;
}
}
if (deps_satisfied == hook_data->dependencies->len) {
spa_list_append (&result, &hook_data->link);
made_progress = TRUE;
} else {
spa_list_append (&remaining, &hook_data->link);
}
}
if (made_progress) {
/* run again with the remaining hooks */
spa_list_insert_list (&collected, &remaining);
spa_list_init (&remaining);
}
else if (!spa_list_is_empty (&remaining)) {
/* if we did not make any progress towards growing the result list,
it means the dependencies cannot be satisfied because of circles */
spa_list_consume (hook_data, &result, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
spa_list_consume (hook_data, &remaining, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
return FALSE;
}
}
}
/* clear hooks and add the sorted ones */
g_ptr_array_set_size (hooks, 0);
spa_list_consume (sorted_hook_data, &result, link) {
spa_list_remove (&sorted_hook_data->link);
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
hook_data_free (sorted_hook_data);
}
return TRUE;
}
typedef struct _EventData EventData;
struct _EventData
{
@ -49,7 +204,8 @@ struct _WpEventDispatcher
GObject parent;
GWeakRef core;
GPtrArray *hooks; /* registered hooks */
GHashTable *defined_hooks; /* registered hooks for defined events */
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
GSource *source; /* the event loop source */
GList *events; /* the events stack */
struct spa_system *system;
@ -160,7 +316,9 @@ static void
wp_event_dispatcher_init (WpEventDispatcher * self)
{
g_weak_ref_init (&self->core, NULL);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
(GDestroyNotify)g_ptr_array_unref);
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
((WpEventSource *) self->source)->dispatcher = self;
@ -184,7 +342,8 @@ wp_event_dispatcher_finalize (GObject * object)
close (self->eventfd);
g_clear_pointer (&self->hooks, g_ptr_array_unref);
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
g_weak_ref_clear (&self->core);
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
@ -284,6 +443,10 @@ void
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
WpEventHook * hook)
{
g_autoptr (GPtrArray) event_types = NULL;
gboolean is_defined = FALSE;
const gchar *hook_name;
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
@ -292,7 +455,74 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == NULL);
wp_event_hook_set_dispatcher (hook, self);
g_ptr_array_add (self->hooks, g_object_ref (hook));
/* Register the event hook in the defined hooks table if it is defined */
hook_name = wp_event_hook_get_name (hook);
event_types = wp_event_hook_get_matching_event_types (hook);
if (event_types) {
for (guint i = 0; i < event_types->len; i++) {
const gchar *event_type = g_ptr_array_index (event_types, i);
GPtrArray *hooks;
wp_debug_object (self, "Registering hook %s for defined event type %s",
hook_name, event_type);
/* Check if the event type was registered in the hash table */
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
if (hooks) {
g_ptr_array_add (hooks, g_object_ref (hook));
if (!sort_hooks (hooks))
goto sort_error;
} else {
GPtrArray *new_hooks = g_ptr_array_new_with_free_func (g_object_unref);
/* Add undefined hooks */
for (guint i = 0; i < self->undefined_hooks->len; i++) {
WpEventHook *uh = g_ptr_array_index (self->undefined_hooks, i);
g_ptr_array_add (new_hooks, g_object_ref (uh));
}
/* Add current hook */
g_ptr_array_add (new_hooks, g_object_ref (hook));
g_hash_table_insert (self->defined_hooks, g_strdup (event_type),
new_hooks);
if (!sort_hooks (new_hooks))
goto sort_error;
}
is_defined = TRUE;
}
}
/* Otherwise just register it as undefined hook */
if (!is_defined) {
GHashTableIter iter;
gpointer value;
wp_debug_object (self, "Registering hook %s for undefined event types",
hook_name);
/* Add it to the defined hooks table */
g_hash_table_iter_init (&iter, self->defined_hooks);
while (g_hash_table_iter_next (&iter, NULL, &value)) {
GPtrArray *defined_hooks = value;
g_ptr_array_add (defined_hooks, g_object_ref (hook));
if (!sort_hooks (defined_hooks))
goto sort_error;
}
/* Add it to the undefined hooks */
g_ptr_array_add (self->undefined_hooks, g_object_ref (hook));
if (!sort_hooks (self->undefined_hooks))
goto sort_error;
}
wp_info_object (self, "Registered hook %s successfully", hook_name);
return;
sort_error:
/* Unregister hook */
wp_event_dispatcher_unregister_hook (self, hook);
wp_warning_object (self,
"Could not register hook %s because of circular dependencies", hook_name);
}
/*!
@ -306,6 +536,9 @@ void
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpEventHook * hook)
{
GHashTableIter iter;
gpointer value;
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
@ -314,11 +547,29 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == self);
wp_event_hook_set_dispatcher (hook, NULL);
g_ptr_array_remove_fast (self->hooks, hook);
/* Remove hook from defined table and undefined list */
g_hash_table_iter_init (&iter, self->defined_hooks);
while (g_hash_table_iter_next (&iter, NULL, &value)) {
GPtrArray *defined_hooks = value;
g_ptr_array_remove (defined_hooks, hook);
}
g_ptr_array_remove (self->undefined_hooks, hook);
}
static void
add_unique (GPtrArray *array, WpEventHook * hook)
{
for (guint i = 0; i < array->len; i++)
if (g_ptr_array_index (array, i) == hook)
return;
g_ptr_array_add (array, g_object_ref (hook));
}
/*!
* \brief Returns an iterator to iterate over all the registered hooks
* \deprecated Use \ref wp_event_dispatcher_new_hooks_for_event_type_iterator
* instead.
* \ingroup wpeventdispatcher
*
* \param self the event dispatcher
@ -327,7 +578,56 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpIterator *
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
{
GPtrArray *items =
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
GHashTableIter iter;
gpointer value;
/* Add all defined hooks */
g_hash_table_iter_init (&iter, self->defined_hooks);
while (g_hash_table_iter_next (&iter, NULL, &value)) {
GPtrArray *hooks = value;
for (guint i = 0; i < hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (hooks, i);
add_unique (items, hook);
}
}
/* Add all undefined hooks */
for (guint i = 0; i < self->undefined_hooks->len; i++) {
WpEventHook *hook = g_ptr_array_index (self->undefined_hooks, i);
add_unique (items, hook);
}
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
}
/*!
* \brief Returns an iterator to iterate over the registered hooks for a
* particular event type.
* \ingroup wpeventdispatcher
*
* \param self the event dispatcher
* \param event_type the event type
* \return (transfer full): a new iterator
* \since 0.5.13
*/
WpIterator *
wp_event_dispatcher_new_hooks_for_event_type_iterator (
WpEventDispatcher * self, const gchar *event_type)
{
GPtrArray *items;
GPtrArray *hooks;
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
if (hooks) {
wp_debug_object (self, "Using %d defined hooks for event type %s",
hooks->len, event_type);
} else {
hooks = self->undefined_hooks;
wp_debug_object (self, "Using %d undefined hooks for event type %s",
hooks->len, event_type);
}
items = g_ptr_array_copy (hooks, (GCopyFunc) g_object_ref, NULL);
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
}

View file

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

View file

@ -254,6 +254,24 @@ wp_event_hook_run (WpEventHook * self,
callback_data);
}
/*!
* \brief Gets all the matching event types for this hook if any.
*
* \ingroup wpeventhook
* \param self the event hook
* \returns (element-type gchar*) (transfer full) (nullable): the matching
* event types for this hook if any.
* \since 0.5.13
*/
GPtrArray *
wp_event_hook_get_matching_event_types (WpEventHook * self)
{
g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
g_return_val_if_fail (
WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
}
/*!
* \brief Finishes the async operation that was started by wp_event_hook_run()
*
@ -321,36 +339,63 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
wp_interest_event_hook_get_instance_private (self);
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
g_autoptr (GObject) subject = wp_event_get_subject (event);
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
guint i;
WpObjectInterest *interest = NULL;
WpInterestMatch match;
const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
WP_INTEREST_MATCH_PW_PROPERTIES |
WP_INTEREST_MATCH_G_PROPERTIES);
for (i = 0; i < priv->interests->len; i++) {
interest = g_ptr_array_index (priv->interests, i);
match = wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
gtype, subject, properties, properties);
/* the interest may have a GType that matches the GType of the subject
or it may have WP_TYPE_EVENT as its GType, in which case it will
match any type of subject */
if (match == WP_INTEREST_MATCH_ALL)
if (wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_NONE,
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
return TRUE;
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
match = wp_object_interest_matches_full (interest, 0,
WP_TYPE_EVENT, NULL, NULL, NULL);
if (match & WP_INTEREST_MATCH_GTYPE)
return TRUE;
}
}
return FALSE;
}
static void
add_unique (GPtrArray *array, const gchar * lookup)
{
for (guint i = 0; i < array->len; i++)
if (g_str_equal (g_ptr_array_index (array, i), lookup))
return;
g_ptr_array_add (array, g_strdup (lookup));
}
static GPtrArray *
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
{
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
WpInterestEventHookPrivate *priv =
wp_interest_event_hook_get_instance_private (self);
GPtrArray *res = g_ptr_array_new_with_free_func (g_free);
guint i;
for (i = 0; i < priv->interests->len; i++) {
WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
g_autoptr (GPtrArray) values =
wp_object_interest_find_defined_constraint_values (interest,
WP_CONSTRAINT_TYPE_NONE, "event.type");
if (!values || values->len == 0) {
/* We always consider the hook undefined if it has at least one interest
* without a defined 'event.type' constraint */
return NULL;
} else {
for (guint j = 0; j < values->len; j++) {
GVariant *v = g_ptr_array_index (values, j);
if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
const gchar *v_str = g_variant_get_string (v, NULL);
add_unique (res, v_str);
}
}
}
}
}
return res;
}
static void
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
{
@ -359,6 +404,8 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
object_class->finalize = wp_interest_event_hook_finalize;
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
hook_class->get_matching_event_types =
wp_interest_event_hook_get_matching_event_types;
}
/*!

View file

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

View file

@ -17,37 +17,11 @@
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
typedef struct _HookData HookData;
struct _HookData
{
struct spa_list link;
WpEventHook *hook;
GPtrArray *dependencies;
};
static inline HookData *
hook_data_new (WpEventHook * hook)
{
HookData *hook_data = g_new0 (HookData, 1);
spa_list_init (&hook_data->link);
hook_data->hook = g_object_ref (hook);
hook_data->dependencies = g_ptr_array_new ();
return hook_data;
}
static void
hook_data_free (HookData *self)
{
g_clear_object (&self->hook);
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
g_free (self);
}
struct _WpEvent
{
grefcount ref;
GData *datalist;
struct spa_list hooks;
GPtrArray *hooks;
/* immutable fields */
gint priority;
@ -96,7 +70,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
WpEvent * self = g_new0 (WpEvent, 1);
g_ref_count_init (&self->ref);
g_datalist_init (&self->datalist);
spa_list_init (&self->hooks);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
self->priority = priority;
self->properties = properties ?
@ -155,11 +129,7 @@ wp_event_get_name(WpEvent *self)
static void
wp_event_free (WpEvent * self)
{
HookData *hook_data;
spa_list_consume (hook_data, &self->hooks, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
g_clear_pointer (&self->hooks, g_ptr_array_unref);
g_datalist_clear (&self->datalist);
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_object (&self->source);
@ -316,33 +286,6 @@ wp_event_get_data (WpEvent * self, const gchar * key)
return g_datalist_get_data (&self->datalist, key);
}
static inline void
record_dependency (struct spa_list *list, const gchar *target,
const gchar *dependency)
{
HookData *hook_data;
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
break;
}
}
}
static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
return TRUE;
}
}
}
return FALSE;
}
/*!
* \brief Collects all the hooks registered in the \a dispatcher that run for
* this \a event
@ -355,198 +298,36 @@ hook_exists_in (const gchar *hook_name, struct spa_list *list)
gboolean
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
{
struct spa_list collected, result, remaining;
g_autoptr (WpIterator) all_hooks = NULL;
g_auto (GValue) value = G_VALUE_INIT;
const gchar *event_type = NULL;
g_return_val_if_fail (event != NULL, FALSE);
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
/* hooks already collected */
if (!spa_list_is_empty (&event->hooks))
return TRUE;
/* Clear all current hooks */
g_ptr_array_set_size (event->hooks, 0);
spa_list_init (&collected);
spa_list_init (&result);
spa_list_init (&remaining);
/* Get the event type */
event_type = wp_properties_get (event->properties, "event.type");
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
event->name, event_type);
/* collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
/* Collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
event_type);
while (wp_iterator_next (all_hooks, &value)) {
WpEventHook *hook = g_value_get_object (&value);
if (wp_event_hook_runs_for_event (hook, event)) {
HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
g_ptr_array_add (event->hooks, g_object_ref (hook));
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
}
g_value_unset (&value);
}
if (!spa_list_is_empty (&collected)) {
HookData *hook_data;
/* convert "before" dependencies into "after" dependencies */
spa_list_for_each (hook_data, &collected, link) {
const gchar * const * strv =
wp_event_hook_get_runs_before_hooks (hook_data->hook);
while (strv && *strv) {
/* record hook_data->hook as a dependency of the *strv hook */
record_dependency (&collected, *strv,
wp_event_hook_get_name (hook_data->hook));
strv++;
return event->hooks->len > 0;
}
}
/* sort */
while (!spa_list_is_empty (&collected)) {
gboolean made_progress = FALSE;
/* examine each hook to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
spa_list_consume (hook_data, &collected, link) {
guint deps_satisfied = 0;
spa_list_remove (&hook_data->link);
wp_trace_boxed (WP_TYPE_EVENT, event,
"examining: %s", wp_event_hook_get_name (hook_data->hook));
for (guint i = 0; i < hook_data->dependencies->len; i++) {
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
/* if the dependency is already in the sorted result list or if
it doesn't exist at all, we consider it satisfied */
if (hook_exists_in (dep, &result) ||
!(hook_exists_in (dep, &collected) ||
hook_exists_in (dep, &remaining))) {
deps_satisfied++;
}
wp_trace_boxed (WP_TYPE_EVENT, event, "depends: %s, satisfied: %u/%u",
dep, deps_satisfied, hook_data->dependencies->len);
}
if (deps_satisfied == hook_data->dependencies->len) {
wp_trace_boxed (WP_TYPE_EVENT, event,
"sorted: "WP_OBJECT_FORMAT"(%s)",
WP_OBJECT_ARGS (hook_data->hook),
wp_event_hook_get_name (hook_data->hook));
spa_list_append (&result, &hook_data->link);
made_progress = TRUE;
} else {
spa_list_append (&remaining, &hook_data->link);
}
}
if (made_progress) {
/* run again with the remaining hooks */
spa_list_insert_list (&collected, &remaining);
spa_list_init (&remaining);
}
else if (!spa_list_is_empty (&remaining)) {
/* if we did not make any progress towards growing the result list,
it means the dependencies cannot be satisfied because of circles */
wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
"dependencies in the collected hooks!");
/* clean up */
spa_list_consume (hook_data, &result, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
spa_list_consume (hook_data, &remaining, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
return FALSE;
}
}
}
spa_list_insert_list (&event->hooks, &result);
return !spa_list_is_empty (&event->hooks);
}
struct event_hooks_iterator_data
{
WpEvent *event;
HookData *cur;
};
static void
event_hooks_iterator_reset (WpIterator *it)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
struct spa_list *list = &it_data->event->hooks;
if (!spa_list_is_empty (list))
it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
}
static gboolean
event_hooks_iterator_next (WpIterator *it, GValue *item)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
struct spa_list *list = &it_data->event->hooks;
if (!spa_list_is_empty (list) &&
!spa_list_is_end (it_data->cur, list, link)) {
g_value_init (item, WP_TYPE_EVENT_HOOK);
g_value_set_object (item, it_data->cur->hook);
it_data->cur = spa_list_next (it_data->cur, link);
return TRUE;
}
return FALSE;
}
static gboolean
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
gpointer data)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
struct spa_list *list = &it_data->event->hooks;
HookData *hook_data;
if (!spa_list_is_empty (list)) {
spa_list_for_each (hook_data, list, link) {
g_auto (GValue) item = G_VALUE_INIT;
g_value_init (&item, WP_TYPE_EVENT_HOOK);
g_value_set_object (&item, hook_data->hook);
if (!func (&item, ret, data))
return FALSE;
}
}
return TRUE;
}
static void
event_hooks_iterator_finalize (WpIterator *it)
{
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
wp_event_unref (it_data->event);
}
static const WpIteratorMethods event_hooks_iterator_methods = {
.version = WP_ITERATOR_METHODS_VERSION,
.reset = event_hooks_iterator_reset,
.next = event_hooks_iterator_next,
.fold = event_hooks_iterator_fold,
.finalize = event_hooks_iterator_finalize,
};
/*!
* \brief Returns an iterator that iterates over all the hooks that were
@ -558,15 +339,8 @@ static const WpIteratorMethods event_hooks_iterator_methods = {
WpIterator *
wp_event_new_hooks_iterator (WpEvent * event)
{
WpIterator *it = NULL;
struct event_hooks_iterator_data *it_data;
GPtrArray *hooks;
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL);
return wp_iterator_new_ptr_array (hooks, WP_TYPE_EVENT_HOOK);
g_return_val_if_fail (event != NULL, NULL);
it = wp_iterator_new (&event_hooks_iterator_methods,
sizeof (struct event_hooks_iterator_data));
it_data = wp_iterator_get_user_data (it);
it_data->event = wp_event_ref (event);
event_hooks_iterator_reset (it);
return it;
}

View file

@ -881,3 +881,51 @@ wp_object_interest_matches_full (WpObjectInterest * self,
}
return result;
}
/*!
* \brief Finds all the defined constraint values for a subject in \a self.
*
* A defined constraint value is the value of a constraint with the 'equal' or
* 'in-list' verb, because the full value must be defined with those verbs. This
* can be useful for cases where we want to enumerate interests that are
* interested in specific subjects.
*
* \ingroup wpobjectinterest
* \param self the object interest
* \param type the constraint type
* \param subject the subject that the constraint applies to
* \returns (element-type GVariant) (transfer full) (nullable): the defined
* constraint values for this object interest.
* \since 0.5.13
*/
GPtrArray *
wp_object_interest_find_defined_constraint_values (WpObjectInterest * self,
WpConstraintType type, const gchar * subject)
{
GPtrArray *res = g_ptr_array_new_with_free_func (
(GDestroyNotify)g_variant_unref);
struct constraint *c;
pw_array_for_each (c, &self->constraints) {
if ((c->type == type || WP_CONSTRAINT_TYPE_NONE == type) &&
g_str_equal (c->subject, subject)) {
switch (c->verb) {
case WP_CONSTRAINT_VERB_EQUALS:
g_ptr_array_add (res, g_variant_ref (c->value));
break;
case WP_CONSTRAINT_VERB_IN_LIST: {
GVariantIter iter;
GVariant *child;
g_variant_iter_init (&iter, c->value);
while ((child = g_variant_iter_next_value (&iter)))
g_ptr_array_add (res, child);
break;
}
default:
break;
}
}
}
return res;
}

View file

@ -130,6 +130,10 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
WpInterestMatchFlags flags, GType object_type, gpointer object,
WpProperties * pw_props, WpProperties * pw_global_props);
WP_API
GPtrArray * wp_object_interest_find_defined_constraint_values (
WpObjectInterest * self, WpConstraintType type, const gchar * subject);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
G_END_DECLS

View file

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

View file

@ -6,7 +6,9 @@
* SPDX-License-Identifier: MIT
*/
#include <fcntl.h>
#include <stdio.h>
#include <spa/utils/cleanup.h>
#include "log.h"
#include "proc-utils.h"
@ -145,6 +147,21 @@ wp_proc_info_get_cgroup (WpProcInfo * self)
return self->cgroup;
}
static FILE *
fdopenat (int dirfd, const char *path, int flags, const char *mode, mode_t perm)
{
int fd = openat (dirfd, path, flags, perm);
if (fd >= 0) {
FILE *f = fdopen (fd, mode);
if (f)
return f;
close (fd);
}
return NULL;
}
/*!
* \brief Gets the process information of a given PID
* \ingroup wpprocutils
@ -155,51 +172,46 @@ WpProcInfo *
wp_proc_utils_get_proc_info (pid_t pid)
{
WpProcInfo *ret = wp_proc_info_new (pid);
g_autofree gchar *status = NULL;
g_autoptr (GError) error = NULL;
gsize length = 0;
char path [64];
spa_autoclose int base_fd = -1;
FILE *file;
g_autofree gchar *line = NULL;
size_t size = 0;
snprintf (path, sizeof(path), "/proc/%d", pid);
base_fd = open (path,
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY, 0);
if (base_fd < 0) {
wp_info ("Could not open process info directory %s, skipping", path);
return ret;
}
/* Get parent PID */
{
g_autofree gchar *path = g_strdup_printf ("/proc/%d/status", pid);
if (g_file_get_contents (path, &status, &length, &error)) {
const gchar *loc = strstr (status, "\nPPid:");
if (loc) {
const gint res = sscanf (loc, "\nPPid:%d\n", &ret->parent);
if (!res || res == EOF)
wp_warning ("failed to parse status PPID for PID %d", pid);
} else {
wp_warning ("failed to find status parent PID for PID %d", pid);
}
} else {
wp_warning ("failed to get status for PID %d: %s", pid, error->message);
}
file = fdopenat (base_fd, "status",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
while (getline (&line, &size, file) > 1)
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
break;
fclose (file);
}
/* Get cgroup */
{
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cgroup", pid);
if (g_file_get_contents (path, &ret->cgroup, &length, &error)) {
if (length > 0)
ret->cgroup [length - 1] = '\0'; /* Remove EOF character */
} else {
wp_warning ("failed to get cgroup for PID %d: %s", pid, error->message);
}
file = fdopenat (base_fd, "cgroup",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
if (getline (&line, &size, file) > 1)
ret->cgroup = g_strstrip (g_strdup (line));
fclose (file);
}
/* Get args */
{
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cmdline", pid);
FILE *file = fopen (path, "rb");
file = fdopenat (base_fd, "cmdline",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
g_autofree gchar *lineptr = NULL;
size_t size = 0;
while (getdelim (&lineptr, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
ret->args[ret->n_args++] = g_strdup (lineptr);
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
ret->args[ret->n_args++] = g_strdup (line);
fclose (file);
} else {
wp_warning ("failed to get cmdline for PID %d: %m", pid);
}
}
return ret;

View file

@ -158,7 +158,25 @@ common_args = [
'-DG_LOG_USE_STRUCTURED',
'-DWP_USE_LOCAL_LOG_TOPIC_IN_G_LOG',
]
# Check if SPA_AUDIO_MAX_CHANNELS can be overridden
# (newer headers have #ifndef guards, older ones don't)
check_spa_max_channels_override = '''
#define SPA_AUDIO_MAX_CHANNELS 128u
#include <spa/param/audio/raw.h>
void main() { int x = SPA_AUDIO_MAX_CHANNELS; }
'''
spa_max_channels = 64
if cc.compiles(check_spa_max_channels_override,
dependencies: spa_dep,
args: ['-Werror'],
name: 'SPA_AUDIO_MAX_CHANNELS override')
common_args += ['-DSPA_AUDIO_MAX_CHANNELS=128u']
spa_max_channels = 128
endif
add_project_arguments(common_args, language: 'c')
summary({'SPA_AUDIO_MAX_CHANNELS': spa_max_channels})
i18n_conf = files()

View file

@ -146,8 +146,8 @@ static int
core_get_properties (lua_State *L)
{
WpCore * core = get_wp_core (L);
g_autoptr (WpProperties) p = wp_core_get_properties (core);
wplua_properties_to_table (L, p);
WpProperties *p = wp_core_get_properties (core);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
return 1;
}
@ -155,7 +155,7 @@ static int
core_get_info (lua_State *L)
{
WpCore * core = get_wp_core (L);
g_autoptr (WpProperties) p = wp_core_get_remote_properties (core);
WpProperties *p = wp_core_get_remote_properties (core);
lua_newtable (L);
lua_pushinteger (L, wp_core_get_remote_cookie (core));
@ -168,7 +168,7 @@ core_get_info (lua_State *L)
lua_setfield (L, -2, "host_name");
lua_pushstring (L, wp_core_get_remote_version (core));
lua_setfield (L, -2, "version");
wplua_properties_to_table (L, p);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
lua_setfield (L, -2, "properties");
return 1;
}
@ -297,8 +297,13 @@ static int
core_update_properties (lua_State *L)
{
WpCore *core = get_wp_core(L);
luaL_checktype (L, 1, LUA_TTABLE);
wp_core_update_properties (core, wplua_table_to_properties (L, 1));
WpProperties *props = NULL;
if (lua_istable (L, 1))
props = wplua_table_to_properties (L, 1);
else
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
wp_core_update_properties (core, props);
return 0;
}
@ -599,6 +604,28 @@ push_wpiterator (lua_State *L, WpIterator *it)
return 2;
}
static int
iterator_reset (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
wp_iterator_reset (it);
return 0;
}
static int
iterator_iterate (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
return push_wpiterator (L, wp_iterator_ref (it));
}
static const luaL_Reg iterator_funcs[] = {
{ "next", iterator_next },
{ "reset", iterator_reset },
{ "iterate", iterator_iterate },
{ NULL, NULL }
};
/* Settings WpIterator */
static int
@ -837,7 +864,11 @@ object_interest_matches (lua_State *L)
matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
}
else if (lua_istable (L, 2)) {
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
g_autoptr (WpProperties) props = NULL;
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
matches = wp_object_interest_matches (interest, props);
} else
luaL_argerror (L, 2, "expected GObject or table");
@ -997,10 +1028,11 @@ impl_metadata_new (lua_State *L)
const char *name = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L),
name, properties);
@ -1017,10 +1049,11 @@ device_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L),
factory, properties);
@ -1037,10 +1070,11 @@ spa_device_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L),
factory, properties);
@ -1105,10 +1139,11 @@ node_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (
wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
WpNode *d = wp_node_new_from_factory (get_wp_export_core (L),
factory, properties);
@ -1214,10 +1249,11 @@ impl_node_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L),
factory, properties);
@ -1250,10 +1286,11 @@ link_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties);
if (l)
@ -1329,9 +1366,12 @@ static int
client_update_properties (lua_State *L)
{
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
WpProperties *properties = NULL;
luaL_checktype (L, 2, LUA_TTABLE);
WpProperties *properties = wplua_table_to_properties (L, 2);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
wp_client_update_properties (client, properties);
return 0;
@ -1391,46 +1431,12 @@ static int
session_item_configure (lua_State *L)
{
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
WpProperties *props = wp_properties_new_empty ();
WpProperties *props;
/* validate arguments */
luaL_checktype (L, 2, LUA_TTABLE);
/* build the configuration properties */
lua_pushnil (L);
while (lua_next (L, 2)) {
const gchar *key = NULL;
g_autofree gchar *var = NULL;
switch (lua_type (L, -1)) {
case LUA_TBOOLEAN:
var = g_strdup_printf ("%u", lua_toboolean (L, -1));
break;
case LUA_TNUMBER:
if (lua_isinteger (L, -1))
var = g_strdup_printf ("%lld", lua_tointeger (L, -1));
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
var = g_strdup_printf ("%f", lua_tonumber (L, -1));
break;
case LUA_TSTRING:
var = g_strdup (lua_tostring (L, -1));
break;
case LUA_TUSERDATA: {
GValue *v = lua_touserdata (L, -1);
gpointer p = g_value_peek_pointer (v);
var = g_strdup_printf ("%p", p);
break;
}
default:
luaL_error (L, "configure does not support lua type ",
lua_typename(L, lua_type(L, -1)));
break;
}
key = luaL_tolstring (L, -2, NULL);
wp_properties_set (props, key, var);
lua_pop (L, 2);
}
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
lua_pushboolean (L, wp_session_item_configure (si, props));
return 1;
@ -1452,12 +1458,23 @@ session_item_remove (lua_State *L)
return 0;
}
static int
session_item_get_property (lua_State *L)
{
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_session_item_get_property (si, key);
lua_pushstring (L, val);
return 1;
}
static const luaL_Reg session_item_methods[] = {
{ "get_associated_proxy", session_item_get_associated_proxy },
{ "reset", session_item_reset },
{ "configure", session_item_configure },
{ "register", session_item_register },
{ "remove", session_item_remove },
{ "get_property", session_item_get_property },
{ NULL, NULL }
};
@ -1527,19 +1544,24 @@ on_enum_params_done (WpPipewireObject * pwobj, GAsyncResult * res,
GClosure * closure)
{
g_autoptr (GError) error = NULL;
GValue val = G_VALUE_INIT;
int n_vals = 0;
GValue vals[2] = { G_VALUE_INIT, G_VALUE_INIT };
int n_vals = 1;
WpIterator *it;
it = wp_pipewire_object_enum_params_finish (pwobj, res, &error);
g_value_init (&vals[0], WP_TYPE_ITERATOR);
g_value_set_boxed (&vals[0], it);
if (!it) {
g_value_init (&val, G_TYPE_STRING);
g_value_set_string (&val, error->message);
n_vals = 1;
g_value_init (&vals[1], G_TYPE_STRING);
g_value_set_string (&vals[1], error->message);
n_vals = 2;
}
g_clear_pointer (&it, wp_iterator_unref);
g_closure_invoke (closure, NULL, n_vals, &val, NULL);
g_value_unset (&val);
g_closure_invoke (closure, NULL, n_vals, vals, NULL);
g_value_unset (&vals[0]);
g_value_unset (&vals[1]);
g_closure_invalidate (closure);
g_closure_unref (closure);
}
@ -1575,11 +1597,22 @@ pipewire_object_set_param (lua_State *L)
return 0;
}
static int
pipewire_object_get_property (lua_State *L)
{
WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_pipewire_object_get_property (pwobj, key);
lua_pushstring (L, val);
return 1;
}
static const luaL_Reg pipewire_object_methods[] = {
{ "enum_params", pipewire_object_enum_params },
{ "iterate_params", pipewire_object_iterate_params },
{ "set_param" , pipewire_object_set_param },
{ "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
{ "get_property", pipewire_object_get_property },
{ NULL, NULL }
};
@ -1606,9 +1639,14 @@ static int
state_save (lua_State *L)
{
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
luaL_checktype (L, 2, LUA_TTABLE);
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
g_autoptr (WpProperties) props = NULL;
g_autoptr (GError) error = NULL;
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
gboolean saved = wp_state_save (state, props, &error);
lua_pushboolean (L, saved);
lua_pushstring (L, error ? error->message : "");
@ -1619,8 +1657,13 @@ static int
state_save_after_timeout (lua_State *L)
{
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
luaL_checktype (L, 2, LUA_TTABLE);
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
g_autoptr (WpProperties) props = NULL;
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
wp_state_save_after_timeout (state, get_wp_core (L), props);
return 0;
}
@ -1629,8 +1672,8 @@ static int
state_load (lua_State *L)
{
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
g_autoptr (WpProperties) props = wp_state_load (state);
wplua_properties_to_table (L, props);
WpProperties *props = wp_state_load (state);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1;
}
@ -1655,10 +1698,11 @@ impl_module_new (lua_State *L)
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
args = luaL_checkstring (L, 2);
if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
luaL_checktype (L, 3, LUA_TTABLE);
if (lua_istable (L, 3))
properties = wplua_table_to_properties (L, 3);
}
else if (!lua_isnone (L, 3) && !lua_isnil (L, 3))
properties = wp_properties_ref (wplua_checkboxed (L, 3,
WP_TYPE_PROPERTIES));
WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
name, args, properties);
@ -1681,9 +1725,10 @@ conf_new (lua_State *L)
WpProperties *p = NULL;
WpConf *conf = NULL;
if (lua_istable (L, 2)) {
if (lua_istable (L, 2))
p = wplua_table_to_properties (L, 2);
}
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
p = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
conf = wp_conf_new (path, p);
if (conf) {
@ -1721,7 +1766,7 @@ conf_get_section_as_properties (lua_State *L)
const char *section = NULL;
g_autoptr (WpConf) conf = NULL;
g_autoptr (WpSpaJson) s = NULL;
g_autoptr (WpProperties) props = NULL;
WpProperties *props = NULL;
int argi = 1;
/* check if called as method on object */
@ -1736,6 +1781,8 @@ conf_get_section_as_properties (lua_State *L)
if (lua_istable (L, argi))
props = wplua_table_to_properties (L, argi);
else if (!lua_isnone (L, argi) && !lua_isnil (L, argi))
props = wp_properties_ref (wplua_checkboxed (L, argi, WP_TYPE_PROPERTIES));
else
props = wp_properties_new_empty ();
@ -1744,7 +1791,7 @@ conf_get_section_as_properties (lua_State *L)
if (s && wp_spa_json_is_object (s))
wp_properties_update_from_json (props, s);
}
wplua_properties_to_table (L, props);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1;
}
@ -1901,10 +1948,12 @@ json_utils_match_rules (lua_State *L)
gboolean res;
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
luaL_checktype (L, 2, LUA_TTABLE);
luaL_checktype (L, 3, LUA_TFUNCTION);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
L, &error);
@ -1920,17 +1969,21 @@ json_utils_match_rules (lua_State *L)
static int
json_utils_match_rules_update_properties (lua_State *L)
{
g_autoptr (WpProperties) properties = NULL;
WpProperties *properties = NULL;
WpSpaJson *json;
int count;
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
luaL_checktype (L, 2, LUA_TTABLE);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
count = wp_json_utils_match_rules_update_properties (json, properties);
wplua_properties_to_table (L, properties);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, properties);
lua_pushinteger (L, count);
return 2;
}
@ -2012,6 +2065,108 @@ static const luaL_Reg proc_utils_funcs[] = {
{ NULL, NULL }
};
/* Properties */
static int
properties_new (lua_State *L)
{
WpProperties *props;
if (lua_istable (L, 1))
props = wplua_table_to_properties (L, 1);
else if (!lua_isnone (L, 1) && !lua_isnil (L, 1))
props = wp_properties_ref (wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES));
else
props = wp_properties_new_empty ();
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1;
}
static int
properties_get_boolean (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_properties_get (props, key);
if (val)
lua_pushboolean (L, spa_atob (val));
else
lua_pushnil (L);
return 1;
}
static int
properties_get_int (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_properties_get (props, key);
if (val) {
gint64 int_val = 0;
if (spa_atoi64 (val, &int_val, 10))
lua_pushinteger (L, int_val);
else
lua_pushnil (L);
} else {
lua_pushnil (L);
}
return 1;
}
static int
properties_get_float (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
const char *key = luaL_checkstring (L, 2);
const char *val = wp_properties_get (props, key);
if (val) {
double d_val = 0;
if (spa_atod (val, &d_val))
lua_pushnumber (L, d_val);
else
lua_pushnil (L);
} else {
lua_pushnil (L);
}
return 1;
}
static int
properties_get_count (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
lua_pushinteger (L, wp_properties_get_count (props));
return 1;
}
static int
properties_copy (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
WpProperties *copy = wp_properties_copy (props);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, copy);
return 1;
}
static int
properties_parse (lua_State *L)
{
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
wplua_properties_to_table (L, props);
return 1;
}
static const luaL_Reg properties_funcs[] = {
{ "get_boolean", properties_get_boolean },
{ "get_int", properties_get_int },
{ "get_float", properties_get_float },
{ "get_count", properties_get_count },
{ "copy", properties_copy },
{ "parse", properties_parse },
{ NULL, NULL }
};
/* WpSettings */
static int
@ -2305,8 +2460,8 @@ static int
event_get_properties (lua_State *L)
{
WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
g_autoptr (WpProperties) props = wp_event_get_properties (event);
wplua_properties_to_table (L, props);
WpProperties *props = wp_event_get_properties (event);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
return 1;
}
@ -2428,10 +2583,11 @@ event_dispatcher_push_event (lua_State *L)
lua_pop (L, 1);
lua_pushliteral (L, "properties");
if (lua_gettable (L, 1) != LUA_TNIL) {
luaL_checktype (L, -1, LUA_TTABLE);
if (lua_istable (L, -1))
properties = wplua_table_to_properties (L, -1);
}
else if (!lua_isnil (L, -1) && !lua_isnone (L, -1) && !lua_isstring (L, -1))
properties = wp_properties_ref (
wplua_checkboxed (L, -1, WP_TYPE_PROPERTIES));
lua_pop (L, 1);
lua_pushliteral (L, "source");
@ -2984,6 +3140,10 @@ wp_lua_scripting_api_init (lua_State *L)
conf_new, conf_methods);
wplua_register_type_methods (L, WP_TYPE_PROC_INFO,
NULL, proc_info_funcs);
wplua_register_type_methods (L, WP_TYPE_ITERATOR,
NULL, iterator_funcs);
wplua_register_type_methods (L, WP_TYPE_PROPERTIES,
properties_new, properties_funcs);
if (!wplua_load_uri (L, URI_API, &error) ||
!wplua_pcall (L, 0, 0, &error)) {

View file

@ -217,6 +217,7 @@ SANDBOX_EXPORT = {
Conf = WpConf,
JsonUtils = JsonUtils,
ProcUtils = ProcUtils,
Properties = WpProperties_new,
SimpleEventHook = WpSimpleEventHook_new,
AsyncEventHook = WpAsyncEventHook_new,
}

View file

@ -300,6 +300,7 @@ spa_json_object_new (lua_State *L)
{
g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object ();
if (lua_istable (L, 1)) {
luaL_checktype (L, 1, LUA_TTABLE);
lua_pushnil (L);
@ -335,6 +336,19 @@ spa_json_object_new (lua_State *L)
lua_pop (L, 1);
}
} else {
WpProperties *props = wplua_checkboxed (L, 1, WP_TYPE_PROPERTIES);
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
for (it = wp_properties_new_iterator (props); wp_iterator_next (it, &item);
g_value_unset (&item)) {
WpPropertiesItem *pi = g_value_get_boxed (&item);
const gchar *key = wp_properties_item_get_key (pi);
const gchar *value = wp_properties_item_get_value (pi);
wp_spa_json_builder_add_property (builder, key);
wp_spa_json_builder_add_string (builder, value);
}
}
wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));
return 1;

View file

@ -29,8 +29,9 @@ _wplua_gboxed___index (lua_State *L)
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
luaL_argcheck (L, obj_v != NULL, 1,
"expected userdata storing GValue<GBoxed>");
const gchar *key = luaL_checkstring (L, 2);
const gchar *key = luaL_tolstring (L, 2, NULL);
GType type = G_VALUE_TYPE (obj_v);
GType boxed_type = type;
lua_CFunction func = NULL;
GHashTable *vtables;
@ -53,6 +54,104 @@ _wplua_gboxed___index (lua_State *L)
lua_pushcfunction (L, func);
return 1;
}
/* If WpProperties type, just return the property value for that key */
if (boxed_type == WP_TYPE_PROPERTIES) {
WpProperties * props = g_value_get_boxed (obj_v);
const gchar *val = wp_properties_get (props, key);
lua_pushstring (L, val);
return 1;
}
return 0;
}
static int
_wplua_gboxed___newindex (lua_State *L)
{
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
luaL_argcheck (L, obj_v != NULL, 1,
"expected userdata storing GValue<GBoxed>");
const gchar *key = luaL_tolstring (L, 2, NULL);
GType type = G_VALUE_TYPE (obj_v);
/* Set property value */
if (type == WP_TYPE_PROPERTIES) {
WpProperties * props = g_value_dup_boxed (obj_v);
g_autofree gchar *val = NULL;
luaL_checkany (L, 3);
switch (lua_type (L, 3)) {
case LUA_TNIL:
break;
case LUA_TUSERDATA: {
if (wplua_gvalue_userdata_type (L, 3) != G_TYPE_INVALID) {
GValue *v = lua_touserdata (L, 3);
gpointer p = g_value_peek_pointer (v);
val = g_strdup_printf ("%p", p);
break;
} else {
val = g_strdup (luaL_tolstring (L, 3, NULL));
break;
}
}
default:
val = g_strdup (luaL_tolstring (L, 3, NULL));
break;
}
props = wp_properties_ensure_unique_owner (props);
wp_properties_set (props, key, val);
g_value_take_boxed (obj_v, props);
} else {
luaL_error (L, "cannot assign property '%s' to boxed type %s",
key, g_type_name (type));
}
return 0;
}
static int
properties_iterator_next (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
g_auto (GValue) item = G_VALUE_INIT;
if (wp_iterator_next (it, &item)) {
WpPropertiesItem *si = g_value_get_boxed (&item);
const gchar *k = wp_properties_item_get_key (si);
const gchar *v = wp_properties_item_get_value (si);
lua_pushstring (L, k);
lua_pushstring (L, v);
return 2;
} else {
lua_pushnil (L);
lua_pushnil (L);
return 2;
}
}
static int
push_properties_wpiterator (lua_State *L, WpIterator *it)
{
lua_pushcfunction (L, properties_iterator_next);
wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
return 2;
}
static int
_wplua_gboxed___pairs (lua_State *L)
{
GValue *obj_v = _wplua_togvalue_userdata_named (L, 1, G_TYPE_BOXED, "GBoxed");
luaL_argcheck (L, obj_v != NULL, 1,
"expected userdata storing GValue<GBoxed>");
GType type = G_VALUE_TYPE (obj_v);
if (type == WP_TYPE_PROPERTIES) {
WpProperties * props = g_value_get_boxed (obj_v);
WpIterator *it = wp_properties_new_iterator (props);
return push_properties_wpiterator (L, it);
} else {
luaL_error (L, "cannot do pairs of boxed type %s", g_type_name (type));
}
return 0;
}
@ -69,6 +168,8 @@ _wplua_init_gboxed (lua_State *L)
{ "__gc", _wplua_gvalue_userdata___gc },
{ "__eq", _wplua_gboxed___eq },
{ "__index", _wplua_gboxed___index },
{ "__newindex", _wplua_gboxed___newindex },
{ "__pairs", _wplua_gboxed___pairs },
{ NULL, NULL }
};

View file

@ -14,7 +14,6 @@ WpProperties *
wplua_table_to_properties (lua_State *L, int idx)
{
WpProperties *p = wp_properties_new_empty ();
const gchar *key, *value;
int table = lua_absindex (L, idx);
if (lua_type (L, table) != LUA_TTABLE) {
@ -24,12 +23,35 @@ wplua_table_to_properties (lua_State *L, int idx)
lua_pushnil(L);
while (lua_next (L, table) != 0) {
const gchar *key = luaL_tolstring (L, -2, NULL);
g_autofree gchar *value = NULL;
/* copy key & value to convert them to string */
key = luaL_tolstring (L, -2, NULL);
value = luaL_tolstring (L, -2, NULL);
wp_properties_set (p, key, value);
luaL_checkany (L, -2);
switch (lua_type (L, -2)) {
case LUA_TNIL:
lua_pop (L, 2);
break;
case LUA_TUSERDATA: {
if (wplua_gvalue_userdata_type(L, -2) != G_TYPE_INVALID) {
GValue *v = lua_touserdata (L, -2);
gpointer p = g_value_peek_pointer (v);
value = g_strdup_printf ("%p", p);
lua_pop (L, 2);
} else {
value = g_strdup (luaL_tolstring (L, -2, NULL));
lua_pop (L, 3);
}
break;
}
default:
value = g_strdup (luaL_tolstring (L, -2, NULL));
lua_pop (L, 3);
break;
}
wp_properties_set (p, key, value);
}
/* sort, because the lua table has a random order and it's too messy to read */
wp_properties_sort (p);
@ -313,9 +335,6 @@ wplua_gvalue_to_lua (lua_State *L, const GValue *v)
lua_pushlightuserdata (L, g_value_get_pointer (v));
break;
case G_TYPE_BOXED:
if (G_VALUE_TYPE (v) == WP_TYPE_PROPERTIES)
wplua_properties_to_table (L, g_value_get_boxed (v));
else
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
break;
case G_TYPE_OBJECT:

View file

@ -103,14 +103,15 @@ static void
bind_call (GObject * obj, GAsyncResult * res, gpointer data)
{
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
GError *err = NULL;
g_autoptr (GError) err = NULL;
GDBusProxy *call;
GVariant *prop;
g_autoptr (GVariant) prop = NULL;
gint init_state;
call = g_dbus_proxy_new_finish (res, &err);
if (call == NULL) {
wp_warning_object (wpmm, "Failed to get call");
g_prefix_error (&err, "Failed to get call: ");
wp_warning_object (wpmm, "%s", err->message);
return;
}
@ -122,8 +123,6 @@ bind_call (GObject * obj, GAsyncResult * res, gpointer data)
if (is_active_state (init_state))
active_calls_inc (wpmm);
g_variant_unref (prop);
}
wpmm->calls = g_list_prepend (wpmm->calls, call);
@ -165,7 +164,7 @@ on_voice_signal (GDBusProxy * iface,
g_object_get (wpmm->dbus, "connection", &conn, NULL);
if (!g_strcmp0 (signal, "CallAdded")) {
g_variant_get (params, "(o)", &path);
g_variant_get (params, "(&o)", &path);
g_dbus_proxy_new (conn,
G_DBUS_PROXY_FLAGS_NONE,
NULL,
@ -175,9 +174,8 @@ on_voice_signal (GDBusProxy * iface,
NULL,
bind_call,
wpmm);
g_free (path);
} else if (!g_strcmp0 (signal, "CallDeleted")) {
g_variant_get (params, "(o)", &path);
g_variant_get (params, "(&o)", &path);
// The user shouldn't have hundreds of calls, so just linear search.
deleted = g_list_find_custom (wpmm->calls, path, match_call_path);
@ -185,8 +183,6 @@ on_voice_signal (GDBusProxy * iface,
g_object_unref (deleted->data);
wpmm->calls = g_list_delete_link (wpmm->calls, deleted);
}
g_free (path);
}
}
@ -196,22 +192,23 @@ list_calls_done (GObject * obj,
gpointer data)
{
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
GVariant *params;
GVariantIter *calls;
g_autoptr (GVariant) params = NULL;
g_autoptr (GVariantIter) calls = NULL;
gchar *path;
GError *err = NULL;
g_autoptr (GError) err = NULL;
g_autoptr (GDBusConnection) conn = NULL;
params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err);
if (params == NULL) {
g_prefix_error (&err, "Failed to list active calls on startup: ");
wp_warning_object (wpmm, "%s", err->message);
g_clear_object (&err);
return;
}
g_object_get (wpmm->dbus, "connection", &conn, NULL);
g_variant_get (params, "(ao)", &calls);
while (g_variant_iter_loop (calls, "o", &path)) {
while (g_variant_iter_loop (calls, "&o", &path)) {
g_dbus_proxy_new (conn,
G_DBUS_PROXY_FLAGS_NONE,
NULL,
@ -222,9 +219,6 @@ list_calls_done (GObject * obj,
bind_call,
wpmm);
}
g_variant_iter_free (calls);
g_variant_unref (params);
}
static void
@ -353,7 +347,7 @@ static void
wp_modem_manager_enable (WpPlugin * self, WpTransition * transition)
{
WpModemManager *wpmm = WP_MODEM_MANAGER (self);
WpCore *core;
g_autoptr (WpCore) core = NULL;
GError *err;
g_autoptr (GDBusConnection) conn = NULL;

View file

@ -137,10 +137,11 @@ si_audio_adapter_get_default_clock_rate (WpSiAudioAdapter * self)
static gboolean
is_unpositioned (struct spa_audio_info_raw *info)
{
uint32_t i;
uint32_t i, n_pos;
if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
return TRUE;
for (i = 0; i < info->channels; i++)
n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
for (i = 0; i < n_pos; i++)
if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
return TRUE;
@ -197,7 +198,7 @@ si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node,
continue;
if (position == NULL ||
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS))
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_N_ELEMENTS(raw_format.position)))
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
if (mono) {
@ -349,7 +350,8 @@ format_audio_raw_build (const struct spa_audio_info_raw *info)
if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
/* Build the position array spa pod */
g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
for (guint i = 0; i < info->channels; i++)
guint n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
for (guint i = 0; i < n_pos; i++)
wp_spa_pod_builder_add_id (position_builder, info->position[i]);
/* Add the position property */

View file

@ -195,12 +195,16 @@ on_link_activated (WpObject * proxy, GAsyncResult * res,
{
WpSiStandardLink *self = wp_transition_get_source_object (transition);
guint len = self->node_links ? self->node_links->len : 0;
g_autoptr (GError) error = NULL;
/* Count the number of failed and active links */
if (wp_object_activate_finish (proxy, res, NULL))
if (wp_object_activate_finish (proxy, res, &error)) {
self->n_active_links++;
else
} else {
self->n_failed_links++;
wp_info_object (self, "Failed to activate link %p: %s", proxy,
error->message);
}
/* Wait for all links to finish activation */
if (self->n_failed_links + self->n_active_links != len)

View file

@ -38,6 +38,7 @@ typedef enum {
typedef enum {
RESCAN_CONTEXT_LINKING,
RESCAN_CONTEXT_DEFAULT_NODES,
RESCAN_CONTEXT_MEDIA_ROLE_VOLUME,
N_RESCAN_CONTEXTS,
} RescanContext;
@ -48,6 +49,7 @@ rescan_context_get_type (void)
static const GEnumValue values[] = {
{ RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" },
{ RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" },
{ RESCAN_CONTEXT_MEDIA_ROLE_VOLUME, "RESCAN_CONTEXT_MEDIA_ROLE_VOLUME", "media-role-volume" },
{ 0, NULL, NULL }
};
if (g_once_init_enter (&gtype_id)) {
@ -161,6 +163,8 @@ get_default_event_priority (const gchar *event_type)
return -490;
else if (!g_strcmp0 (event_type, "rescan-for-linking"))
return -500;
else if (!g_strcmp0 (event_type, "rescan-for-media-role-volume"))
return -510;
else if (!g_strcmp0 (event_type, "node-state-changed"))
return 50;
else if (!g_strcmp0 (event_type, "metadata-changed"))

View file

@ -123,6 +123,16 @@ msgstr ""
msgid "Ducking level"
msgstr ""
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid "Automatically detect channel count and positions for HDMI devices (experimental)"
msgstr ""
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr ""
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
@ -155,7 +165,7 @@ msgstr ""
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio nodes in MONO"
msgid "Configure all audio device sink nodes in MONO"
msgstr ""
#. /wireplumber.settings.schema/node.features.audio.mono/name

View file

@ -7,19 +7,17 @@
msgid ""
msgstr ""
"Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2025-08-21 03:57+0000\n"
"PO-Revision-Date: 2025-08-21 15:45+0200\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
"PO-Revision-Date: 2025-12-15 23:31+0100\n"
"Last-Translator: Martin Srebotnjak <miles@filmsi.net>\n"
"Language-Team: Slovenian GNOME Translation Team <gnome-si@googlegroups.com>\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n"
"%100==4 ? 3 : 0);\n"
"X-Generator: Poedit 2.2.1\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n"
"X-Generator: Poedit 3.8\n"
#. WirePlumber
#.
@ -49,7 +47,7 @@ msgstr "Razdeli %s"
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
@ -75,6 +73,7 @@ msgstr "Modem"
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
@ -201,6 +200,34 @@ msgstr "Privzeta glasnost za zvočne vire"
msgid "Default source volume"
msgstr "Privzeta izvorna glasnost"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"Samodejno utišaj vse zvočne naprave, ko so aktivne žične slušalke/zvočniki "
"odklopljeni, za preprečitev nenamernega zvočnega izhoda"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Samodejna utišaj zvok pri prekinitvi žične zvokovne povezave"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"Samodejno utišaj vse zvočne naprave, ko so aktivne slušalke/zvočniki "
"Bluetooth odklopljeni, da preprečite nenamerni izhod zvoka"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Samodejni utišaj pri prekinitvi zvokovne povezave Bluetooth"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
@ -247,6 +274,19 @@ msgstr ""
msgid "Ducking level"
msgstr "Stopnja umikanja"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"Samodejno zaznaj število kanalov in položaje za naprave HDMI (poskusno)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "Samodejno zaznaj kanale HDMI (poskusno)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
@ -277,6 +317,16 @@ msgstr "Omogoči vrata nadzornih zvočnikov na zvočnih vozliščih"
msgid "Monitor ports"
msgstr "Vrata zvočnih monitorjev"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "Prilagodi vsa vozlišča zvokovnih ponorov v MONO"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Mono"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"

400
po/tr.po
View file

@ -1,26 +1,25 @@
# Turkish translation for PipeWire.
# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
# This file is distributed under the same license as the PipeWire package.
# Necdet Yücel <necdetyucel@gmail.com>, 2014.
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
# Oğuz Ersen <oguzersen@protonmail.com>, 2021.
# Turkish translation for WirePlumber.
# Copyright (C) 2025 WirePlumber's COPYRIGHT HOLDER
# This file is distributed under the same license as the WirePlumber package.
#
# Sabri Ünal <yakushabb@gmail.com>, 2025.
# Emin Tufan Çetin <etcetin@gmail.com>, 2025
#
msgid ""
msgstr ""
"Project-Id-Version: PipeWire master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-12-06 21:31+0300\n"
"Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n"
"Language-Team: Turkish <tr>\n"
"Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2025-11-09 04:07+0000\n"
"PO-Revision-Date: 2025-11-09 08:00+0300\n"
"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
"Language-Team: Turkish <takim@gnome.org.tr>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.4.2\n"
"X-Generator: Poedit 3.8\n"
#. WirePlumber
#.
@ -28,13 +27,19 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. unique device/node name tables
#. SPA ids to node names: name = id_name_table[device_id][node_id]
#. create the underlying hidden ALSA node
#. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Bölük %s"
#. Connect ObjectConfig events to the right node
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. try to negotiate the max amount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
@ -45,15 +50,360 @@ msgstr ""
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Dahili Ses"
#: src/scripts/monitors/alsa.lua:438
msgid "Loopback"
msgstr "Geri Döngü"
#: src/scripts/monitors/alsa.lua:224
#: src/scripts/monitors/alsa.lua:440
msgid "Built-in Audio"
msgstr "Yerleşik Ses"
#: src/scripts/monitors/alsa.lua:442
msgid "Modem"
msgstr "Modem"
#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from the rules in the configuration file
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:114
#, lua-format
msgid "BLE MIDI %d"
msgstr "BLE MIDI %d"
#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#.
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Yerleşik Ön Kamera"
#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Yerleşik Arka Kamera"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/description
#: wireplumber.conf
msgid ""
"Always show microphone for Bluetooth headsets, and switch to headset mode "
"when recording"
msgstr ""
"Bluetooth kulaklıklar için her zaman mikrofonu göster ve kayıt sırasında "
"kulaklık kipine geç"
#. /wireplumber.settings.schema/bluetooth.autoswitch-to-headset-profile/name
#: wireplumber.conf
msgid "Auto-switch to headset profile"
msgstr "Kulaklık profiline kendiliğinden geç"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/description
#: wireplumber.conf
msgid "Remember and restore Bluetooth headset mode status"
msgstr "Bluetooth kulaklık kipi durumunu anımsa ve geri yükle"
#. /wireplumber.settings.schema/bluetooth.use-persistent-storage/name
#: wireplumber.conf
msgid "Persistent storage"
msgstr "Kalıcı depolama"
#. /wireplumber.settings.schema/device.restore-profile/description
#: wireplumber.conf
msgid "Remember and restore device profiles"
msgstr "Aygıt profillerini anımsa ve geri yükle"
#. /wireplumber.settings.schema/device.restore-profile/name
#: wireplumber.conf
msgid "Restore profile"
msgstr "Profili geri yükle"
#. /wireplumber.settings.schema/device.restore-routes/description
#: wireplumber.conf
msgid "Remember and restore device routes"
msgstr "Aygıt rotalarını anımsa ve geri yükle"
#. /wireplumber.settings.schema/device.restore-routes/name
#: wireplumber.conf
msgid "Restore routes"
msgstr "Rotaları geri yükle"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/description
#: wireplumber.conf
msgid "The default volume for audio sinks"
msgstr "Ses alıcıları için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/device.routes.default-sink-volume/name
#: wireplumber.conf
msgid "Default sink volume"
msgstr "Öntanımlı alıcı ses düzeyi"
#. /wireplumber.settings.schema/device.routes.default-source-volume/description
#: wireplumber.conf
msgid "The default volume for audio sources"
msgstr "Ses kaynakları için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/device.routes.default-source-volume/name
#: wireplumber.conf
msgid "Default source volume"
msgstr "Öntanımlı kaynak ses düzeyi"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active wired headphones/speakers "
"are disconnected to prevent unintended sound output"
msgstr ""
"İstenmeyen ses çıktısını önlemek için etkin kablolu kulaklık/hoparlör "
"bağlantısı kesildiğinde tüm ses aygıtlarını kendiliğinden sustur"
#. /wireplumber.settings.schema/device.routes.mute-on-alsa-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on wired audio disconnect"
msgstr "Kablolu ses bağlantısı kesildiğinde kendiliğinden sustur"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/description
#: wireplumber.conf
msgid ""
"Automatically mute all audio devices when active Bluetooth headphones/"
"speakers are disconnected to prevent unintended sound output"
msgstr ""
"İstenmeyen ses çıktısını önlemek için etkin Bluetooth kulaklık/hoparlör "
"bağlantısı kesildiğinde tüm ses aygıtlarını kendiliğinden sustur"
#. /wireplumber.settings.schema/device.routes.mute-on-bluetooth-playback-removed/name
#: wireplumber.conf
msgid "Auto-mute on Bluetooth audio disconnect"
msgstr "Bluetooth ses bağlantısı kesildiğinde kendiliğinden sustur"
#. /wireplumber.settings.schema/linking.allow-moving-streams/description
#: wireplumber.conf
msgid "Streams may be moved by adding PipeWire metadata at runtime"
msgstr "Akışlar, çalışma zamanında PipeWire üst verileri eklenerek taşınabilir"
#. /wireplumber.settings.schema/linking.allow-moving-streams/name
#: wireplumber.conf
msgid "Allow moving streams"
msgstr "Akışları taşımaya izin ver"
#. /wireplumber.settings.schema/linking.follow-default-target/description
#: wireplumber.conf
msgid "Streams connected to the default device follow when default changes"
msgstr "Öntanımlı aygıta bağlı akışlar öntanımlı değiştiğinde izler"
#. /wireplumber.settings.schema/linking.follow-default-target/name
#: wireplumber.conf
msgid "Follow default target"
msgstr "Öntanımlı hedefi izle"
#. /wireplumber.settings.schema/linking.pause-playback/description
#: wireplumber.conf
msgid "Pause media players if their target sink is removed"
msgstr "Hedef alıcıları kaldırılırsa ortam oynatıcıları duraklat"
#. /wireplumber.settings.schema/linking.pause-playback/name
#: wireplumber.conf
msgid "Pause playback if output removed"
msgstr "Çıktı kaldırılırsa oynatmayı duraklat"
#. /wireplumber.settings.schema/linking.role-based.duck-level/description
#: wireplumber.conf
msgid ""
"The volume level to apply when ducking (= reducing volume for a higher "
"priority stream to be audible) in the role-based linking policy"
msgstr ""
"Rol tabanlı bağlantı ilkesinde eğilirken (= daha öncelikli akışın "
"duyulabilmesi için ses düzeyinin azaltılması) uygulanacak ses düzeyi"
#. /wireplumber.settings.schema/linking.role-based.duck-level/name
#: wireplumber.conf
msgid "Ducking level"
msgstr "Eğilme düzeyi"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr ""
"HDMI aygıtları için kanal sayısını ve konumlarını kendiliğinden algıla "
"(deneysel)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "HDMI kanallarını kendiliğinden algıla (deneysel)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
msgstr "Kamera keşif zaman aşımı, saniye türünden"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/name
#: wireplumber.conf
msgid "Discovery timeout"
msgstr "Keşif zaman aşımı"
#. /wireplumber.settings.schema/node.features.audio.control-port/description
#: wireplumber.conf
msgid "Enable control ports on audio nodes"
msgstr "Ses düğümlerinde denetim bağlantı noktalarını etkinleştir"
#. /wireplumber.settings.schema/node.features.audio.control-port/name
#: wireplumber.conf
msgid "Control ports"
msgstr "Denetim bağlantı noktaları"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/description
#: wireplumber.conf
msgid "Enable monitor ports on audio nodes"
msgstr "Ses düğümlerinde izleme bağlantı noktalarını etkinleştir"
#. /wireplumber.settings.schema/node.features.audio.monitor-ports/name
#: wireplumber.conf
msgid "Monitor ports"
msgstr "İzleme bağlantı noktaları"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio nodes in MONO"
msgstr "Tüm ses düğümlerini MONO olarak yapılandır"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf
msgid "Mono"
msgstr "Mono"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/description
#: wireplumber.conf
msgid "Do not convert audio to F32 format"
msgstr "Sesi F32 biçimine dönüştürme"
#. /wireplumber.settings.schema/node.features.audio.no-dsp/name
#: wireplumber.conf
msgid "No DSP"
msgstr "DSP yok"
#. /wireplumber.settings.schema/node.filter.forward-format/description
#: wireplumber.conf
msgid "Forward format on filter nodes or not"
msgstr "Süzgeç düğümlerinde biçimi ilet ya da iletme"
#. /wireplumber.settings.schema/node.filter.forward-format/name
#: wireplumber.conf
msgid "Forward format"
msgstr "Biçimi ilet"
#. /wireplumber.settings.schema/node.restore-default-targets/description
#: wireplumber.conf
msgid "Remember and restore default audio/video input/output devices"
msgstr "Öntanımlı ses/video girdi/çıktı aygıtlarını anımsa ve geri yükle"
#. /wireplumber.settings.schema/node.restore-default-targets/name
#: wireplumber.conf
msgid "Restore default target"
msgstr "Öntanımlı hedefi geri yükle"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/description
#: wireplumber.conf
msgid "The default volume for capture nodes"
msgstr "Yakalama düğümleri için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/node.stream.default-capture-volume/name
#: wireplumber.conf
msgid "Default capture volume"
msgstr "Öntanımlı yakalama ses düzeyi"
#. /wireplumber.settings.schema/node.stream.default-media-role/description
#: wireplumber.conf
msgid "Default media.role to assign on streams that do not specify it"
msgstr "Belirtilmeyen akışlarda atanacak öntanımlı ortam rolü"
#. /wireplumber.settings.schema/node.stream.default-media-role/name
#: wireplumber.conf
msgid "Default media role"
msgstr "Öntanımlı ortam rolü"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/description
#: wireplumber.conf
msgid "The default volume for playback nodes"
msgstr "Oynatma düğümleri için öntanımlı ses düzeyi"
#. /wireplumber.settings.schema/node.stream.default-playback-volume/name
#: wireplumber.conf
msgid "Default playback volume"
msgstr "Öntanımlı oynatma ses düzeyi"
#. /wireplumber.settings.schema/node.stream.restore-props/description
#: wireplumber.conf
msgid "Remember and restore properties of streams"
msgstr "Akışların özelliklerini anımsa ve geri yükle"
#. /wireplumber.settings.schema/node.stream.restore-props/name
#: wireplumber.conf
msgid "Restore properties"
msgstr "Özellikleri geri yükle"
#. /wireplumber.settings.schema/node.stream.restore-target/description
#: wireplumber.conf
msgid "Remember and restore stream targets"
msgstr "Akış hedeflerini anımsa ve geri yükle"
#. /wireplumber.settings.schema/node.stream.restore-target/name
#: wireplumber.conf
msgid "Restore target"
msgstr "Hedefi geri yükle"

View file

@ -13,8 +13,8 @@ msgstr ""
"Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2025-10-01 16:13+0000\n"
"PO-Revision-Date: 2025-10-02 07:57+0800\n"
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
"PO-Revision-Date: 2025-12-16 10:10+0800\n"
"Last-Translator: lumingzh <lumingzh@qq.com>\n"
"Language-Team: Chinese (China) <i18n-zh@googlegroups.com>\n"
"Language: zh_CN\n"
@ -53,7 +53,7 @@ msgstr "分离 %s"
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
@ -79,6 +79,7 @@ msgstr "调制解调器"
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use HDMI channel detection if enabled in settings
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
@ -273,6 +274,18 @@ msgstr ""
msgid "Ducking level"
msgstr "回避级别"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/description
#: wireplumber.conf
msgid ""
"Automatically detect channel count and positions for HDMI devices "
"(experimental)"
msgstr "自动检测 HDMI 设备的声道数量和位置(实验性)"
#. /wireplumber.settings.schema/monitor.alsa.autodetect-hdmi-channels/name
#: wireplumber.conf
msgid "Automatically detect HDMI channels (experimental)"
msgstr "自动检测 HDMI 声道(实验性)"
#. /wireplumber.settings.schema/monitor.camera-discovery-timeout/description
#: wireplumber.conf
msgid "The camera discovery timeout in milliseconds"
@ -305,8 +318,8 @@ msgstr "监视器端口"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio nodes in MONO"
msgstr "在单声道中配置所有音频节点"
msgid "Configure all audio device sink nodes in MONO"
msgstr "在单声道中配置所有音频设备信宿节点"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf

View file

@ -627,12 +627,17 @@ wireplumber.components = [
name = node/filter-forward-format.lua, type = script/lua
provides = hooks.filter.forward-format
}
{
name = node/filter-graph.lua, type = script/lua
provides = hooks.filter.graph
}
{
type = virtual, provides = policy.node
requires = [ hooks.node.create-session-item ]
wants = [ hooks.node.suspend
hooks.stream.state
hooks.filter.forward-format ]
hooks.filter.forward-format
hooks.filter.graph ]
}
{
name = node/software-dsp.lua, type = script/lua
@ -717,10 +722,21 @@ wireplumber.components = [
provides = hooks.linking.role-based.rescan
requires = [ api.mixer ]
}
{
name = node/find-media-role-default-volume.lua, type = script/lua
provides = hooks.node.role-based.default-volume
requires = [ hooks.linking.role-based.rescan ]
}
{
name = linking/find-media-role-sink-target.lua, type = script/lua
provides = hooks.linking.target.find-media-role-sink
}
{
type = virtual, provides = policy.linking.role-based
requires = [ policy.linking.standard,
hooks.linking.role-based.rescan ]
hooks.linking.role-based.rescan,
hooks.node.role-based.default-volume,
hooks.linking.target.find-media-role-sink ]
}
## Standard policy definition
@ -912,6 +928,12 @@ wireplumber.settings.schema = {
min = 0
max = 60000
}
monitor.alsa.autodetect-hdmi-channels = {
name = "Automatically detect HDMI channels (experimental)"
description = "Automatically detect channel count and positions for HDMI devices (experimental)"
type = "bool"
default = false
}
## Node
node.features.audio.no-dsp = {
@ -934,7 +956,7 @@ wireplumber.settings.schema = {
}
node.features.audio.mono = {
name = "Mono"
description = "Configure all audio nodes in MONO"
description = "Configure all audio device sink nodes in MONO"
type = "bool"
default = false
}

View file

@ -0,0 +1,43 @@
node.filter-graph.rules = [
## The list of filter graph rules
## This rule example creates two filter graphs for each audio source node
# {
# matches = [
# {
# ## This matches all audio source nodes
# media.class = "Audio/Source"
# }
# ]
# actions = {
# create-filter-graph = [
# ## Multiple filter graphs can be defined here.
# ## The syntax is the same as the pipewire filter-chain conf files.
#
# ## This is an example of a bultin passthrough filter
# {
# nodes = [
# {
# type = builtin
# label = copy
# name = passthrough
# }
# ]
# }
#
# ## This is an example of a LADSPA rnnoise filter
# {
# nodes = [
# {
# type = ladspa
# name = rnnoise
# plugin = librnnoise_ladspa
# label = noise_suppressor_stereo
# }
# ]
# }
# ]
# }
# }
]

View file

@ -150,6 +150,7 @@ wireplumber.components = [
policy.role-based.priority = 100
policy.role-based.action.same-priority = "mix"
policy.role-based.action.lower-priority = "cork"
policy.role-based.preferred-target = "Speaker"
}
}
provides = loopback.sink.role.alert

View file

@ -19,5 +19,25 @@ monitor.alsa.rules = [
api.alsa.headroom = 2048
}
}
},
# VMware & VirtualBox on Windows hosts require more headroom to
# avoid stuttering.
{
matches = [
{
node.name = "~alsa_input.pci.*"
cpu.vm.name = "~^(vmware)|(oracle)$"
}
{
node.name = "~alsa_output.pci.*"
cpu.vm.name = "~^(vmware)|(oracle)$"
}
]
actions = {
update-props = {
api.alsa.period-size = 1024
api.alsa.headroom = 8192
}
}
}
]

View file

@ -24,6 +24,9 @@ SimpleEventHook {
local om = source:call ("get-object-manager", "metadata")
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
if metadata == nil then
return
end
if selected_node then
local key = "default." .. def_node_type

View file

@ -7,6 +7,7 @@
cutils = require ("common-utils")
log = Log.open_topic ("s-automute-alsa-routes")
hooks_registered = false
function setRoute (device, route, mute)
local param = Pod.Object {
@ -194,17 +195,19 @@ evaluate_mute_on_node_removed_hook = SimpleEventHook {
function toggleState ()
local mute_alsa = Settings.get_boolean ("device.routes.mute-on-alsa-playback-removed")
local mute_bluez = Settings.get_boolean ("device.routes.mute-on-bluetooth-playback-removed")
if mute_alsa or mute_bluez then
if (mute_alsa or mute_bluez) and not hooks_registered then
nodes_info = {}
mute_alsa_devices_hook:register ()
update_nodes_info_hook:register ()
evaluate_mute_on_device_route_changed_hook:register ()
evaluate_mute_on_node_removed_hook:register ()
else
hooks_registered = true
elseif not mute_alsa and not mute_bluez and hooks_registered then
mute_alsa_devices_hook:remove ()
update_nodes_info_hook:remove ()
evaluate_mute_on_device_route_changed_hook:remove ()
evaluate_mute_on_node_removed_hook:remove ()
hooks_registered = false
end
end

View file

@ -49,7 +49,7 @@ function handlePersistentSetting (enable)
-- the state storage
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
and State ("bluetooth-autoswitch") or nil
headset_profiles = state and state:load () or {}
headset_profiles = state and state:load () or Properties()
else
state = nil
headset_profiles = nil

View file

@ -27,7 +27,7 @@ SimpleEventHook {
local device = event:get_subject ()
local event_properties = event:get_properties ()
local active_ids = event_properties ["profile.active-device-ids"]
local selected_routes = event:get_data ("selected-routes") or {}
local selected_routes = event:get_data ("selected-routes") or Properties()
local dev_info = devinfo:get_device_info (device)
assert (dev_info)

View file

@ -34,7 +34,10 @@ find_stored_profile_hook = SimpleEventHook {
end
local device = event:get_subject ()
local dev_name = device.properties["device.name"]
local device_props = device.properties
local dev_name = device_props["device.name"]
local dont_restore_off_profile = cutils.parseBool (
device_props["session.dont-restore-off-profile"])
if not dev_name then
log:critical (device, "invalid device.name")
return
@ -45,7 +48,8 @@ find_stored_profile_hook = SimpleEventHook {
if profile_name then
for p in device:iterate_params ("EnumProfile") do
local profile = cutils.parseParam (p, "EnumProfile")
if profile.name == profile_name and profile.available ~= "no" then
if profile.name == profile_name and profile.available ~= "no" and
(not dont_restore_off_profile or profile.index ~= 0) then
selected_profile = profile
break
end

View file

@ -36,7 +36,7 @@ find_stored_routes_hook = SimpleEventHook {
local event_properties = event:get_properties ()
local profile_name = event_properties ["profile.name"]
local active_ids = event_properties ["profile.active-device-ids"]
local selected_routes = event:get_data ("selected-routes") or {}
local selected_routes = event:get_data ("selected-routes") or Properties()
local dev_info = devinfo:get_device_info (device)
assert (dev_info)
@ -108,13 +108,13 @@ apply_route_props_hook = SimpleEventHook {
},
execute = function (event)
local device = event:get_subject ()
local selected_routes = event:get_data ("selected-routes") or {}
local selected_routes = event:get_data ("selected-routes") or Properties()
local new_selected_routes = {}
local dev_info = devinfo:get_device_info (device)
assert (dev_info)
if next (selected_routes) == nil then
if selected_routes:get_count () == 0 then
log:info (device, "No routes selected to set on " .. dev_info.name)
return
end
@ -159,28 +159,23 @@ store_or_restore_routes_hook = AsyncEventHook {
},
steps = {
start = {
next = "evaluate",
next = "none",
execute = function (event, transition)
local source = event:get_source ()
local device = event:get_subject ()
-- Make sure the routes are always updated before evaluating them.
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762
local device = event:get_subject ()
device:enum_params ("EnumRoute", function (_, e)
device:enum_params ("EnumRoute", function (enum_route_it, e)
local selected_routes = {}
local push_select_routes = false
-- check for error
if e then
transition:return_error ("failed to enum routes: "
.. tostring (e));
else
transition:advance ()
return
end
end)
end
},
evaluate = {
next = "none",
execute = function (event, transition)
local device = event:get_subject ()
local source = event:get_source ()
local selected_routes = {}
local push_select_routes = false
-- Make sure the device is still valid
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
@ -197,7 +192,7 @@ store_or_restore_routes_hook = AsyncEventHook {
local new_route_infos = {}
-- look at all the routes and update/reset cached information
for p in device:iterate_params ("EnumRoute") do
for p in enum_route_it:iterate() do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
@ -282,6 +277,7 @@ store_or_restore_routes_hook = AsyncEventHook {
end
transition:advance ()
end)
end
}
}

View file

@ -9,7 +9,7 @@ Hooks
The hooks in this section are organized in 3 sub-categories. The first category
includes hooks that are triggered by changes in the graph. Some of them are tasked
to schedule a "rescan-for-linking" event, which is the lowest priority event and
to schedule a "rescan-for-linking" event, which is the lowest priority linking event and
its purpose is to scan through all the linkable session items and link them
to a particular target. The "rescan-for-linking" event is always scheduled to run
once for all the graph changes in a cycle. This is achieved by flagging the event

View file

@ -13,7 +13,8 @@ SimpleEventHook {
name = "linking/find-default-target",
after = { "linking/find-defined-target",
"linking/find-filter-target",
"linking/find-media-role-target" },
"linking/find-media-role-target",
"linking/find-media-role-sink-target" },
before = "linking/prepare-link",
interests = {
EventInterest {

View file

@ -0,0 +1,81 @@
-- WirePlumber
--
-- Copyright © 2025 Phosh.mobi e.V.
--
-- SPDX-License-Identifier: MIT
--
-- Pick up a preferred target node for the output stream of role-based loopbacks
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/find-media-role-sink-target",
after = { "linking/find-defined-target",
"linking/find-media-role-target" },
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local _, om, si, si_props, _, target =
lutils:unwrap_select_target_event (event)
local node_name = si_props["node.name"]
local target_direction = cutils.getTargetDirection (si_props)
local media_class = si_props["media.class"]
local link_group = si_props["node.link-group"]
local is_virtual = si_props["node.virtual"]
log:info (si, string.format ("Lookup for '%s' (%s) / '%s' / '%s'",
node_name, tostring (si_props ["node.id"]), media_class, link_group))
--- bypass the hook if the target is already set or there's no link group
if target or media_class ~= "Stream/Output/Audio" or not is_virtual or link_group == nil then
return
end
--- We link the output node but the relevant properties are on the input node
--- of the link group
local input_node = om:lookup {
type = "SiLinkable",
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.link-group", "=", link_group },
}
if input_node == nil then
log:warning (si, string.format("No input node for %s found", link_group))
return
end
local target_name = input_node.properties["policy.role-based.preferred-target"]
--- no preferred target
if target_name == nil then
return
end
local si_target = om:lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "node.name", "=", target_name },
}
if si_target == nil then
si_target = om:lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "node.nick", "=", target_name },
}
end
if si_target then
log:info (si,
string.format ("... role based sink target picked: %s (%s)",
tostring (si_target.properties ["node.name"]),
tostring (si_target.properties ["node.id"])))
event:set_data ("target", si_target)
end
end
}:register ()

View file

@ -26,7 +26,7 @@ function checkFilter (si, om, handle_nonstreams)
-- always return true if this is not a filter
local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"]
local link_group = node:get_property ("node.link-group")
if link_group == nil then
return true
end
@ -43,36 +43,34 @@ function checkFilter (si, om, handle_nonstreams)
end
function checkLinkable (si, om, handle_nonstreams)
local si_props = si.properties
-- For the rest of them, only handle stream session items
if not si_props or (si_props ["item.node.type"] ~= "stream"
and not handle_nonstreams) then
return false, si_props
if si:get_property ("item.node.type") ~= "stream" and
not handle_nonstreams then
return false
end
-- check filters
if not checkFilter (si, om, handle_nonstreams) then
return false, si_props
return false
end
return true, si_props
return true
end
function unhandleLinkable (si, om)
local si_id = si.id
local valid, si_props = checkLinkable (si, om, true)
if not valid then
if not checkLinkable (si, om, true) then
return
end
local si_id = si.id
log:info (si, string.format ("unhandling item %d", si_id))
-- iterate over all the links in the graph and
-- remove any links associated with this item
for silink in om:iterate { type = "SiLink" } do
local out_id = tonumber (silink.properties ["out.item.id"])
local in_id = tonumber (silink.properties ["in.item.id"])
local silink_props = silink.properties
local out_id = silink_props:get_int ("out.item.id")
local in_id = silink_props:get_int ("in.item.id")
if out_id == si_id or in_id == si_id then
local in_flags = lutils:get_flags (in_id)
@ -84,7 +82,7 @@ function unhandleLinkable (si, om)
out_flags.peer_id = nil
end
if cutils.parseBool (silink.properties["is.role.policy.link"]) then
if silink_props:get_boolean ("is.role.policy.link") then
lutils.clearPriorityMediaRoleLink(silink)
end
@ -113,17 +111,67 @@ SimpleEventHook {
end
}:register ()
-- Handle newly added linkable immediately without waiting for full rescan
-- Only for simple cases where we know it won't affect other parts of the graph
SimpleEventHook {
name = "linking/linkable-added-immediate",
before = "linking/rescan-trigger",
interests = {
EventInterest {
Constraint { "event.type", "=", "session-item-added" },
Constraint { "event.session-item.interface", "=", "linkable" },
},
},
execute = function (event)
local si = event:get_subject ()
local source = event:get_source ()
local om = source:call ("get-object-manager", "session-item")
if not checkLinkable (si, om, false) then
return
end
-- Don't handle immediately if this is a smart filter that could affect other nodes
local node = si:get_associated_proxy ("node")
local link_group = node:get_property ("node.link-group")
if link_group then
local direction = cutils.getTargetDirection (si.properties)
if futils.is_filter_smart (direction, link_group) then
-- Smart filters need full rescan to handle cascading effects
return
end
end
-- Only handle if autoconnect is enabled
local autoconnect = si:get_property ("node.autoconnect")
if autoconnect ~= "true" then
return
end
-- Check if this is a simple stream (most common case)
-- Don't handle device nodes or special nodes that might become default targets
if si:get_property ("item.node.type") ~= "stream" then
return
end
-- Push select-target event immediately for simple stream case
source:call ("push-event", "select-target", si, nil)
end
}:register ()
function handleLinkables (source)
local om = source:call ("get-object-manager", "session-item")
for si in om:iterate { type = "SiLinkable" } do
local valid, si_props = checkLinkable (si, om)
if not valid then
if not checkLinkable (si, om) then
goto skip_linkable
end
-- Get properties
local si_props = si.properties
-- check if we need to link this node at all
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
local autoconnect = si_props:get_boolean ("node.autoconnect")
if not autoconnect then
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
goto skip_linkable
@ -155,7 +203,7 @@ SimpleEventHook {
Constraint { "node.link-group", "+" },
} do
local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"]
local link_group = node:get_property ("node.link-group")
local direction = cutils.getTargetDirection (si.properties)
if futils.is_filter_smart (direction, link_group) and
futils.is_filter_disabled (direction, link_group) then
@ -235,7 +283,6 @@ SimpleEventHook {
},
execute = function (event)
local si = event:get_subject ()
local si_props = si.properties
local source = event:get_source ()
-- clear timeout source, if any

View file

@ -297,9 +297,9 @@ function createNode(parent, id, obj_type, factory, properties)
properties["node.description"] = desc:gsub("(:)", " ")
end
-- add api.alsa.card.* properties for rule matching purposes
-- add api.alsa.card.* and alsa.* properties for rule matching purposes
for k, v in pairs(dev_props) do
if k:find("^api%.alsa%.card%..*") then
if k:find("^api%.alsa%.card%..*") or k:find("^alsa%..*") then
properties[k] = v
end
end
@ -494,6 +494,11 @@ function prepareDevice(parent, id, obj_type, factory, properties)
factory = "api.alsa.acp.device"
end
-- use HDMI channel detection if enabled in settings
if Settings.get_boolean ("monitor.alsa.autodetect-hdmi-channels") then
properties["api.acp.use-eld-channels"] = true
end
-- use device reservation, if available
if rd_plugin and properties["api.alsa.card"] then
local rd_name = "Audio" .. properties["api.alsa.card"]

View file

@ -78,7 +78,7 @@ end
function createMonitor()
local monitor_props = {}
for k, v in pairs(config.properties or {}) do
for k, v in pairs(config.properties or Properties()) do
monitor_props[k] = v
end

View file

@ -157,7 +157,7 @@ SimpleEventHook {
-- Create group loopback module if it does not exist
local m = group_loopback_modules [direction][group]
if m == nil then
Log.warning ("Creating " .. direction .. " loopback for audio group " .. group ..
Log.info ("Creating " .. direction .. " loopback for audio group " .. group ..
(target_object and (" with target object " .. tostring (target_object)) or ""))
m = CreateStreamLoopback (stream_props, group, target_object, direction)
group_loopback_modules [direction][group] = m

View file

@ -16,6 +16,7 @@ items = {}
function configProperties (node)
local properties = node.properties
local media_class = properties ["media.class"] or ""
local factory_name = properties ["factory.name"] or ""
-- ensure a media.type is set
if not properties ["media.type"] then
@ -40,6 +41,7 @@ function configProperties (node)
properties ["item.features.control-port"] =
Settings.get_boolean ("node.features.audio.control-port")
properties ["item.features.mono"] =
(factory_name == "api.alsa.pcm.sink" or factory_name == "api.bluez5.a2dp.sink") and
Settings.get_boolean ("node.features.audio.mono")
properties ["node.id"] = node ["bound-id"]

View file

@ -0,0 +1,53 @@
-- WirePlumber
--
-- Copyright © 2025 The WirePlumber project contributors
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
log = Log.open_topic("s-node")
config = {}
config.rules = Conf.get_section_as_json ("node.filter-graph.rules", Json.Array{})
function setNodeFilterGraphParams (node, graph_params)
local pod = Pod.Object {
"Spa:Pod:Object:Param:Props", "Props",
params = Pod.Struct (graph_params)
}
node:set_params("Props", pod)
end
SimpleEventHook {
name = "node/create-filter-graph",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "library.name", "=", "audioconvert/libspa-audioconvert", type = "pw" },
},
},
execute = function(event)
local node = event:get_subject()
JsonUtils.match_rules (config.rules, node.properties, function (action, value)
if action == "create-filter-graph" then
local graphs = value:parse (1)
local graph_params = {}
for idx, val in ipairs (graphs) do
local index = tonumber(idx) - 1
local key = "audioconvert.filter-graph." .. tostring (index)
log:info (node, "setting node filter graph param '" .. key .. "' to: " .. val)
table.insert(graph_params, key)
table.insert(graph_params, val)
end
setNodeFilterGraphParams (node, graph_params)
end
end)
end
}:register()

View file

@ -0,0 +1,94 @@
-- WirePlumber
--
-- Copyright © 2025 Phosh.mobi e.V.
-- @author Guido Günther <agx@sigxcpu.org>
--
-- SPDX-License-Identifier: MIT
--
-- Select the media role default volume
log = Log.open_topic("s-node")
local cutils = require ("common-utils")
function findHighestPriorityRoleNode (node_om)
local best_role = nil
local best_prio = 0
local default_role = Settings.get ("node.stream.default-media-role")
if default_role then
default_role = default_role:parse()
end
for ni in node_om:iterate {
type = "node",
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.name", "#", "input.loopback.sink.role.*" },
} do
local ni_props = ni.properties
local roles = ni_props["device.intended-roles"]
local node_name = ni_props ["node.name"]
local prio = tonumber(ni_props ["policy.role-based.priority"])
-- Use the node that handles the default_role as fallback
-- when no node is in running state
if best_role == nil and roles and default_role then
local roles_table = Json.Raw(roles):parse()
for i, v in ipairs (roles_table) do
if default_role == v then
best_role = node_name
best_prio = prio
break
end
end
end
if ni.state == "running" then
if prio > best_prio then
best_role = node_name
best_prio = prio
end
end
end
log:info (string.format ("Volume control is on : '%s', prio %d", best_role, best_prio))
local metadata = cutils.get_default_metadata_object ()
metadata:set (0, "current.role-based.volume.control", "Spa:String:JSON",
Json.Object { ["name"] = best_role }:to_string ())
end
SimpleEventHook {
name = "node/rescan-for-media-role-volume",
interests = {
EventInterest {
Constraint { "event.type", "=", "rescan-for-media-role-volume" }
},
},
execute = function (event)
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
findHighestPriorityRoleNode (node_om)
end
}:register ()
-- Track best volume control for media role based priorities
SimpleEventHook {
name = "node/find-media-role-default-volume",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
},
EventInterest {
Constraint { "event.type", "=", "node-state-changed" },
Constraint { "media.class", "=", "Audio/Sink" },
Constraint { "node.name", "#", "input.loopback.sink.role.*" }
},
},
execute = function (event)
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
source:call ("schedule-rescan", "media-role-volume")
end
}:register ()

View file

@ -4,6 +4,11 @@ executable('wpctl',
dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep, libintl_dep],
)
install_data('shell-completion/wpctl.bash',
install_dir: get_option('datadir') / 'bash-completion/completions',
rename: 'wpctl'
)
install_data('shell-completion/wpctl.zsh',
install_dir: get_option('datadir') / 'zsh/site-functions',
rename: '_wpctl'

View file

@ -0,0 +1,30 @@
_wpctl_pw_defaults() {
local defaults="@DEFAULT_SINK@ @DEFAULT_AUDIO_SINK@ @DEFAULT_SOURCE@
@DEFAULT_AUDIO_SOURCE@ @DEFAULT_VIDEO_SOURCE@"
COMPREPLY+=($(compgen -W "$defaults" -- "$cur"))
}
_wpctl() {
local cur prev words cword
local commands="status get-volume inspect set-default set-volume set-mute
set-profile set-route clear-default settings set-log-level"
_init_completion -n = || return
if [[ ${#COMP_WORDS[@]} -eq 2 ]]; then
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
return
fi
case $prev in
get-volume | inspect | set-volume | set-mute | set-profile | set-route)
_wpctl_pw_defaults
;;
clear-default)
COMPREPLY+=($(compgen -W "0 1 2" -- "$cur"))
;;
esac
}
complete -F _wpctl wpctl

View file

@ -174,7 +174,6 @@ test_node (TestFixture *f, gconstpointer data)
props = wp_pipewire_object_get_properties (proxy);
g_assert_nonnull (props);
g_assert_true (wp_properties_peek_dict (props) == info->props);
id = wp_properties_get (props, PW_KEY_OBJECT_ID);
g_assert_nonnull (id);
g_assert_cmpint (info->id, ==, atoi(id));

View file

@ -64,3 +64,9 @@ test(
args: ['lua-api-tests', 'event-hooks.lua'],
env: common_env,
)
test(
'test-lua-properties',
script_tester,
args: ['lua-api-tests', 'properties.lua'],
env: common_env,
)

View file

@ -177,6 +177,23 @@ assert (#val == 0)
assert (val.key1 == nil)
assert (json:get_data() == "{}")
json = Json.Object (
Properties {
["key0"] = nil,
["key1"] = false,
["key2"] = 64,
["key3"] = 2.71,
["key4"] = "string",
}
)
assert (json:is_object())
val = json:parse ()
assert (val.key0 == nil)
assert (val.key1 == "false")
assert (val.key2 == "64")
assert (tonumber (val.key3) > 2.70 and tonumber (val.key3) < 2.72)
assert (val.key4 == "string")
-- Raw
json = Json.Raw ("[\"foo\", \"bar\"]")
assert (json:is_array())

View file

@ -0,0 +1,93 @@
-- create empty properties
props = Properties ()
-- set nil
props["key-nil"] = nil
assert (props["key-nil"] == nil)
-- set bool
props["key-bool"] = false
assert (props["key-bool"] == "false")
assert (props:get_boolean ("key-bool") == false)
props["key-bool"] = true
assert (props["key-bool"] == "true")
assert (props:get_boolean ("key-bool") == true)
-- set int
props["key-int"] = 4
assert (props["key-int"] == "4")
assert (props:get_int ("key-int") == 4)
-- set float
props["key-float"] = 3.14
val = props:get_float ("key-float")
assert (val > 3.13 and val < 3.15)
-- set string
props["key-string"] = "value"
assert (props["key-string"] == "value")
assert (props:get_boolean ("key-string") == false)
assert (props:get_int ("key-string") == nil)
assert (props:get_float ("key-string") == nil)
-- copy
copy = props:copy ()
assert (copy["key-nil"] == nil)
assert (copy:get_boolean ("key-bool") == true)
assert (copy:get_int ("key-int") == 4)
val = copy:get_float ("key-float")
assert (val > 3.13 and val < 3.15)
assert (copy["key-string"] == "value")
-- remove int property
props["key-int"] = nil
assert (props["key-int"] == nil)
assert (copy:get_int ("key-int") == 4)
-- create properties from table
props = Properties {
["key0"] = nil,
["key1"] = false,
["key2"] = 64,
["key3"] = 2.71,
["key4"] = "string",
}
assert (props["key0"] == nil)
assert (props:get_boolean ("key1") == false)
assert (props:get_int ("key2") == 64)
val = props:get_float ("key3")
assert (val > 2.70 and val < 2.72)
assert (props["key4"] == "string")
-- count
assert (props:get_count () == 4)
-- parse
parsed = props:parse ()
assert (parsed["key0"] == nil)
assert (parsed["key1"] == "false")
assert (tonumber (parsed["key2"]) == 64)
val = tonumber (parsed["key3"])
assert (val > 2.70 and val < 2.72)
assert (parsed["key4"] == "string")
-- pairs
values = {}
for k, v in pairs (props) do
values [k] = v
end
assert (values["key0"] == nil)
assert (values["key1"] == "false")
assert (tonumber (values["key2"]) == 64)
val = tonumber (values["key3"])
assert (val > 2.70 and val < 2.72)
assert (values["key4"] == "string")
-- Make sure the reference changes are also updated
local properties = Properties ()
properties["key"] = "value"
assert (properties["key"] == "value")
local properties2 = properties
properties2["key"] = "another-value"
assert (properties2["key"] == "another-value")
assert (properties["key"] == "another-value")