From c3ebd79f009a638ee791105beeacbf3e8918732e Mon Sep 17 00:00:00 2001 From: Julian Bouzas Date: Thu, 4 Dec 2025 10:53:22 -0500 Subject: [PATCH] lib: Add new WpCollection API This API allows grouping globals into collections. A collection is essentially a metadata object with information about all the globals it collects. Grouping globals into collections has the advantage of avoiding defining complex set of properties to match the interested globals, and they are also a more generic way to represent a set of globals that share something in common. --- lib/wp/collection.c | 462 ++++++++++++++++++++++++++++++++++++ lib/wp/collection.h | 49 ++++ lib/wp/meson.build | 2 + lib/wp/private/collection.h | 27 +++ lib/wp/wp.h | 1 + 5 files changed, 541 insertions(+) create mode 100644 lib/wp/collection.c create mode 100644 lib/wp/collection.h create mode 100644 lib/wp/private/collection.h diff --git a/lib/wp/collection.c b/lib/wp/collection.c new file mode 100644 index 00000000..12569f33 --- /dev/null +++ b/lib/wp/collection.c @@ -0,0 +1,462 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include "core.h" +#include "collection.h" +#include "metadata.h" +#include "log.h" +#include "object-manager.h" + +#include "private/collection.h" + +WP_DEFINE_LOCAL_LOG_TOPIC ("wp-collection") + +/*! \defgroup wpcollection WpCollection */ +/*! + * \struct WpCollection + * + * The WpCollection class allows grouping multiple WirePlumber global proxies + * together so they can be treated as a single unit. + * + * WpCollection API. + */ + +struct _WpCollection +{ + WpObject parent; + + gchar *name; + + WpObjectManager *metadata_om; + GWeakRef metadata; + GHashTable *globals_info; +}; + +enum { + PROP_0, + PROP_NAME, +}; + +G_DEFINE_TYPE (WpCollection, wp_collection, WP_TYPE_OBJECT) + +static void +wp_collection_init (WpCollection * self) +{ + g_weak_ref_init (&self->metadata, NULL); + + self->globals_info = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, (GDestroyNotify) wp_spa_json_unref); +} + +static void +wp_collection_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpCollection *self = WP_COLLECTION (object); + + switch (property_id) { + case PROP_NAME: + g_clear_pointer (&self->name, g_free); + self->name = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_collection_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpCollection *self = WP_COLLECTION (object); + + switch (property_id) { + case PROP_NAME: + g_value_set_string (value, self->name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +enum { + STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START, +}; + +static WpObjectFeatures +wp_collection_get_supported_features (WpObject * self) +{ + return WP_COLLECTION_LOADED; +} + +static guint +wp_collection_activate_get_next_step (WpObject * self, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + g_return_val_if_fail (missing == WP_COLLECTION_LOADED, + WP_TRANSITION_STEP_ERROR); + + return STEP_LOAD; +} + +static void +on_metadata_changed (WpMetadata *m, guint32 subject, + const gchar *key, const gchar *type, const gchar *value, gpointer d) +{ + WpCollection *self = WP_COLLECTION (d); + + if (key) { + WpProperties *info; + guint32 global_id = SPA_ID_INVALID; + + /* Parse global Id */ + if (!spa_atou32 (key, &global_id, 10)) { + wp_warning_object (self, + "Failed to parse global Id from metadata key '%s'. Ignoring...", key); + return; + } + + /* Check if value is valid */ + if (value) { + g_autoptr (WpSpaJson) json_val = wp_spa_json_new_from_string (value); + gboolean val = FALSE; + if (!wp_spa_json_is_boolean (json_val) || + !wp_spa_json_parse_boolean (json_val, &val) || !val) { + wp_warning_object (self, + "Metadata value '%s' is not a JSON boolean value set to TRUE. " + "Ignoring change...", value); + return; + } + + info = g_hash_table_lookup (self->globals_info, + GUINT_TO_POINTER (global_id)); + if (!info) { + WpSpaJson *new_info = wp_spa_json_new_boolean (TRUE); + g_hash_table_insert (self->globals_info, GUINT_TO_POINTER (global_id), + new_info); + wp_info_object (self, "Added global Id %u", global_id); + } + } else { + g_hash_table_remove (self->globals_info, GUINT_TO_POINTER (global_id)); + wp_info_object (self, "Removed global Id %u", global_id); + } + } else { + g_hash_table_remove_all (self->globals_info); + wp_info_object (self, "Removed all global Ids"); + } +} + +static void +on_metadata_added (WpObjectManager *om, WpMetadata *m, gpointer d) +{ + WpTransition * transition = WP_TRANSITION (d); + WpCollection * self = wp_transition_get_source_object (transition); + g_autoptr (WpProperties) props = NULL; + const gchar *metadata_name = NULL; + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) item = G_VALUE_INIT; + + /* Make sure the metadata has a name */ + props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (m)); + g_return_if_fail (props); + metadata_name = wp_properties_get (props, "metadata.name"); + g_return_if_fail (metadata_name); + g_return_if_fail (g_str_equal (metadata_name, self->name)); + + /* Listen for metadata changes */ + g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed), + self, 0); + + /* Update the globals info table with the information from metadata */ + it = wp_metadata_new_iterator (m, 0); + for (; wp_iterator_next (it, &item); g_value_unset (&item)) { + WpMetadataItem *mi = g_value_get_boxed (&item); + const gchar *key = wp_metadata_item_get_key (mi); + const gchar *value = wp_metadata_item_get_value (mi); + guint32 global_id = SPA_ID_INVALID; + WpSpaJson *info = NULL; + + /* Parse global Id */ + if (!spa_atou32 (key, &global_id, 10)) { + wp_warning_object (self, + "Failed to parse global Id from metadata key '%s'. Ignoring...", + key); + continue; + } + + /* Check if value is valid */ + if (value) { + g_autoptr (WpSpaJson) json_val = wp_spa_json_new_from_string (value); + gboolean val = FALSE; + if (!wp_spa_json_is_boolean (json_val) || + !wp_spa_json_parse_boolean (json_val, &val) || !val) { + wp_warning_object (self, + "Metadata value '%s' is not a JSON boolean value set to TRUE. " + "Ignoring change...", value); + continue; + } + } + + info = g_hash_table_lookup (self->globals_info, + GUINT_TO_POINTER (global_id)); + if (!info) { + WpSpaJson *new_info = wp_spa_json_new_boolean (TRUE); + g_hash_table_insert (self->globals_info, GUINT_TO_POINTER (global_id), + new_info); + } + } + + /* Set the metadata weak ref */ + g_weak_ref_set (&self->metadata, m); + + /* Set the loaded feature */ + wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_LOADED, 0); +} + +static void +wp_collection_activate_execute_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + WpCollection *self = WP_COLLECTION (object); + g_autoptr (WpCore) core = wp_object_get_core (object); + + switch (step) { + case STEP_LOAD: { + g_clear_object (&self->metadata_om); + self->metadata_om = wp_object_manager_new (); + + wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + "metadata.name", "=s", self->name, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + "wireplumber.collection", "=b", TRUE, + NULL); + wp_object_manager_request_object_features (self->metadata_om, + WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL); + g_signal_connect_object (self->metadata_om, "object-added", + G_CALLBACK (on_metadata_added), transition, 0); + wp_core_install_object_manager (core, self->metadata_om); + + wp_info_object (self, "looking for metadata object named %s", self->name); + break; + } + + case WP_TRANSITION_STEP_ERROR: + break; + default: + g_assert_not_reached (); + } +} + +static void +wp_collection_deactivate (WpObject * object, WpObjectFeatures features) +{ + WpCollection *self = WP_COLLECTION (object); + + g_clear_object (&self->metadata_om); + g_weak_ref_set (&self->metadata, NULL); + g_hash_table_remove_all (self->globals_info); + + wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL); +} + +static void +wp_collection_finalize (GObject * object) +{ + WpCollection *self = WP_COLLECTION (object); + + g_clear_object (&self->metadata_om); + g_weak_ref_clear (&self->metadata); + g_clear_pointer (&self->globals_info, g_hash_table_unref); + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (wp_collection_parent_class)->finalize (object); +} + +static void +wp_collection_class_init (WpCollectionClass * klass) +{ + GObjectClass * object_class = (GObjectClass *) klass; + WpObjectClass *wpobject_class = (WpObjectClass *) klass; + + object_class->finalize = wp_collection_finalize; + object_class->set_property = wp_collection_set_property; + object_class->get_property = wp_collection_get_property; + + wpobject_class->get_supported_features = wp_collection_get_supported_features; + wpobject_class->activate_get_next_step = wp_collection_activate_get_next_step; + wpobject_class->activate_execute_step = wp_collection_activate_execute_step; + wpobject_class->deactivate = wp_collection_deactivate; + + g_object_class_install_property (object_class, PROP_NAME, + g_param_spec_string ("name", "name", + "The collection name", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} + +/*! + * \brief Creates a new WpCollection object + * + * \ingroup wpcollection + * \param core the WpCore + * \param name: the name of the collection + * \returns (transfer full): a new WpCollection object + */ +WpCollection * +wp_collection_new (WpCore * core, const gchar * name) +{ + g_return_val_if_fail (core, NULL); + g_return_val_if_fail (name, NULL); + + return g_object_new (WP_TYPE_COLLECTION, "core", core, "name", name, NULL); +} + +/*! + * \brief Gets the name of the collection + * + * \ingroup wpcollection + * \param self the WpCollection + * \returns (transfer none): the name of the collection + */ +const gchar * +wp_collection_get_name (WpCollection *self) +{ + g_return_val_if_fail (WP_IS_COLLECTION (self), NULL); + + return self->name; +} + +/*! + * \brief Gets the metadata of the collection + * + * \ingroup wpcollection + * \param self the WpCollection + * \returns (nullable) (transfer full): the metadata of the collection + */ +WpMetadata * +wp_collection_get_metadata (WpCollection *self) +{ + g_return_val_if_fail (WP_IS_COLLECTION (self), NULL); + + return g_weak_ref_get (&self->metadata); +} + +/*! + * \brief Adds a global into the collection + * + * \ingroup wpcollection + * \param self the WpCollection + * \param global (transfer none): the global to collect into the collection + * \returns TRUE if the global was collected, FALSE otherwise. + */ +gboolean +wp_collection_collect_global (WpCollection *self, WpGlobalProxy *global) +{ + g_autoptr (WpMetadata) m = NULL; + g_autofree gchar *key = NULL; + g_autoptr (WpSpaJson) json_value = NULL; + guint32 bound_id; + + g_return_val_if_fail (WP_IS_COLLECTION (self), FALSE); + g_return_val_if_fail (WP_IS_GLOBAL_PROXY (global), FALSE); + g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (global), + WP_PROXY_FEATURE_BOUND), FALSE); + + m = g_weak_ref_get (&self->metadata); + if (!m) + return FALSE; + + if (wp_collection_contains_global (self, global)) + return TRUE; + + bound_id = wp_proxy_get_bound_id (WP_PROXY (global)); + json_value = wp_spa_json_new_boolean (TRUE); + key = g_strdup_printf ("%u", bound_id); + + g_hash_table_insert (self->globals_info, GUINT_TO_POINTER (bound_id), + wp_spa_json_ref (json_value)); + wp_metadata_set (m, 0, key, "Spa:String:JSON", + wp_spa_json_get_data (json_value)); + return TRUE; +} + +/*! + * \brief Drops a global from the collection + * + * \ingroup wpcollection + * \param self the WpCollection + * \param global (transfer none): the global to drop from the collection + * \returns TRUE if the global was dropped, FALSE otherwise. + */ +gboolean +wp_collection_drop_global (WpCollection *self, WpGlobalProxy *global) +{ + g_autoptr (WpMetadata) m = NULL; + g_autofree gchar *key = NULL; + guint32 bound_id; + + g_return_val_if_fail (WP_IS_COLLECTION (self), FALSE); + g_return_val_if_fail (WP_IS_GLOBAL_PROXY (global), FALSE); + g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (global), + WP_PROXY_FEATURE_BOUND), FALSE); + + bound_id = wp_proxy_get_bound_id (WP_PROXY (global)); + + m = g_weak_ref_get (&self->metadata); + if (!m) + return FALSE; + + g_hash_table_remove (self->globals_info, GUINT_TO_POINTER (bound_id)); + key = g_strdup_printf ("%u", bound_id); + wp_metadata_set (m, 0, key, NULL, NULL); + return TRUE; +} + +/*! + * \brief Checks whether the collection contains a global or not + * + * \ingroup wpcollection + * \param self the WpCollection + * \param global (transfer none): the global to check + * \returns TRUE if the collection contains the global, FALSE otherwise. + */ +gboolean +wp_collection_contains_global (WpCollection *self, WpGlobalProxy *global) +{ + guint32 bound_id; + + g_return_val_if_fail (WP_IS_COLLECTION (self), FALSE); + g_return_val_if_fail (WP_IS_GLOBAL_PROXY (global), FALSE); + g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (global), + WP_PROXY_FEATURE_BOUND), FALSE); + + bound_id = wp_proxy_get_bound_id (WP_PROXY (global)); + return g_hash_table_contains (self->globals_info, + GUINT_TO_POINTER (bound_id)); +} + +/*! + * \brief Gets the total number of globals that the collection has + * + * \ingroup wpcollection + * \param self the WpCollection + * \returns the total number of globals the collection has + */ +guint +wp_collection_get_global_count (WpCollection *self) +{ + g_return_val_if_fail (WP_IS_COLLECTION (self), 0); + + return g_hash_table_size (self->globals_info); +} diff --git a/lib/wp/collection.h b/lib/wp/collection.h new file mode 100644 index 00000000..84d667f9 --- /dev/null +++ b/lib/wp/collection.h @@ -0,0 +1,49 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WIREPLUMBER_COLLECTION_H__ +#define __WIREPLUMBER_COLLECTION_H__ + +#include "metadata.h" + +G_BEGIN_DECLS + +/*! + * \brief Flags to be used as WpObjectFeatures for WpCollection. + * \ingroup wpcollection + */ +typedef enum { /*< flags >*/ + /*! Loads the collection */ + WP_COLLECTION_LOADED = (1 << 0), +} WpCollectionFeatures; + +/*! + * \brief The WpCollection GType + * \ingroup wpcollection + */ +#define WP_TYPE_COLLECTION (wp_collection_get_type ()) + +WP_API +G_DECLARE_FINAL_TYPE (WpCollection, wp_collection, WP, COLLECTION, WpObject) + +WP_API +const gchar * wp_collection_get_name (WpCollection *self); + +WP_API +WpMetadata *wp_collection_get_metadata (WpCollection *self); + +WP_API +gboolean wp_collection_contains_global (WpCollection *self, + WpGlobalProxy *global); + +WP_API +guint wp_collection_get_global_count (WpCollection *self); + +G_END_DECLS + +#endif diff --git a/lib/wp/meson.build b/lib/wp/meson.build index 9aacc624..f7780a0c 100644 --- a/lib/wp/meson.build +++ b/lib/wp/meson.build @@ -1,6 +1,7 @@ wp_lib_sources = files( 'base-dirs.c', 'client.c', + 'collection.c', 'component-loader.c', 'conf.c', 'core.c', @@ -48,6 +49,7 @@ wp_lib_priv_sources = files( wp_lib_headers = files( 'base-dirs.h', 'client.h', + 'collection.h', 'component-loader.h', 'conf.h', 'core.h', diff --git a/lib/wp/private/collection.h b/lib/wp/private/collection.h new file mode 100644 index 00000000..1a09b839 --- /dev/null +++ b/lib/wp/private/collection.h @@ -0,0 +1,27 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WIREPLUMBER_PRIVATE_COLLECTION_H__ +#define __WIREPLUMBER_PRIVATE_COLLECTION_H__ + +#include "global-proxy.h" + +G_BEGIN_DECLS + +typedef struct _WpCollection WpCollection; + +WpCollection * wp_collection_new (WpCore * core, const gchar * name); + +gboolean wp_collection_collect_global (WpCollection *self, + WpGlobalProxy *global); + +gboolean wp_collection_drop_global (WpCollection *self, WpGlobalProxy *global); + +G_END_DECLS + +#endif diff --git a/lib/wp/wp.h b/lib/wp/wp.h index 3e010f96..8b62837b 100644 --- a/lib/wp/wp.h +++ b/lib/wp/wp.h @@ -47,6 +47,7 @@ #include "wpversion.h" #include "factory.h" #include "settings.h" +#include "collection.h" G_BEGIN_DECLS