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