Compare commits

..

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

58 changed files with 954 additions and 2680 deletions

View file

@ -154,22 +154,13 @@ 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
# 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=[]
- 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=[]
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
# misc environment only for wireplumber
@ -245,9 +236,6 @@ 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:

View file

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

View file

@ -1,75 +1,5 @@
WirePlumber 0.5.13
~~~~~~~~~~~~~~~~~~
Additions & Enhancements:
- Added internal filter graph support for audio nodes, allowing users to
create audio preprocessing and postprocessing chains without exposing
filters to applications, useful for software DSP (!743)
- Added new Lua Properties API that significantly improves performance by
avoiding constant serialization between WpProperties and Lua tables,
resulting in approximately 40% faster node linking (!757)
- Added WpIterator Lua API for more efficient parameter enumeration (!746)
- Added bash completions for wpctl command (!762)
- Added script to find suitable volume control when using role-based policy,
allowing volume sliders to automatically adjust the volume of the currently
active role (e.g., ringing, call, media) (!711)
- Added experimental HDMI channel detection setting to use HDMI ELD
information for channel configuration (!749)
- Enhanced role-based policy to allow setting preferred target sinks for
media role loopbacks via ``policy.role-based.preferred-target`` (!754)
- Enhanced Bluetooth profile autoswitch logic to be more robust and handle
saved profiles correctly, including support for loopback sink nodes (!739)
- Enhanced ALSA monitor to include ``alsa.*`` device properties on nodes for
rule matching (!761)
- Optimized stream node linking for common cases to reduce latency when new
audio/video streams are added (!760)
- Improved event dispatcher performance by using hash table registration for
event hooks, eliminating performance degradation as more hooks are
registered (!765)
- Increased audio headroom for VMware and VirtualBox virtual machines (!756)
- Added setting to prevent restoring "Off" profiles via
``session.dont-restore-off-profile`` property (!753)
- Added support for 128 audio channels when compiled with a recent version of
PipeWire (pipewire#4995; CI checks in !768)
Fixes:
- Fixed memory leaks and issues in the modem manager module (!770, !764)
- Fixed MPRIS module incorrectly treating GHashTable as GObject (!759)
- Fixed warning messages when process files in ``/proc/<pid>/*`` don't exist,
particularly when processes are removed quickly (#816, !717)
- Fixed MONO audio configuration to only apply to device sink nodes, allowing
multi-channel mixing in the graph (!769)
- Fixed event dispatcher hook registration and removal to avoid spurious
errors (!747)
- Improved logging for standard-link activation failures (!744)
- Simplified event-hook interest matching for better performance (!758)
Past releases
~~~~~~~~~~~~~
WirePlumber 0.5.12
..................
~~~~~~~~~~~~~~~~~~
Additions & Enhancements:
@ -97,6 +27,9 @@ Fixes:
- Improved device hook documentation and configuration (!736)
Past releases
~~~~~~~~~~~~~
WirePlumber 0.5.11
..................

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-based
.. describe:: policy.role-priority-system
Enables the role based priority system policy. This system creates virtual sinks
Enables the role priority system policy. This system creates virtual sinks
that group streams based on their ``media.role`` property, and assigns a
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 -C build run
$ make 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 selected for a
After that, once the media class of a device node has been select 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_copy_dict (info->props);
props = wp_properties_new_wrap_dict (info->props);
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
id, type, info->factory_name);

View file

@ -15,161 +15,6 @@
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
{
@ -204,8 +49,7 @@ struct _WpEventDispatcher
GObject parent;
GWeakRef core;
GHashTable *defined_hooks; /* registered hooks for defined events */
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
GPtrArray *hooks; /* registered hooks */
GSource *source; /* the event loop source */
GList *events; /* the events stack */
struct spa_system *system;
@ -316,9 +160,7 @@ static void
wp_event_dispatcher_init (WpEventDispatcher * self)
{
g_weak_ref_init (&self->core, NULL);
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->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;
@ -342,8 +184,7 @@ wp_event_dispatcher_finalize (GObject * object)
close (self->eventfd);
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
g_clear_pointer (&self->hooks, g_ptr_array_unref);
g_weak_ref_clear (&self->core);
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
@ -443,10 +284,6 @@ 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));
@ -455,74 +292,7 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == NULL);
wp_event_hook_set_dispatcher (hook, self);
/* 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);
g_ptr_array_add (self->hooks, g_object_ref (hook));
}
/*!
@ -536,9 +306,6 @@ 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));
@ -547,29 +314,11 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
g_return_if_fail (already_registered_dispatcher == self);
wp_event_hook_set_dispatcher (hook, NULL);
/* 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));
g_ptr_array_remove_fast (self->hooks, 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
@ -578,56 +327,7 @@ add_unique (GPtrArray *array, WpEventHook * hook)
WpIterator *
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
{
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);
GPtrArray *items =
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
}

View file

@ -41,12 +41,7 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
WpEventHook * hook);
WP_API
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);
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
G_END_DECLS

View file

@ -254,24 +254,6 @@ 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()
*
@ -339,61 +321,34 @@ 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);
if (wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_NONE,
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
match = wp_object_interest_matches_full (interest,
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
gtype, subject, properties, properties);
/* the interest may have a GType that matches the GType of the subject
or it may have WP_TYPE_EVENT as its GType, in which case it will
match any type of subject */
if (match == WP_INTEREST_MATCH_ALL)
return TRUE;
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
match = wp_object_interest_matches_full (interest, 0,
WP_TYPE_EVENT, NULL, NULL, NULL);
if (match & WP_INTEREST_MATCH_GTYPE)
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;
return FALSE;
}
static void
@ -404,8 +359,6 @@ 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,10 +39,8 @@ struct _WpEventHookClass
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
/*< private >*/
WP_PADDING(4)
WP_PADDING(5)
};
WP_API
@ -69,9 +67,6 @@ 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,11 +17,37 @@
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;
GPtrArray *hooks;
struct spa_list hooks;
/* immutable fields */
gint priority;
@ -70,7 +96,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);
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
spa_list_init (&self->hooks);
self->priority = priority;
self->properties = properties ?
@ -129,7 +155,11 @@ wp_event_get_name(WpEvent *self)
static void
wp_event_free (WpEvent * self)
{
g_clear_pointer (&self->hooks, g_ptr_array_unref);
HookData *hook_data;
spa_list_consume (hook_data, &self->hooks, link) {
spa_list_remove (&hook_data->link);
hook_data_free (hook_data);
}
g_datalist_clear (&self->datalist);
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_object (&self->source);
@ -286,6 +316,33 @@ wp_event_get_data (WpEvent * self, const gchar * key)
return g_datalist_get_data (&self->datalist, key);
}
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
@ -298,37 +355,199 @@ wp_event_get_data (WpEvent * self, const gchar * key)
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);
/* Clear all current hooks */
g_ptr_array_set_size (event->hooks, 0);
/* hooks already collected */
if (!spa_list_is_empty (&event->hooks))
return TRUE;
/* 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);
spa_list_init (&collected);
spa_list_init (&result);
spa_list_init (&remaining);
/* Collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
event_type);
/* collect hooks that run for this event */
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
while (wp_iterator_next (all_hooks, &value)) {
WpEventHook *hook = g_value_get_object (&value);
if (wp_event_hook_runs_for_event (hook, event)) {
g_ptr_array_add (event->hooks, g_object_ref (hook));
HookData *hook_data = hook_data_new (hook);
/* record "after" dependencies directly */
const gchar * const * strv =
wp_event_hook_get_runs_after_hooks (hook_data->hook);
while (strv && *strv) {
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
strv++;
}
spa_list_append (&collected, &hook_data->link);
wp_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);
}
return event->hooks->len > 0;
if (!spa_list_is_empty (&collected)) {
HookData *hook_data;
/* convert "before" dependencies into "after" dependencies */
spa_list_for_each (hook_data, &collected, link) {
const gchar * const * strv =
wp_event_hook_get_runs_before_hooks (hook_data->hook);
while (strv && *strv) {
/* record hook_data->hook as a dependency of the *strv hook */
record_dependency (&collected, *strv,
wp_event_hook_get_name (hook_data->hook));
strv++;
}
}
/* sort */
while (!spa_list_is_empty (&collected)) {
gboolean made_progress = FALSE;
/* examine each hook to see if its dependencies are satisfied in the
result list; if yes, then append it to the result too */
spa_list_consume (hook_data, &collected, link) {
guint deps_satisfied = 0;
spa_list_remove (&hook_data->link);
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
* collected by wp_event_collect_hooks()
@ -339,8 +558,15 @@ wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
WpIterator *
wp_event_new_hooks_iterator (WpEvent * event)
{
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);
WpIterator *it = NULL;
struct event_hooks_iterator_data *it_data;
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,51 +881,3 @@ 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,10 +130,6 @@ 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_copy_dict (props);
d->properties = wp_properties_new_wrap_dict (props);
g_object_notify (G_OBJECT (instance), "properties");
}

View file

@ -6,9 +6,7 @@
* SPDX-License-Identifier: MIT
*/
#include <fcntl.h>
#include <stdio.h>
#include <spa/utils/cleanup.h>
#include "log.h"
#include "proc-utils.h"
@ -147,21 +145,6 @@ 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
@ -172,46 +155,51 @@ WpProcInfo *
wp_proc_utils_get_proc_info (pid_t pid)
{
WpProcInfo *ret = wp_proc_info_new (pid);
char path [64];
spa_autoclose int base_fd = -1;
FILE *file;
g_autofree gchar *line = NULL;
size_t size = 0;
snprintf (path, sizeof(path), "/proc/%d", pid);
base_fd = open (path,
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY, 0);
if (base_fd < 0) {
wp_info ("Could not open process info directory %s, skipping", path);
return ret;
}
g_autofree gchar *status = NULL;
g_autoptr (GError) error = NULL;
gsize length = 0;
/* Get parent PID */
file = fdopenat (base_fd, "status",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
while (getline (&line, &size, file) > 1)
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
break;
fclose (file);
{
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);
}
}
/* Get cgroup */
file = fdopenat (base_fd, "cgroup",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
if (getline (&line, &size, file) > 1)
ret->cgroup = g_strstrip (g_strdup (line));
fclose (file);
{
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);
}
}
/* Get args */
file = fdopenat (base_fd, "cmdline",
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
if (file) {
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
ret->args[ret->n_args++] = g_strdup (line);
fclose (file);
{
g_autofree gchar *path = g_strdup_printf ("/proc/%d/cmdline", pid);
FILE *file = fopen (path, "rb");
if (file) {
g_autofree gchar *lineptr = NULL;
size_t size = 0;
while (getdelim (&lineptr, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
ret->args[ret->n_args++] = g_strdup (lineptr);
fclose (file);
} else {
wp_warning ("failed to get cmdline for PID %d: %m", pid);
}
}
return ret;

View file

@ -1,5 +1,5 @@
project('wireplumber', ['c'],
version : '0.5.13',
version : '0.5.12',
license : 'MIT',
meson_version : '>= 0.59.0',
default_options : [
@ -158,25 +158,7 @@ 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);
WpProperties *p = wp_core_get_properties (core);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, p);
g_autoptr (WpProperties) p = wp_core_get_properties (core);
wplua_properties_to_table (L, p);
return 1;
}
@ -155,7 +155,7 @@ static int
core_get_info (lua_State *L)
{
WpCore * core = get_wp_core (L);
WpProperties *p = wp_core_get_remote_properties (core);
g_autoptr (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_pushboxed (L, WP_TYPE_PROPERTIES, p);
wplua_properties_to_table (L, p);
lua_setfield (L, -2, "properties");
return 1;
}
@ -297,13 +297,8 @@ static int
core_update_properties (lua_State *L)
{
WpCore *core = get_wp_core(L);
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);
luaL_checktype (L, 1, LUA_TTABLE);
wp_core_update_properties (core, wplua_table_to_properties (L, 1));
return 0;
}
@ -604,28 +599,6 @@ 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
@ -864,11 +837,7 @@ 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 = 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));
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
matches = wp_object_interest_matches (interest, props);
} else
luaL_argerror (L, 2, "expected GObject or table");
@ -1028,11 +997,10 @@ impl_metadata_new (lua_State *L)
const char *name = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
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);
@ -1049,11 +1017,10 @@ device_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
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);
@ -1070,11 +1037,10 @@ spa_device_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
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);
@ -1139,11 +1105,10 @@ node_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
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);
@ -1249,11 +1214,10 @@ impl_node_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
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);
@ -1286,11 +1250,10 @@ link_new (lua_State *L)
const char *factory = luaL_checkstring (L, 1);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
luaL_checktype (L, 2, LUA_TTABLE);
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)
@ -1366,12 +1329,9 @@ static int
client_update_properties (lua_State *L)
{
WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
WpProperties *properties = NULL;
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
luaL_checktype (L, 2, LUA_TTABLE);
WpProperties *properties = wplua_table_to_properties (L, 2);
wp_client_update_properties (client, properties);
return 0;
@ -1431,12 +1391,46 @@ static int
session_item_configure (lua_State *L)
{
WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
WpProperties *props;
WpProperties *props = wp_properties_new_empty ();
if (lua_istable (L, 2))
props = wplua_table_to_properties (L, 2);
else
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
/* validate arguments */
luaL_checktype (L, 2, LUA_TTABLE);
/* build the configuration properties */
lua_pushnil (L);
while (lua_next (L, 2)) {
const gchar *key = NULL;
g_autofree gchar *var = NULL;
switch (lua_type (L, -1)) {
case LUA_TBOOLEAN:
var = g_strdup_printf ("%u", lua_toboolean (L, -1));
break;
case LUA_TNUMBER:
if (lua_isinteger (L, -1))
var = g_strdup_printf ("%lld", lua_tointeger (L, -1));
else
var = g_strdup_printf ("%f", lua_tonumber (L, -1));
break;
case LUA_TSTRING:
var = g_strdup (lua_tostring (L, -1));
break;
case LUA_TUSERDATA: {
GValue *v = lua_touserdata (L, -1);
gpointer p = g_value_peek_pointer (v);
var = g_strdup_printf ("%p", p);
break;
}
default:
luaL_error (L, "configure does not support lua type ",
lua_typename(L, lua_type(L, -1)));
break;
}
key = luaL_tolstring (L, -2, NULL);
wp_properties_set (props, key, var);
lua_pop (L, 2);
}
lua_pushboolean (L, wp_session_item_configure (si, props));
return 1;
@ -1458,23 +1452,12 @@ 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 }
};
@ -1544,24 +1527,19 @@ on_enum_params_done (WpPipewireObject * pwobj, GAsyncResult * res,
GClosure * closure)
{
g_autoptr (GError) error = NULL;
GValue vals[2] = { G_VALUE_INIT, G_VALUE_INIT };
int n_vals = 1;
GValue val = G_VALUE_INIT;
int n_vals = 0;
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 (&vals[1], G_TYPE_STRING);
g_value_set_string (&vals[1], error->message);
n_vals = 2;
g_value_init (&val, G_TYPE_STRING);
g_value_set_string (&val, error->message);
n_vals = 1;
}
g_clear_pointer (&it, wp_iterator_unref);
g_closure_invoke (closure, NULL, n_vals, vals, NULL);
g_value_unset (&vals[0]);
g_value_unset (&vals[1]);
g_closure_invoke (closure, NULL, n_vals, &val, NULL);
g_value_unset (&val);
g_closure_invalidate (closure);
g_closure_unref (closure);
}
@ -1597,22 +1575,11 @@ 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 }
};
@ -1639,14 +1606,9 @@ static int
state_save (lua_State *L)
{
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
g_autoptr (WpProperties) props = NULL;
luaL_checktype (L, 2, LUA_TTABLE);
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
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 : "");
@ -1657,13 +1619,8 @@ static int
state_save_after_timeout (lua_State *L)
{
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
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));
luaL_checktype (L, 2, LUA_TTABLE);
g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
wp_state_save_after_timeout (state, get_wp_core (L), props);
return 0;
}
@ -1672,8 +1629,8 @@ static int
state_load (lua_State *L)
{
WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
WpProperties *props = wp_state_load (state);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
g_autoptr (WpProperties) props = wp_state_load (state);
wplua_properties_to_table (L, props);
return 1;
}
@ -1698,11 +1655,10 @@ 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_istable (L, 3))
if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
luaL_checktype (L, 3, LUA_TTABLE);
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);
@ -1725,10 +1681,9 @@ 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) {
@ -1766,7 +1721,7 @@ conf_get_section_as_properties (lua_State *L)
const char *section = NULL;
g_autoptr (WpConf) conf = NULL;
g_autoptr (WpSpaJson) s = NULL;
WpProperties *props = NULL;
g_autoptr (WpProperties) props = NULL;
int argi = 1;
/* check if called as method on object */
@ -1781,8 +1736,6 @@ 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 ();
@ -1791,7 +1744,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_pushboxed (L, WP_TYPE_PROPERTIES, props);
wplua_properties_to_table (L, props);
return 1;
}
@ -1948,12 +1901,10 @@ 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));
properties = wplua_table_to_properties (L, 2);
res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
L, &error);
@ -1969,21 +1920,17 @@ json_utils_match_rules (lua_State *L)
static int
json_utils_match_rules_update_properties (lua_State *L)
{
WpProperties *properties = NULL;
g_autoptr (WpProperties) properties = NULL;
WpSpaJson *json;
int count;
json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
if (lua_istable (L, 2))
properties = wplua_table_to_properties (L, 2);
else
properties = wp_properties_ref (wplua_checkboxed (L, 2,
WP_TYPE_PROPERTIES));
luaL_checktype (L, 2, LUA_TTABLE);
properties = wplua_table_to_properties (L, 2);
count = wp_json_utils_match_rules_update_properties (json, properties);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, properties);
wplua_properties_to_table (L, properties);
lua_pushinteger (L, count);
return 2;
}
@ -2065,108 +2012,6 @@ 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
@ -2460,8 +2305,8 @@ static int
event_get_properties (lua_State *L)
{
WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
WpProperties *props = wp_event_get_properties (event);
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
g_autoptr (WpProperties) props = wp_event_get_properties (event);
wplua_properties_to_table (L, props);
return 1;
}
@ -2583,11 +2428,10 @@ event_dispatcher_push_event (lua_State *L)
lua_pop (L, 1);
lua_pushliteral (L, "properties");
if (lua_istable (L, -1))
if (lua_gettable (L, 1) != LUA_TNIL) {
luaL_checktype (L, -1, LUA_TTABLE);
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");
@ -3140,10 +2984,6 @@ 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,7 +217,6 @@ SANDBOX_EXPORT = {
Conf = WpConf,
JsonUtils = JsonUtils,
ProcUtils = ProcUtils,
Properties = WpProperties_new,
SimpleEventHook = WpSimpleEventHook_new,
AsyncEventHook = WpAsyncEventHook_new,
}

View file

@ -300,54 +300,40 @@ 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);
luaL_checktype (L, 1, LUA_TTABLE);
lua_pushnil (L);
while (lua_next (L, -2)) {
/* We only add table values with string keys */
if (lua_type (L, -2) == LUA_TSTRING) {
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
lua_pushnil (L);
while (lua_next (L, -2)) {
/* We only add table values with string keys */
if (lua_type (L, -2) == LUA_TSTRING) {
wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));
switch (lua_type (L, -1)) {
case LUA_TBOOLEAN:
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
break;
case LUA_TNUMBER:
if (lua_isinteger (L, -1))
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
else
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
break;
case LUA_TSTRING:
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
break;
case LUA_TUSERDATA: {
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
wp_spa_json_builder_add_json (builder, json);
break;
}
default:
luaL_error (L, "Json does not support lua type %s",
lua_typename(L, lua_type(L, -1)));
break;
switch (lua_type (L, -1)) {
case LUA_TBOOLEAN:
wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
break;
case LUA_TNUMBER:
if (lua_isinteger (L, -1))
wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
else
wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
break;
case LUA_TSTRING:
wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
break;
case LUA_TUSERDATA: {
WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
wp_spa_json_builder_add_json (builder, json);
break;
}
default:
luaL_error (L, "Json does not support lua type %s",
lua_typename(L, lua_type(L, -1)));
break;
}
}
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);
}
lua_pop (L, 1);
}
wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));

View file

@ -29,9 +29,8 @@ _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_tolstring (L, 2, NULL);
const gchar *key = luaL_checkstring (L, 2);
GType type = G_VALUE_TYPE (obj_v);
GType boxed_type = type;
lua_CFunction func = NULL;
GHashTable *vtables;
@ -54,104 +53,6 @@ _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;
}
@ -168,8 +69,6 @@ _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,6 +14,7 @@ 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) {
@ -23,34 +24,11 @@ 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 */
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;
}
key = luaL_tolstring (L, -2, NULL);
value = luaL_tolstring (L, -2, NULL);
wp_properties_set (p, key, value);
lua_pop (L, 3);
}
/* sort, because the lua table has a random order and it's too messy to read */
@ -335,7 +313,10 @@ wplua_gvalue_to_lua (lua_State *L, const GValue *v)
lua_pushlightuserdata (L, g_value_get_pointer (v));
break;
case G_TYPE_BOXED:
wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
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:
case G_TYPE_INTERFACE: {

View file

@ -103,15 +103,14 @@ static void
bind_call (GObject * obj, GAsyncResult * res, gpointer data)
{
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
g_autoptr (GError) err = NULL;
GError *err = NULL;
GDBusProxy *call;
g_autoptr (GVariant) prop = NULL;
GVariant *prop;
gint init_state;
call = g_dbus_proxy_new_finish (res, &err);
if (call == NULL) {
g_prefix_error (&err, "Failed to get call: ");
wp_warning_object (wpmm, "%s", err->message);
wp_warning_object (wpmm, "Failed to get call");
return;
}
@ -123,6 +122,8 @@ 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);
@ -164,7 +165,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,
@ -174,8 +175,9 @@ 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);
@ -183,6 +185,8 @@ on_voice_signal (GDBusProxy * iface,
g_object_unref (deleted->data);
wpmm->calls = g_list_delete_link (wpmm->calls, deleted);
}
g_free (path);
}
}
@ -192,23 +196,22 @@ list_calls_done (GObject * obj,
gpointer data)
{
WpModemManager *wpmm = WP_MODEM_MANAGER (data);
g_autoptr (GVariant) params = NULL;
g_autoptr (GVariantIter) calls = NULL;
GVariant *params;
GVariantIter *calls;
gchar *path;
g_autoptr (GError) err = NULL;
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,
@ -219,6 +222,9 @@ list_calls_done (GObject * obj,
bind_call,
wpmm);
}
g_variant_iter_free (calls);
g_variant_unref (params);
}
static void
@ -347,7 +353,7 @@ static void
wp_modem_manager_enable (WpPlugin * self, WpTransition * transition)
{
WpModemManager *wpmm = WP_MODEM_MANAGER (self);
g_autoptr (WpCore) core = NULL;
WpCore *core;
GError *err;
g_autoptr (GDBusConnection) conn = NULL;

View file

@ -98,10 +98,8 @@ static void item_free (gpointer data)
{
Item *item = data;
g_clear_pointer (&item->desktop_entry, g_free);
g_clear_pointer (&item->flatpak_app_id, g_free);
g_clear_pointer (&item->flatpak_instance_id, g_free);
g_free (item);
free(item->desktop_entry);
free(item);
}
static Players *players_new (GDBusConnection *conn)
@ -130,7 +128,7 @@ static void players_unref (Players *players)
return;
g_mutex_clear (&players->lock);
g_clear_pointer (&players->items, g_hash_table_unref);
g_clear_object (&players->items);
g_clear_object (&players->conn);
g_clear_object (&players->cancellable);
g_free (players);
@ -238,7 +236,7 @@ static void item_desktop_entry_cb (GObject *source_object, GAsyncResult* res, gp
}
g_variant_get (result, "(v)", &value);
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
if (!g_str_equal(g_variant_get_type_string (value), "s")) {
wp_info ("%p: bad value for DesktopEntry for '%s'", update->players, update->bus_name);
return;
}
@ -428,8 +426,6 @@ wp_mpris_plugin_operation_finalize (GObject *object)
WpMprisPluginOperation *self = WP_MPRIS_PLUGIN_OPERATION (object);
g_clear_object (&self->conn);
G_OBJECT_CLASS (wp_mpris_plugin_operation_parent_class)->finalize (object);
}
static void
@ -533,7 +529,9 @@ wp_mpris_plugin_disable (WpPlugin * plugin)
static gpointer
wp_mpris_plugin_get_players (WpMprisPlugin *self)
{
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("av"));
g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
g_variant_builder_init (&b, G_VARIANT_TYPE ("av"));
if (self->players) {
g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->players->lock);

View file

@ -137,11 +137,10 @@ si_audio_adapter_get_default_clock_rate (WpSiAudioAdapter * self)
static gboolean
is_unpositioned (struct spa_audio_info_raw *info)
{
uint32_t i, n_pos;
uint32_t i;
if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
return TRUE;
n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
for (i = 0; i < n_pos; i++)
for (i = 0; i < info->channels; i++)
if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
return TRUE;
@ -198,7 +197,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_N_ELEMENTS(raw_format.position)))
!spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS))
SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
if (mono) {
@ -350,8 +349,7 @@ 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 ();
guint n_pos = SPA_MIN(info->channels, SPA_N_ELEMENTS(info->position));
for (guint i = 0; i < n_pos; i++)
for (guint i = 0; i < info->channels; i++)
wp_spa_pod_builder_add_id (position_builder, info->position[i]);
/* Add the position property */

View file

@ -195,16 +195,12 @@ 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, &error)) {
if (wp_object_activate_finish (proxy, res, NULL))
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,7 +38,6 @@ typedef enum {
typedef enum {
RESCAN_CONTEXT_LINKING,
RESCAN_CONTEXT_DEFAULT_NODES,
RESCAN_CONTEXT_MEDIA_ROLE_VOLUME,
N_RESCAN_CONTEXTS,
} RescanContext;
@ -49,7 +48,6 @@ 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)) {
@ -163,8 +161,6 @@ 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,16 +123,6 @@ 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"
@ -165,7 +155,7 @@ msgstr ""
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgid "Configure all audio nodes in MONO"
msgstr ""
#. /wireplumber.settings.schema/node.features.audio.mono/name

View file

@ -2,22 +2,24 @@
# Copyright (C) 2024 WirePlumber's COPYRIGHT HOLDER
# This file is distributed under the same license as the WirePlumber package.
#
# Martin <miles@filmsi.net>, 2024, 2025.
# Martin <miles@filmsi.net>, 2024, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
"POT-Creation-Date: 2025-12-15 16:28+0000\n"
"PO-Revision-Date: 2025-12-15 23:31+0100\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"
"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 3.8\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"
#. WirePlumber
#.
@ -47,7 +49,7 @@ msgstr "Razdeli %s"
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* and alsa.* properties for rule matching purposes
#. add api.alsa.card.* 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
@ -73,7 +75,6 @@ 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
@ -200,34 +201,6 @@ 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"
@ -274,19 +247,6 @@ 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"
@ -317,16 +277,6 @@ 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"

398
po/tr.po
View file

@ -1,25 +1,26 @@
# 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
# 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.
#
msgid ""
msgstr ""
"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"
"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"
"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: Poedit 3.8\n"
"X-Generator: Weblate 4.4.2\n"
#. WirePlumber
#.
@ -27,19 +28,13 @@ msgstr ""
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. SPA ids to node names: name = id_name_table[device_id][node_id]
#. create the underlying hidden ALSA node
#. not suitable for loopback
#: src/scripts/monitors/alsa.lua:106
#, lua-format
msgid "Split %s"
msgstr "Bölük %s"
#. Connect ObjectConfig events to the right node
#. 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
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max amount of channels
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
@ -50,360 +45,15 @@ msgstr "Bölük %s"
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. add cpu.vm.name for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. handle split HW node
#. create split PCM node
#. apply properties from config.rules
#. 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:438
msgid "Loopback"
msgstr "Geri Döngü"
#: src/scripts/monitors/alsa.lua:440
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Yerleşik Ses"
msgstr "Dahili Ses"
#: src/scripts/monitors/alsa.lua:442
#: src/scripts/monitors/alsa.lua:224
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-12-15 16:28+0000\n"
"PO-Revision-Date: 2025-12-16 10:10+0800\n"
"POT-Creation-Date: 2025-10-01 16:13+0000\n"
"PO-Revision-Date: 2025-10-02 07:57+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.* and alsa.* properties for rule matching purposes
#. add api.alsa.card.* 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,7 +79,6 @@ 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
@ -274,18 +273,6 @@ 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"
@ -318,8 +305,8 @@ msgstr "监视器端口"
#. /wireplumber.settings.schema/node.features.audio.mono/description
#: wireplumber.conf
msgid "Configure all audio device sink nodes in MONO"
msgstr "在单声道中配置所有音频设备信宿节点"
msgid "Configure all audio nodes in MONO"
msgstr "在单声道中配置所有音频节点"
#. /wireplumber.settings.schema/node.features.audio.mono/name
#: wireplumber.conf

View file

@ -627,17 +627,12 @@ 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.graph ]
hooks.filter.forward-format ]
}
{
name = node/software-dsp.lua, type = script/lua
@ -722,21 +717,10 @@ 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.node.role-based.default-volume,
hooks.linking.target.find-media-role-sink ]
hooks.linking.role-based.rescan ]
}
## Standard policy definition
@ -928,12 +912,6 @@ 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 = {
@ -956,7 +934,7 @@ wireplumber.settings.schema = {
}
node.features.audio.mono = {
name = "Mono"
description = "Configure all audio device sink nodes in MONO"
description = "Configure all audio nodes in MONO"
type = "bool"
default = false
}

View file

@ -1,43 +0,0 @@
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,7 +150,6 @@ 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,25 +19,5 @@ 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,9 +24,6 @@ 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,7 +7,6 @@
cutils = require ("common-utils")
log = Log.open_topic ("s-automute-alsa-routes")
hooks_registered = false
function setRoute (device, route, mute)
local param = Pod.Object {
@ -195,19 +194,17 @@ 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) and not hooks_registered then
if mute_alsa or mute_bluez then
nodes_info = {}
mute_alsa_devices_hook:register ()
update_nodes_info_hook:register ()
evaluate_mute_on_device_route_changed_hook:register ()
evaluate_mute_on_node_removed_hook:register ()
hooks_registered = true
elseif not mute_alsa and not mute_bluez and hooks_registered then
else
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

@ -28,25 +28,43 @@
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-device")
persistent_storage_hooks_registered = false
autoswitch_hooks_registered = false
local PROFILE_RESTORE_TIMEOUT_MSEC = 2000
local PROFILE_SWITCH_TIMEOUT_MSEC = 500
state = nil
headset_profiles = nil
local state = nil
local headset_profiles = {}
local non_headset_profiles = {}
local capture_stream_links = {}
local profile_restore_timeout_msec = 2000
local profile_switch_timeout_msec = 500
local INVALID = -1
local restore_timeout_source = {}
local switch_timeout_source = {}
function saveHeadsetProfile (device, profile_name, persistent)
local last_profiles = {}
local active_streams = {}
local previous_streams = {}
function handlePersistentSetting (enable)
if enable and state == nil then
-- the state storage
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
and State ("bluetooth-autoswitch") or nil
headset_profiles = state and state:load () or {}
else
state = nil
headset_profiles = nil
end
end
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
end)
function saveHeadsetProfile (device, profile_name)
local key = "saved-headset-profile:" .. device.properties ["device.name"]
headset_profiles [key] = profile_name
if state ~= nil and persistent then
state:save_after_timeout (headset_profiles)
end
state:save_after_timeout (headset_profiles)
end
function getSavedHeadsetProfile (device)
@ -54,72 +72,85 @@ function getSavedHeadsetProfile (device)
return headset_profiles [key]
end
function saveNonHeadsetProfile (device, profile_name)
non_headset_profiles [device.properties ["device.name"]] = profile_name
function saveLastProfile (device, profile_name)
last_profiles [device.properties ["device.name"]] = profile_name
end
function getSavedNonHeadsetProfile (device)
return non_headset_profiles [device.properties ["device.name"]]
function getSavedLastProfile (device)
return last_profiles [device.properties ["device.name"]]
end
function isSwitchedToHeadsetProfile (device)
return getSavedLastProfile (device) ~= nil
end
function findProfile (device, index, name)
for p in device:iterate_params ("EnumProfile") do
local profile = cutils.parseParam (p, "EnumProfile")
if profile ~= nil then
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile
end
if not profile then
goto skip_enum_profile
end
log:debug ("Profile name: " .. profile.name .. ", priority: "
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile.priority, profile.index, profile.name
end
::skip_enum_profile::
end
return nil
return INVALID, INVALID, nil
end
function getCurrentProfile (device)
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
if profile then
return profile
return profile.name
end
end
return nil
end
function highestPrioProfileWithInputRoute (device)
local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
if route ~= nil and route.profiles ~= nil and route.direction == "Input" then
for _, v in pairs (route.profiles) do
local p = findProfile (device, v)
if p ~= nil then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
end
end
end
end
end
return found_profile
end
local profile_priority = INVALID
local profile_index = INVALID
local profile_name = nil
function highestPrioProfileWithoutInputRoute (device)
local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
-- Parse pod
if not route then
goto skip_enum_route
end
if route.direction ~= "Input" then
goto skip_enum_route
end
log:debug ("Route with index: " .. tostring (route.index) .. ", direction: "
.. route.direction .. ", name: " .. route.name .. ", description: "
.. route.description .. ", priority: " .. route.priority)
if route.profiles then
for _, v in pairs (route.profiles) do
local p = findProfile (device, v)
if p ~= nil then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
local priority, index, name = findProfile (device, v)
if priority ~= INVALID then
if profile_priority < priority then
profile_priority = priority
profile_index = index
profile_name = name
end
end
end
end
::skip_enum_route::
end
return found_profile
return profile_priority, profile_index, profile_name
end
function hasProfileInputRoute (device, profile_index)
@ -146,40 +177,49 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
return
end
-- Do not switch if the current profile is already a headset profile
local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil and
hasProfileInputRoute (device, cur_profile.index) then
log:info (device,
"Current profile is already a headset profile, no need to switch")
local cur_profile_name = getCurrentProfile (device)
local priority, index, name = findProfile (device, nil, cur_profile_name)
if hasProfileInputRoute (device, index) then
log:info ("Current profile has input route, not switching")
return
end
-- Get saved headset profile if any, otherwise find the highest priority one
local profile = nil
local profile_name = getSavedHeadsetProfile (device)
if profile_name ~= nil then
profile = findProfile (device, nil, profile_name)
if profile ~= nil and not hasProfileInputRoute (device, profile.index) then
saveHeadsetProfile (device, nil, false)
end
end
if profile == nil then
profile = highestPrioProfileWithInputRoute (device)
if isSwitchedToHeadsetProfile (device) then
log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
return
end
-- Switch if headset profile was found
if profile ~= nil then
local saved_headset_profile = getSavedHeadsetProfile (device)
index = INVALID
if saved_headset_profile then
priority, index, name = findProfile (device, nil, saved_headset_profile)
if index ~= INVALID and not hasProfileInputRoute (device, index) then
index = INVALID
saveHeadsetProfile (device, nil)
end
end
if index == INVALID then
priority, index, name = highestPrioProfileWithInputRoute (device)
end
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = profile.index,
save = false
index = index
}
log:info (device, "Switching profile from: " .. cur_profile.name
.. " to: " .. profile.name)
-- store the current profile (needed when restoring)
saveLastProfile (device, cur_profile_name)
-- switch to headset profile
log:info ("Setting profile of '"
.. device.properties ["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params ("Profile", pod)
else
log:warning ("Could not find valid headset profile, not switching")
log:warning ("Got invalid index when switching profile")
end
end
@ -193,40 +233,45 @@ function restoreProfile (dev_id, device_om)
return
end
-- Do not restore if the current profile is already a non-headset profile
local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil and
not hasProfileInputRoute (device, cur_profile.index) then
log:info (device,
"Current profile is already a non-headset profile, no need to restore")
if not isSwitchedToHeadsetProfile (device) then
log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
return
end
-- Get saved non-headset profile if any, otherwise find the highest priority one
local profile = nil
local profile_name = getSavedNonHeadsetProfile (device)
if profile_name ~= nil then
profile = findProfile (device, nil, profile_name)
if profile ~= nil and hasProfileInputRoute (device, profile.index) then
saveNonHeadsetProfile (device, nil)
local profile_name = getSavedLastProfile (device)
local cur_profile_name = getCurrentProfile (device)
local priority, index, name
if cur_profile_name then
priority, index, name = findProfile (device, nil, cur_profile_name)
if index ~= INVALID and hasProfileInputRoute (device, index) then
log:info ("Setting saved headset profile to: " .. cur_profile_name)
saveHeadsetProfile (device, cur_profile_name)
end
end
if profile == nil then
profile = highestPrioProfileWithoutInputRoute (device)
end
-- Restore if non-headset profile was found
if profile ~= nil then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = profile.index,
save = false
}
log:info (device, "Restoring profile from: " .. cur_profile.name
.. " to: " .. profile.name)
device:set_params ("Profile", pod)
else
log:warning ("Could not find valid non-headset profile, not switching")
if profile_name then
priority, index, name = findProfile (device, nil, profile_name)
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
}
-- clear last profile as we will restore it now
saveLastProfile (device, nil)
-- restore previous profile
log:info ("Restoring profile of '"
.. device.properties ["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params ("Profile", pod)
else
log:warning ("Failed to restore profile")
end
end
end
@ -235,89 +280,95 @@ function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy ()
restore_timeout_source[dev_id] = nil
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
end
if switch_timeout_source[dev_id] ~= nil then
switch_timeout_source[dev_id]:destroy ()
switch_timeout_source[dev_id] = nil
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
end
-- create new switch callback
log:info ("Triggering profile switch on device " .. tostring (dev_id))
switch_timeout_source[dev_id] = Core.timeout_add (PROFILE_SWITCH_TIMEOUT_MSEC, function ()
switch_timeout_source[dev_id] = Core.timeout_add (profile_switch_timeout_msec, function ()
switch_timeout_source[dev_id] = nil
switchDeviceToHeadsetProfile (dev_id, device_om)
end)
end
function triggerRestoreProfile (dev_id, device_om)
-- we never restore the device profiles if there are active streams
for _, v in pairs (active_streams) do
if v == dev_id then
return
end
end
-- Always clear any pending restore/switch callbacks when triggering a new restore
if switch_timeout_source[dev_id] ~= nil then
switch_timeout_source[dev_id]:destroy ()
switch_timeout_source[dev_id] = nil
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
end
if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy ()
restore_timeout_source[dev_id] = nil
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
end
-- create new restore callback
log:info ("Triggering profile restore on device " .. tostring (dev_id))
restore_timeout_source[dev_id] = Core.timeout_add (PROFILE_RESTORE_TIMEOUT_MSEC, function ()
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
restore_timeout_source[dev_id] = nil
restoreProfile (dev_id, device_om)
end)
end
function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups)
local stream_id = stream["bound-id"]
-- Make sure the node is linked
local link = link_om:lookup {
Constraint { "link.input.node", "=", stream_id, type = "pw-global"}
}
if link == nil then
return nil
end
local peer_id = link.properties["link.output.node"]
-- If the peer node is the BT loopback source node, return its Id.
-- Otherwise recursively advance in the graph if it is linked to a filter.
local bt_node = node_om:lookup {
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
}
if bt_node ~= nil then
return bt_node
else
local filter_main_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "node.link-group", "+", type = "pw" }
-- We consider a Stream of interest if it is linked to a bluetooth loopback
-- source filter
function checkStreamStatus (stream, node_om, visited_link_groups)
-- check if the stream is linked to a bluetooth loopback source
local stream_id = tonumber(stream["bound-id"])
local peer_id = lutils.getNodePeerId (stream_id)
if peer_id ~= nil then
local bt_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
}
if filter_main_node ~= nil then
local filter_link_group = filter_main_node.properties ["node.link-group"]
if visited_link_groups == nil then
visited_link_groups = {}
if bt_node ~= nil then
local dev_id = bt_node.properties["device.id"]
if dev_id ~= nil then
-- If a stream we previously saw stops running, we consider it
-- inactive, because some applications (Teams) just cork input
-- streams, but don't close them.
if previous_streams [stream.id] == dev_id and
stream.state ~= "running" then
return nil
end
return dev_id
end
if visited_link_groups [filter_link_group] then
return nil
else
visited_link_groups [filter_link_group] = true
end
for filter_stream_node in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
} do
local filter_stream_id = filter_stream_node["bound-id"]
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_id, node_om, link_om, visited_link_groups)
if bt_node ~= nil then
return bt_node
else
-- Check if it is linked to a filter main node, and recursively advance if so
local filter_main_node = node_om:lookup {
Constraint { "bound-id", "=", peer_id, type = "gobject" },
Constraint { "node.link-group", "+", type = "pw" }
}
if filter_main_node ~= nil then
-- Now check all stream nodes for this filter
local filter_link_group = filter_main_node.properties ["node.link-group"]
if visited_link_groups == nil then
visited_link_groups = {}
end
if visited_link_groups [filter_link_group] then
return nil
else
visited_link_groups [filter_link_group] = true
end
for filter_stream_node in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
} do
local dev_id = checkStreamStatus (filter_stream_node, node_om, visited_link_groups)
if dev_id ~= nil then
return dev_id
end
end
end
end
@ -326,65 +377,60 @@ function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om
return nil
end
function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
local bt_node_id = bt_node["bound-id"]
function handleStream (stream, node_om, device_om)
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
return
end
local dev_id = checkStreamStatus (stream, node_om)
if dev_id ~= nil then
active_streams [stream.id] = dev_id
previous_streams [stream.id] = dev_id
triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
else
dev_id = active_streams [stream.id]
active_streams [stream.id] = nil
if dev_id ~= nil then
triggerRestoreProfile (dev_id, device_om)
end
end
end
function handleAllStreams (node_om, device_om)
for stream in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "node.link-group", "-", type = "pw" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
} do
local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om)
if linked_bt_node ~= nil then
local linked_bt_node_id = linked_bt_node ["bound-id"]
if tonumber (linked_bt_node_id) == tonumber (bt_node_id) then
return true
end
end
handleStream (stream, node_om, device_om)
end
return false
end
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
SimpleEventHook {
name = "node-removed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "evaluate-bluetooth-profiles" },
Constraint { "event.type", "=", "node-removed" },
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
},
},
execute = function (event)
local stream = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device")
local link_om = source:call ("get-object-manager", "link")
-- Evaluate all bluetooth loopback source nodes, and switch to headset
-- profile only if the node is running and linked to a stream that is not a
-- monitor, otherwise just restore the profile.
--
-- If the bluetooth node is linked to a stream that is a monitor, its state
-- will be 'running', so we cannot just rely on the state to know if we
-- have to switch or not, we also need to check if the node is linked to
-- a stream that is not a monitor.
for bt_node in node_om:iterate {
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
} do
local bt_node_state = bt_node["state"]
local bt_dev_id = bt_node.properties ["device.id"]
if bt_node_state == "running" and
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
triggerSwitchDeviceToHeadsetProfile (bt_dev_id, device_om)
else
triggerRestoreProfile (bt_dev_id, device_om)
end
local dev_id = active_streams[stream.id]
active_streams[stream.id] = nil
previous_streams[stream.id] = nil
if dev_id ~= nil then
triggerRestoreProfile (dev_id, device_om)
end
end
}
}:register ()
local link_added_hook = SimpleEventHook {
SimpleEventHook {
name = "link-added@autoswitch-bluetooth-profile",
interests = {
EventInterest {
@ -392,149 +438,46 @@ local link_added_hook = SimpleEventHook {
},
},
execute = function (event)
local link = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local link = event:get_subject ()
local in_stream_id = link.properties["link.input.node"]
local device_om = source:call ("get-object-manager", "device")
local link_props = link.properties
-- Only evaluate bluetooth profiles if a capture stream was linked
local stream = node_om:lookup {
for stream in node_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "node.link-group", "-", type = "pw" },
Constraint { "stream.monitor", "!", "true", type = "pw" },
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
Constraint { "bound-id", "=", in_stream_id, type = "gobject" },
}
if stream ~= nil then
capture_stream_links [link.id] = true
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
} do
local in_id = tonumber(link_props["link.input.node"])
local stream_id = tonumber(stream["bound-id"])
if in_id == stream_id then
handleStream (stream, node_om, device_om)
end
end
end
}
}:register ()
local link_removed_hook = SimpleEventHook {
name = "link-removed@autoswitch-bluetooth-profile",
SimpleEventHook {
name = "bluez-device-added@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "link-removed" },
},
},
execute = function (event)
local source = event:get_source ()
local link = event:get_subject ()
-- Only evaluate bluetooth profiles if a capture stream was unlinked
if capture_stream_links [link.id] then
capture_stream_links [link.id] = nil
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
end
}
local state_changed_hook = SimpleEventHook {
name = "bluez-loopback-state-changed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-state-changed" },
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
},
},
execute = function (event)
local source = event:get_source ()
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
local node_added_hook = SimpleEventHook {
name = "bluez-loopback-added@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "media.class", "matches", "Audio/Source" },
Constraint { "device.id", "+" },
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
},
},
execute = function (event)
local source = event:get_source ()
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
local device_profile_changed_hook = SimpleEventHook {
name = "device/store-user-selected-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
Constraint { "event.subject.param-id", "=", "Profile" },
Constraint { "event.type", "=", "device-added" },
Constraint { "device.api", "=", "bluez5" },
},
},
execute = function (event)
local device = event:get_subject ()
local source = event:get_source ()
local node_om = source:call ("get-object-manager", "node")
local device_om = source:call ("get-object-manager", "device")
-- Always save the current profile when it changes
local cur_profile = getCurrentProfile (device)
if cur_profile ~= nil then
if hasProfileInputRoute (device, cur_profile.index) then
log:info (device, "Saving headset profile " .. cur_profile.name)
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
else
log:info (device, "Saving non-headset profile " .. cur_profile.name)
saveNonHeadsetProfile (device, cur_profile.name)
end
end
-- Devices are unswitched initially
saveLastProfile (device, nil)
-- Handle all streams when BT device is added
handleAllStreams (node_om, device_om)
end
}
}:register ()
function evaluatePersistentStorage ()
if Settings.get_boolean ("bluetooth.use-persistent-storage") and
not persistent_storage_hooks_registered then
state = State ("bluetooth-autoswitch")
headset_profiles = state:load ()
persistent_storage_hooks_registered = true
elseif persistent_storage_hooks_registered then
state = nil
headset_profiles = {}
persistent_storage_hooks_registered = false
end
end
function evaluateAutoswitch ()
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") and
not autoswitch_hooks_registered then
capture_stream_links = {}
restore_timeout_source = {}
switch_timeout_source = {}
evaluate_bluetooth_profiles_hook:register ()
link_added_hook:register ()
link_removed_hook:register ()
state_changed_hook:register ()
node_added_hook:register ()
device_profile_changed_hook:register ()
autoswitch_hooks_registered = true
elseif autoswitch_hooks_registered then
capture_stream_links = nil
restore_timeout_source = nil
switch_timeout_source = nil
evaluate_bluetooth_profiles_hook:remove ()
link_added_hook:remove ()
link_removed_hook:remove ()
state_changed_hook:remove ()
node_added_hook:remove ()
device_profile_changed_hook:remove ()
autoswitch_hooks_registered = false
end
end
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
evaluatePersistentStorage ()
end)
evaluatePersistentStorage ()
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
evaluateAutoswitch ()
end)
evaluateAutoswitch ()

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 Properties()
local selected_routes = event:get_data ("selected-routes") or {}
local dev_info = devinfo:get_device_info (device)
assert (dev_info)

View file

@ -34,10 +34,7 @@ find_stored_profile_hook = SimpleEventHook {
end
local device = event:get_subject ()
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"])
local dev_name = device.properties["device.name"]
if not dev_name then
log:critical (device, "invalid device.name")
return
@ -48,8 +45,7 @@ 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" and
(not dont_restore_off_profile or profile.index ~= 0) then
if profile.name == profile_name and profile.available ~= "no" 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 Properties()
local selected_routes = event:get_data ("selected-routes") or {}
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 Properties()
local selected_routes = event:get_data ("selected-routes") or {}
local new_selected_routes = {}
local dev_info = devinfo:get_device_info (device)
assert (dev_info)
if selected_routes:get_count () == 0 then
if next (selected_routes) == nil then
log:info (device, "No routes selected to set on " .. dev_info.name)
return
end
@ -159,126 +159,130 @@ store_or_restore_routes_hook = AsyncEventHook {
},
steps = {
start = {
next = "none",
next = "evaluate",
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
device:enum_params ("EnumRoute", function (enum_route_it, e)
local selected_routes = {}
local push_select_routes = false
-- check for error
local device = event:get_subject ()
device:enum_params ("EnumRoute", function (_, e)
if e then
transition:return_error ("failed to enum routes: "
.. tostring (e));
return
end
-- Make sure the device is still valid
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
else
transition:advance ()
return
end
local dev_info = devinfo:get_device_info (device)
if not dev_info then
transition:advance ()
return
end
local new_route_infos = {}
-- look at all the routes and update/reset cached information
for p in enum_route_it:iterate() do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
-- find cached route information
local route_info = devinfo.find_route_info (dev_info, route, true)
if not route_info then
goto skip_enum_route
end
-- update properties
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos [route.index] = route_info
::skip_enum_route::
end
-- update route_infos with new prev_active, active and save changes
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params ("Route") do
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = devinfo.find_route_info (dev_info, route, false)
if not route_info then
goto skip_route
end
-- update route_info state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- make sure we save this as a preferred route
log:info (device,
string.format ("new active route(%s) found of device(%s)",
route.name, dev_info.name))
route_info.prev_active = true
route_info.active = true
selected_routes [tostring (route.device)] =
Json.Object { index = route_info.index }:to_string ()
push_select_routes = true
elseif route.available ~= "no" and route.save and route.props then
-- just save route properties
log:info (device,
string.format ("storing route(%s) props of device(%s)",
route.name, dev_info.name))
saveRouteProps (dev_info, route)
end
::skip_route::
end
-- save selected routes for the active profile
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
saveProfileRoutes (dev_info, profile.name)
end
-- push a select-routes event to re-apply the routes with new properties
if push_select_routes then
local e = source:call ("create-event", "select-routes", device, nil)
e:set_data ("selected-routes", selected_routes)
EventDispatcher.push_event (e)
end
transition:advance ()
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
transition:advance ()
return
end
local dev_info = devinfo:get_device_info (device)
if not dev_info then
transition:advance ()
return
end
local new_route_infos = {}
-- look at all the routes and update/reset cached information
for p in device:iterate_params ("EnumRoute") do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
-- find cached route information
local route_info = devinfo.find_route_info (dev_info, route, true)
if not route_info then
goto skip_enum_route
end
-- update properties
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos [route.index] = route_info
::skip_enum_route::
end
-- update route_infos with new prev_active, active and save changes
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params ("Route") do
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = devinfo.find_route_info (dev_info, route, false)
if not route_info then
goto skip_route
end
-- update route_info state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- make sure we save this as a preferred route
log:info (device,
string.format ("new active route(%s) found of device(%s)",
route.name, dev_info.name))
route_info.prev_active = true
route_info.active = true
selected_routes [tostring (route.device)] =
Json.Object { index = route_info.index }:to_string ()
push_select_routes = true
elseif route.available ~= "no" and route.save and route.props then
-- just save route properties
log:info (device,
string.format ("storing route(%s) props of device(%s)",
route.name, dev_info.name))
saveRouteProps (dev_info, route)
end
::skip_route::
end
-- save selected routes for the active profile
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
saveProfileRoutes (dev_info, profile.name)
end
-- push a select-routes event to re-apply the routes with new properties
if push_select_routes then
local e = source:call ("create-event", "select-routes", device, nil)
e:set_data ("selected-routes", selected_routes)
EventDispatcher.push_event (e)
end
transition:advance ()
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 linking event and
to schedule a "rescan-for-linking" event, which is the lowest priority 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,8 +13,7 @@ SimpleEventHook {
name = "linking/find-default-target",
after = { "linking/find-defined-target",
"linking/find-filter-target",
"linking/find-media-role-target",
"linking/find-media-role-sink-target" },
"linking/find-media-role-target" },
before = "linking/prepare-link",
interests = {
EventInterest {

View file

@ -1,81 +0,0 @@
-- 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:get_property ("node.link-group")
local link_group = node.properties["node.link-group"]
if link_group == nil then
return true
end
@ -43,34 +43,36 @@ 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 si:get_property ("item.node.type") ~= "stream" and
not handle_nonstreams then
return false
if not si_props or (si_props ["item.node.type"] ~= "stream"
and not handle_nonstreams) then
return false, si_props
end
-- check filters
if not checkFilter (si, om, handle_nonstreams) then
return false
return false, si_props
end
return true
return true, si_props
end
function unhandleLinkable (si, om)
if not checkLinkable (si, om, true) then
local si_id = si.id
local valid, si_props = checkLinkable (si, om, true)
if not valid 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 silink_props = silink.properties
local out_id = silink_props:get_int ("out.item.id")
local in_id = silink_props:get_int ("in.item.id")
local out_id = tonumber (silink.properties ["out.item.id"])
local in_id = tonumber (silink.properties ["in.item.id"])
if out_id == si_id or in_id == si_id then
local in_flags = lutils:get_flags (in_id)
@ -82,7 +84,7 @@ function unhandleLinkable (si, om)
out_flags.peer_id = nil
end
if silink_props:get_boolean ("is.role.policy.link") then
if cutils.parseBool (silink.properties["is.role.policy.link"]) then
lutils.clearPriorityMediaRoleLink(silink)
end
@ -111,67 +113,17 @@ 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
if not checkLinkable (si, om) then
local valid, si_props = checkLinkable (si, om)
if not valid then
goto skip_linkable
end
-- Get properties
local si_props = si.properties
-- check if we need to link this node at all
local autoconnect = si_props:get_boolean ("node.autoconnect")
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
if not autoconnect then
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
goto skip_linkable
@ -203,7 +155,7 @@ SimpleEventHook {
Constraint { "node.link-group", "+" },
} do
local node = si:get_associated_proxy ("node")
local link_group = node:get_property ("node.link-group")
local link_group = node.properties["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
@ -283,6 +235,7 @@ 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.* and alsa.* properties for rule matching purposes
-- add api.alsa.card.* properties for rule matching purposes
for k, v in pairs(dev_props) do
if k:find("^api%.alsa%.card%..*") or k:find("^alsa%..*") then
if k:find("^api%.alsa%.card%..*") then
properties[k] = v
end
end
@ -494,11 +494,6 @@ 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 Properties()) do
for k, v in pairs(config.properties or {}) do
monitor_props[k] = v
end

View file

@ -7,9 +7,7 @@
COMBINE_OFFSET = 64
LOOPBACK_SOURCE_ID = 128
LOOPBACK_SINK_ID = 129
DEVICE_SOURCE_ID = 0
DEVICE_SINK_ID = 1
cutils = require ("common-utils")
log = Log.open_topic ("s-monitors")
@ -25,8 +23,6 @@ config.properties["api.bluez5.connection-info"] = true
-- Properties used for previously creating a SCO source node. key: SPA device id
sco_source_node_properties = {}
-- Properties used for previously creating a SCO or A2DP sink node. key: SPA device id
sco_a2dp_sink_node_properties = {}
devices_om = ObjectManager {
Interest {
@ -275,16 +271,6 @@ function createNode(parent, id, type, factory, properties)
name_prefix = name_prefix .. "_internal"
end
-- hide the sink node because we use the loopback sink instead
if parent:get_managed_object (LOOPBACK_SINK_ID) ~= nil and
(factory == "api.bluez5.sco.sink" or
factory == "api.bluez5.a2dp.sink") then
properties["bluez5.sink-loopback-target"] = true
properties["api.bluez5.internal"] = true
-- add 'internal' to name prefix to not be confused with loopback node
name_prefix = name_prefix .. "_internal"
end
-- set the node name
local name = name_prefix .. "." ..
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
@ -318,12 +304,9 @@ function createNode(parent, id, type, factory, properties)
parent:set_managed_pending(id)
else
log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id))
properties["bluez5.loopback"] = false
if factory == "api.bluez5.sco.source" then
properties["bluez5.loopback"] = false
sco_source_node_properties[parent_spa_id] = properties
elseif factory == "api.bluez5.sco.sink" or factory == "api.bluez5.a2dp.sink" then
properties["bluez5.sink-loopback"] = false
sco_a2dp_sink_node_properties[parent_spa_id] = properties
end
local node = LocalNode("adapter", properties)
node:activate(Feature.Proxy.BOUND)
@ -335,20 +318,14 @@ function removeNode(parent, id)
local dev_props = parent.properties
local parent_spa_id = tonumber(dev_props["spa.object.id"])
local src_properties = sco_source_node_properties[parent_spa_id]
local sink_properties = sco_a2dp_sink_node_properties[parent_spa_id]
log:debug("Remove node: " .. tostring (id))
if src_properties ~= nil and id == tonumber(src_properties["spa.object.id"]) then
log:debug("Clear old SCO source properties")
log:debug("Clear old SCO properties")
sco_source_node_properties[parent_spa_id] = nil
end
if sink_properties ~= nil and id == tonumber(sink_properties["spa.object.id"]) then
log:debug("Clear old SCO-A2DP sink properties")
sco_a2dp_sink_node_properties[parent_spa_id] = nil
end
-- Clear also the device set module, if any
parent:store_managed_object(id + COMBINE_OFFSET, nil)
end
@ -423,7 +400,6 @@ end
function removeDevice(parent, id)
sco_source_node_properties[id] = nil
sco_a2dp_sink_node_properties[id] = nil
end
function createMonitor()
@ -466,6 +442,7 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
["device.id"] = dev_id,
["card.profile.device"] = DEVICE_SOURCE_ID,
["device.routes"] = "1",
["priority.driver"] = 2010,
["priority.session"] = 2010,
["bluez5.loopback"] = true,
["filter.smart"] = true,
@ -479,41 +456,6 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
end
function CreateDeviceLoopbackSink (dev_name, dec_desc, dev_id)
local args = Json.Object {
["capture.props"] = Json.Object {
["node.name"] = string.format ("bluez_output.%s", dev_name),
["node.description"] = string.format ("%s", dec_desc),
["node.virtual"] = false,
["audio.position"] = "[FL, FR]",
["media.class"] = "Audio/Sink",
["device.id"] = dev_id,
["card.profile.device"] = DEVICE_SINK_ID,
["device.routes"] = "1",
["priority.session"] = 2010,
["bluez5.sink-loopback"] = true,
["filter.smart"] = true,
["filter.smart.target"] = Json.Object {
["bluez5.sink-loopback-target"] = true,
["bluez5.sink-loopback"] = false,
["device.id"] = dev_id
}
},
["playback.props"] = Json.Object {
["node.name"] = string.format ("bluez_playback_internal.%s", dev_name),
["media.class"] = "Stream/Output/Audio/Internal",
["node.description"] =
string.format ("Bluetooth internal playback stream for %s", dec_desc),
["bluez5.sink-loopback"] = true,
["node.passive"] = true,
["node.dont-fallback"] = true,
["node.linger"] = true,
["state.restore-props"] = false,
}
}
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
end
function checkProfiles (dev)
local device_id = dev["bound-id"]
local props = dev.properties
@ -546,19 +488,19 @@ function checkProfiles (dev)
return
end
-- Create the source loopback device if never created before
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
if source_loopback == nil then
-- Create the loopback device if never created before
local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
if loopback == nil then
local dev_name = props["api.bluez5.address"] or props["device.name"]
local dec_desc = props["device.description"] or props["device.name"]
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
log:info("create SCO source loopback node: " .. dev_name)
log:info("create SCO loopback node: " .. dev_name)
-- sanitize description, replace ':' with ' '
dec_desc = dec_desc:gsub("(:)", " ")
source_loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback)
-- recreate any sco source node
local properties = sco_source_node_properties[device_spa_id]
@ -579,39 +521,6 @@ function checkProfiles (dev)
end
end
end
local sink_loopback = spa_device:get_managed_object (LOOPBACK_SINK_ID)
if sink_loopback == nil then
local dev_name = props["api.bluez5.address"] or props["device.name"]
local dec_desc = props["device.description"] or props["device.name"]
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
log:info("create SCO-A2DP sink loopback node: " .. dev_name)
-- sanitize description, replace ':' with ' '
dec_desc = dec_desc:gsub("(:)", " ")
sink_loopback = CreateDeviceLoopbackSink (dev_name, dec_desc, device_id)
spa_device:store_managed_object(LOOPBACK_SINK_ID, sink_loopback)
-- recreate any sco-a2dp sink node
local properties = sco_a2dp_sink_node_properties[device_spa_id]
if properties ~= nil then
local node_id = tonumber(properties["spa.object.id"])
local node = spa_device:get_managed_object (node_id)
if node ~= nil then
log:info("Recreate node: " .. properties["node.name"] .. ": " ..
properties["factory.name"] .. " " .. tostring (node_id))
spa_device:store_managed_object(node_id, nil)
properties["bluez5.sink-loopback-target"] = true
properties["api.bluez5.internal"] = true
node = LocalNode("adapter", properties)
node:activate(Feature.Proxy.BOUND)
spa_device:store_managed_object(node_id, node)
end
end
end
end
function onDeviceParamsChanged (dev, param_name)

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.info ("Creating " .. direction .. " loopback for audio group " .. group ..
Log.warning ("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,7 +16,6 @@ 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
@ -41,7 +40,6 @@ 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

@ -1,53 +0,0 @@
-- 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

@ -1,94 +0,0 @@
-- 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,11 +4,6 @@ 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

@ -1,30 +0,0 @@
_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,6 +174,7 @@ 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,9 +64,3 @@ 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,23 +177,6 @@ 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

@ -1,93 +0,0 @@
-- 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")