diff --git a/lib/wp/collection.c b/lib/wp/collection.c new file mode 100644 index 00000000..6b3fc1bb --- /dev/null +++ b/lib/wp/collection.c @@ -0,0 +1,991 @@ +/* WirePlumber + * + * Copyright © 2026 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include "collection.h" +#include "core.h" +#include "log.h" +#include "error.h" +#include "wpenums.h" + +#include +#include +#include + +WP_DEFINE_LOCAL_LOG_TOPIC ("wp-collection") + +/*! \defgroup wpcollection WpCollection */ +/*! + * \struct WpCollection + * + * The WpCollection class allows accessing the properties and methods of + * PipeWire collection object (`struct pw_collection`). + * + * A WpCollection is constructed internally when a new collection object appears + * on the PipeWire registry and it is made available through the WpObjectManager + * API. + * + * \gsignals + * + * \par global_added + * \parblock + * \code + * void + * global_added_callback (WpCollection * self, + * guint global_id, + * gpointer user_data) + * \endcode + * Emitted when a gobal was added into the collection + * + * Parameters: + * - `global_id` - the added global id + * + * Flags: G_SIGNAL_RUN_LAST + * \endparblock + * + * \par global_removed + * \parblock + * \code + * void + * global_removed_callback (WpCollection * self, + * guint global_id, + * gpointer user_data) + * \endcode + * Emitted when a gobal was removed from the collection + * + * Parameters: + * - `global_id` - the removed global id + * + * Flags: G_SIGNAL_RUN_LAST + * \endparblock + */ + +enum { + SIGNAL_GLOBAL_COLLECTED, + SIGNAL_GLOBAL_DROPPED, + N_SIGNALS, +}; + +static guint32 signals[N_SIGNALS] = {0}; + +/* data structure */ + +struct item +{ + uint32_t subject; + gchar *key; + gchar *type; + gchar *value; +}; + +static void +set_item (struct item * item, uint32_t subject, const char * key, + const char * type, const char * value) +{ + item->subject = subject; + item->key = key ? g_strdup (key) : NULL; + item->type = type ? g_strdup (type) : NULL; + item->value = value ? g_strdup (value) : NULL; +} + +static void +clear_item (struct item * item) +{ + g_clear_pointer (&item->key, g_free); + g_clear_pointer (&item->type, g_free); + g_clear_pointer (&item->value, g_free); + spa_zero (*item); +} + +static struct item * +find_item (struct pw_array * metadata, uint32_t subject, const char * key) +{ + struct item *item; + + pw_array_for_each (item, metadata) { + if (item->subject == subject && (key == NULL || !strcmp (item->key, key))) + return item; + } + return NULL; +} + +static void +clear_items (struct pw_array * metadata) +{ + struct item *item; + + pw_array_consume (item, metadata) { + clear_item (item); + pw_array_remove (metadata, item); + } + pw_array_reset (metadata); +} + +struct _WpCollection +{ + WpGlobalProxy parent; + + struct pw_metadata *iface; + struct spa_hook listener; + struct pw_array metadata; + gboolean listener_added; +}; + + +G_DEFINE_TYPE (WpCollection, wp_collection, WP_TYPE_GLOBAL_PROXY) + +static void +wp_collection_init (WpCollection * self) +{ + pw_array_init (&self->metadata, 4096); +} + +static void +wp_collection_finalize (GObject * object) +{ + WpCollection * self = WP_COLLECTION (object); + + pw_array_clear (&self->metadata); + + G_OBJECT_CLASS (wp_collection_parent_class)->finalize (object); +} + +static WpObjectFeatures +wp_collection_get_supported_features (WpObject * object) +{ + return WP_PROXY_FEATURE_BOUND | WP_COLLECTION_FEATURE_DATA; +} + +enum { + STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START, + STEP_CACHE +}; + +static guint +wp_collection_activate_get_next_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + g_return_val_if_fail ( + missing & (WP_PROXY_FEATURE_BOUND | WP_COLLECTION_FEATURE_DATA), + WP_TRANSITION_STEP_ERROR); + + /* bind if not already bound */ + if (missing & WP_PROXY_FEATURE_BOUND) + return STEP_BIND; + else + return STEP_CACHE; +} + +static void +wp_collection_activate_execute_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + switch (step) { + case STEP_CACHE: + /* just wait for initial_sync_done() */ + break; + default: + WP_OBJECT_CLASS (wp_collection_parent_class)-> + activate_execute_step (object, transition, step, missing); + break; + } +} + +static int +metadata_event_property (void *object, uint32_t subject, const char *key, + const char *type, const char *value) +{ + WpCollection *self = WP_COLLECTION (object); + struct item *item = NULL; + + /* Clear subject if key is NULL */ + if (key == NULL) { + while (true) { + guint32 global_id = SPA_ID_INVALID; + + item = find_item (&self->metadata, subject, NULL); + if (item == NULL) + break; + + pw_array_remove (&self->metadata, item); + + if (spa_atou32 (item->key, &global_id, 0)) + g_signal_emit (self, signals[SIGNAL_GLOBAL_DROPPED], 0, global_id); + + clear_item (item); + } + return 0; + } + + item = find_item (&self->metadata, subject, key); + if (item == NULL) { + if (value == NULL) + return 0; + item = pw_array_add (&self->metadata, sizeof (*item)); + if (item == NULL) + return -errno; + } else { + clear_item (item); + } + + if (value != NULL) { + guint32 global_id = SPA_ID_INVALID; + set_item (item, subject, key, type, value); + if (spa_atou32 (key, &global_id, 0)) + g_signal_emit (self, signals[SIGNAL_GLOBAL_COLLECTED], 0, global_id); + } else { + guint32 global_id = SPA_ID_INVALID; + pw_array_remove (&self->metadata, item); + if (spa_atou32 (key, &global_id, 0)) + g_signal_emit (self, signals[SIGNAL_GLOBAL_DROPPED], 0, global_id); + } + + return 0; +} + +static const struct pw_metadata_events metadata_events = { + PW_VERSION_METADATA_EVENTS, + .property = metadata_event_property, +}; + +static void +wp_collection_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy) +{ + WpCollection *self = WP_COLLECTION (proxy); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + self->iface = (struct pw_metadata *) pw_proxy; + pw_metadata_add_listener (self->iface, &self->listener, &metadata_events, + self); + self->listener_added = TRUE; + + wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_FEATURE_DATA, 0); +} + +static void +wp_collection_pw_proxy_destroyed (WpProxy * proxy) +{ + WpCollection *self = WP_COLLECTION (proxy); + + if (self->listener_added) { + spa_hook_remove (&self->listener); + self->listener_added = FALSE; + } + clear_items (&self->metadata); + wp_object_update_features (WP_OBJECT (self), 0, WP_COLLECTION_FEATURE_DATA); + + WP_PROXY_CLASS (wp_collection_parent_class)->pw_proxy_destroyed (proxy); +} + +static void +wp_collection_class_init (WpCollectionClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpObjectClass *wpobject_class = (WpObjectClass *) klass; + WpProxyClass *proxy_class = (WpProxyClass *) klass; + + object_class->finalize = wp_collection_finalize; + + 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; + + proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Metadata; + proxy_class->pw_iface_version = PW_VERSION_METADATA; + proxy_class->pw_proxy_created = wp_collection_pw_proxy_created; + proxy_class->pw_proxy_destroyed = wp_collection_pw_proxy_destroyed; + + signals[SIGNAL_GLOBAL_COLLECTED] = g_signal_new ("global-collected", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + + signals[SIGNAL_GLOBAL_DROPPED] = g_signal_new ("global-dropped", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); +} + +/*! + * \brief Gets the name of the collection + * + * \ingroup wpcollection + * \param self the collection object + * \returns (nullable): the name of the collection + */ +const gchar * +wp_collection_get_name (WpCollection * self) +{ + g_autoptr (WpProperties) props = NULL; + + g_return_val_if_fail (WP_IS_COLLECTION (self), NULL); + + props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)); + return props ? wp_properties_get (props, "collection.name") : NULL; +} + +/*! + * \brief Gets the total number of globals the collection has collected + * + * \ingroup wpcollection + * \param self the collection object + * \returns the total number of globals the collection has collected + */ +gsize +wp_collection_get_size (WpCollection * self) +{ + g_return_val_if_fail (WP_IS_COLLECTION (self), 0); + + return pw_array_get_len (&self->metadata, struct item); +} + +/*! + * \brief Checks if a global ID is collected in the collection + * + * \ingroup wpcollection + * \param self the collection object + * \param global_id the global ID to check + * \returns TRUE if the global ID is collected in the colleciton, FALSE + * otherwise. + */ +gboolean +wp_collection_contains_global (WpCollection * self, guint32 global_id) +{ + struct item *item; + + g_return_val_if_fail (WP_IS_COLLECTION (self), FALSE); + + pw_array_for_each (item, &self->metadata) { + guint32 id = SPA_ID_INVALID; + if (spa_atou32 (item->key, &id, 0) && id == global_id) + return TRUE; + } + return FALSE; +} + +/*! + * \brief Collects a global ID into the collection. + * + * \ingroup wpcollection + * \param self the collection object + * \param global_id the global ID to collect + */ +void +wp_collection_collect_global (WpCollection * self, guint32 global_id) +{ + g_autofree gchar *global_id_str = NULL; + + g_return_if_fail (WP_IS_COLLECTION (self)); + g_return_if_fail (global_id != SPA_ID_INVALID); + + global_id_str = g_strdup_printf ("%u", global_id); + pw_metadata_set_property (self->iface, 0, global_id_str, NULL, "collected"); +} + +/*! + * \brief Drops a global ID from the collection. + * + * \ingroup wpcollection + * \param self the collection object + * \param global_id the global ID to drop + */ +void +wp_collection_drop_global (WpCollection * self, guint32 global_id) +{ + g_autofree gchar *global_id_str = NULL; + + g_return_if_fail (WP_IS_COLLECTION (self)); + g_return_if_fail (global_id != SPA_ID_INVALID); + + global_id_str = g_strdup_printf ("%u", global_id); + pw_metadata_set_property (self->iface, 0, global_id_str, NULL, NULL); +} + +/*! + * \brief Clears all collected global Ids + * \ingroup wpcollection + * \param self the collection object + */ +void +wp_collection_clear (WpCollection * self) +{ + g_return_if_fail (WP_IS_COLLECTION (self)); + + pw_metadata_clear (self->iface); +} + +struct collection_iterator_data +{ + WpCollection *collection; + const struct item *item; +}; + +static void +collection_iterator_reset (WpIterator *it) +{ + struct collection_iterator_data *it_data = wp_iterator_get_user_data (it); + WpCollection *self = it_data->collection; + + it_data->item = pw_array_first (&self->metadata); +} + +static gboolean +collection_iterator_next (WpIterator *it, GValue *item) +{ + struct collection_iterator_data *it_data = wp_iterator_get_user_data (it); + WpCollection *self = it_data->collection; + + while (pw_array_check (&self->metadata, it_data->item)) { + guint global_id = SPA_ID_INVALID; + if (spa_atou32 (it_data->item->key, &global_id, 0)) { + g_value_init (item, G_TYPE_UINT); + g_value_set_uint (item, global_id); + it_data->item++; + return TRUE; + } + it_data->item++; + } + return FALSE; +} + +static void +collection_iterator_finalize (WpIterator *it) +{ + struct collection_iterator_data *it_data = wp_iterator_get_user_data (it); + g_object_unref (it_data->collection); +} + +static const WpIteratorMethods collection_iterator_methods = { + .version = WP_ITERATOR_METHODS_VERSION, + .reset = collection_iterator_reset, + .next = collection_iterator_next, + .finalize = collection_iterator_finalize, +}; + +/*! + * \brief Iterates over all the collected global IDs. + * + * \ingroup wpcollection + * \param self a collection object + * \returns (transfer full): an iterator that iterates over the collected global + * IDs. The type of the iterator item is an unsigned integer. + */ +WpIterator * +wp_collection_new_iterator (WpCollection * self) +{ + g_autoptr (WpIterator) it = NULL; + struct collection_iterator_data *it_data; + + g_return_val_if_fail (WP_IS_COLLECTION (self), NULL); + + it = wp_iterator_new (&collection_iterator_methods, + sizeof (struct collection_iterator_data)); + it_data = wp_iterator_get_user_data (it); + it_data->collection = g_object_ref (self); + it_data->item = pw_array_first (&self->metadata); + return g_steal_pointer (&it); +} + +/*! + * \struct WpImplCollection + * The implementation side of the collection object. + * + * Activate this object with at least WP_PROXY_FEATURE_BOUND to export it to + * PipeWire. + */ +struct _WpImplCollection +{ + WpProxy parent; + + gchar *name; + WpProperties *properties; + + struct pw_metadata *iface; + struct pw_impl_metadata *impl; + struct spa_hook listener; + struct pw_array metadata; +}; + +enum { + PROP_0, + PROP_NAME, + PROP_PROPERTIES, +}; + +enum { + IMPL_SIGNAL_GLOBAL_COLLECTED, + IMPL_SIGNAL_GLOBAL_DROPPED, + IMPL_N_SIGNALS, +}; + +static guint32 impl_signals[IMPL_N_SIGNALS] = {0}; + +G_DEFINE_TYPE (WpImplCollection, wp_impl_collection, WP_TYPE_PROXY) + +static void +wp_impl_collection_init (WpImplCollection * self) +{ + pw_array_init (&self->metadata, 4096); +} + +static int +impl_metadata_event_property (void *object, uint32_t subject, const char *key, + const char *type, const char *value) +{ + WpImplCollection *self = WP_IMPL_COLLECTION (object); + struct item *item = NULL; + + /* Clear subject if key is NULL */ + if (key == NULL) { + while (true) { + guint32 global_id = SPA_ID_INVALID; + + item = find_item (&self->metadata, subject, NULL); + if (item == NULL) + break; + + pw_array_remove (&self->metadata, item); + + if (spa_atou32 (item->key, &global_id, 0)) + g_signal_emit (self, impl_signals[IMPL_SIGNAL_GLOBAL_DROPPED], 0, + global_id); + + clear_item (item); + } + return 0; + } + + item = find_item (&self->metadata, subject, key); + if (item == NULL) { + if (value == NULL) + return 0; + item = pw_array_add (&self->metadata, sizeof (*item)); + if (item == NULL) + return -errno; + } else { + clear_item (item); + } + + if (value != NULL) { + guint32 global_id = SPA_ID_INVALID; + set_item (item, subject, key, type, value); + if (spa_atou32 (key, &global_id, 0)) + g_signal_emit (self, impl_signals[IMPL_SIGNAL_GLOBAL_COLLECTED], 0, + global_id); + } else { + guint32 global_id = SPA_ID_INVALID; + pw_array_remove (&self->metadata, item); + if (spa_atou32 (key, &global_id, 0)) + g_signal_emit (self, impl_signals[IMPL_SIGNAL_GLOBAL_DROPPED], 0, + global_id); + } + + return 0; +} + +static const struct pw_impl_metadata_events impl_metadata_events = { + PW_VERSION_IMPL_METADATA_EVENTS, + .property = impl_metadata_event_property, +}; + +static void +wp_impl_collection_constructed (GObject *object) +{ + WpImplCollection *self = WP_IMPL_COLLECTION (object); + g_autoptr (WpCore) core = NULL; + struct pw_context *pw_context; + struct pw_properties *props; + + core = wp_object_get_core (WP_OBJECT (self)); + g_return_if_fail (core); + pw_context = wp_core_get_pw_context (core); + g_return_if_fail (pw_context); + + /* Make sure the collection name and flag is set */ + if (!self->properties) + self->properties = wp_properties_new_empty (); + wp_properties_set (self->properties, "collection.name", self->name); + wp_properties_set (self->properties, "wireplumber.collection", "true"); + + props = wp_properties_to_pw_properties (self->properties); + self->impl = pw_context_create_metadata (pw_context, self->name, props , 0); + g_return_if_fail (self->impl); + self->iface = pw_impl_metadata_get_implementation (self->impl); + g_return_if_fail (self->iface); + + pw_impl_metadata_add_listener (self->impl, &self->listener, + &impl_metadata_events, self); + + G_OBJECT_CLASS (wp_impl_collection_parent_class)->constructed (object); +} + +static void +wp_impl_collection_finalize (GObject * object) +{ + WpImplCollection *self = WP_IMPL_COLLECTION (object); + + pw_array_clear (&self->metadata); + spa_hook_remove (&self->listener); + g_clear_pointer (&self->impl, pw_impl_metadata_destroy); + g_clear_pointer (&self->properties, wp_properties_unref); + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (wp_impl_collection_parent_class)->finalize (object); +} + +static void +wp_impl_collection_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpImplCollection *self = WP_IMPL_COLLECTION (object); + + switch (property_id) { + case PROP_NAME: + g_clear_pointer (&self->name, g_free); + self->name = g_value_dup_string (value); + break; + case PROP_PROPERTIES: + g_clear_pointer (&self->properties, wp_properties_unref); + self->properties = g_value_dup_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_impl_collection_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpImplCollection *self = WP_IMPL_COLLECTION (object); + + switch (property_id) { + case PROP_NAME: + g_value_set_string (value, self->name); + break; + case PROP_PROPERTIES: + g_value_set_boxed (value, self->properties); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +enum { + STEP_EXPORT = WP_TRANSITION_STEP_CUSTOM_START, +}; + +static WpObjectFeatures +wp_impl_collection_get_supported_features (WpObject * object) +{ + return WP_PROXY_FEATURE_BOUND; +} + +static guint +wp_impl_collection_activate_get_next_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + g_return_val_if_fail (missing & (WP_PROXY_FEATURE_BOUND), + WP_TRANSITION_STEP_ERROR); + + return STEP_EXPORT; +} + +static void +wp_impl_collection_activate_execute_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + WpImplCollection *self = WP_IMPL_COLLECTION (object); + + switch (step) { + case STEP_EXPORT: { + g_autoptr (WpCore) core = wp_object_get_core (object); + struct pw_core *pw_core = wp_core_get_pw_core (core); + const struct pw_properties *props = NULL; + + /* no pw_core -> we are not connected */ + if (!pw_core) { + wp_transition_return_error (WP_TRANSITION (transition), g_error_new ( + WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "The WirePlumber core is not connected; " + "object cannot be exported to PipeWire")); + return; + } + + props = pw_impl_metadata_get_properties (self->impl); + wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core, + PW_TYPE_INTERFACE_Metadata, &props->dict, self->iface, 0)); + break; + } + default: + WP_OBJECT_CLASS (wp_impl_collection_parent_class)-> + activate_execute_step (object, transition, step, missing); + break; + } +} + +static void +wp_impl_collection_class_init (WpImplCollectionClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpObjectClass *wpobject_class = (WpObjectClass *) klass; + WpProxyClass *proxy_class = (WpProxyClass *) klass; + + object_class->constructed = wp_impl_collection_constructed; + object_class->finalize = wp_impl_collection_finalize; + object_class->set_property = wp_impl_collection_set_property; + object_class->get_property = wp_impl_collection_get_property; + + wpobject_class->get_supported_features = + wp_impl_collection_get_supported_features; + wpobject_class->activate_get_next_step = + wp_impl_collection_activate_get_next_step; + wpobject_class->activate_execute_step = + wp_impl_collection_activate_execute_step; + + proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Metadata; + proxy_class->pw_iface_version = PW_VERSION_METADATA; + + 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)); + + g_object_class_install_property (object_class, PROP_PROPERTIES, + g_param_spec_boxed ("properties", "properties", + "The collection properties", WP_TYPE_PROPERTIES, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + impl_signals[IMPL_SIGNAL_GLOBAL_COLLECTED] = g_signal_new ("global-collected", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); + + impl_signals[IMPL_SIGNAL_GLOBAL_DROPPED] = g_signal_new ("global-dropped", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_UINT); +} + +/*! + * \brief Creates a new impl collection + * \ingroup wpcollection + * \param core the core + * \param name (nullable): the collection name + * \param properties (nullable) (transfer full): the collection properties + * \returns (transfer full): a new WpImplCollection + */ +WpImplCollection * +wp_impl_collection_new (WpCore * core, const gchar *name, + WpProperties *properties) +{ + g_autoptr (WpProperties) props = properties; + + g_return_val_if_fail (WP_IS_CORE (core), NULL); + + return g_object_new (WP_TYPE_IMPL_COLLECTION, + "core", core, + "name", name, + "properties", props, + NULL); +} + +/*! + * \brief Gets the properties of the impl collection + * + * \ingroup wpcollection + * \param self the impl collection object + \returns (transfer full) (nullable): the properties of the impl collection + */ +WpProperties * +wp_impl_collection_get_properties (WpImplCollection *self) +{ + g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), NULL); + + return self->properties ? wp_properties_ref (self->properties) : NULL; +} + +/*! + * \brief Gets the name of the impl collection + * + * \ingroup wpcollection + * \param self the impl collection object + * \returns (nullable): the name of the impl collection + */ +const gchar * +wp_impl_collection_get_name (WpImplCollection *self) +{ + g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), NULL); + + return self->name; +} + +/*! + * \brief Gets the total number of globals the impl collection has collected + * + * \ingroup wpcollection + * \param self the impl collection object + * \returns the total number of globals the impl collection has collected + */ +gsize +wp_impl_collection_get_size (WpImplCollection * self) +{ + g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), 0); + + return pw_array_get_len (&self->metadata, struct item); +} + +/*! + * \brief Checks if a global ID is collected in the impl collection + * + * \ingroup wpcollection + * \param self the impl collection object + * \param global_id the global ID to check + * \returns TRUE if the global ID is collected in the colleciton, FALSE + * otherwise. + */ +gboolean +wp_impl_collection_contains_global (WpImplCollection * self, guint32 global_id) +{ + struct item *item; + + g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), FALSE); + + pw_array_for_each (item, &self->metadata) { + guint32 id = SPA_ID_INVALID; + if (spa_atou32 (item->key, &id, 0) && id == global_id) + return TRUE; + } + return FALSE; +} + +/*! + * \brief Collects a global ID into the impl collection. + * + * \ingroup wpcollection + * \param self the impl collection object + * \param global_id the global ID to collect + */ +void +wp_impl_collection_collect_global (WpImplCollection * self, guint32 global_id) +{ + g_autofree gchar *global_id_str = NULL; + + g_return_if_fail (WP_IS_IMPL_COLLECTION (self)); + g_return_if_fail (global_id != SPA_ID_INVALID); + + global_id_str = g_strdup_printf ("%u", global_id); + pw_metadata_set_property (self->iface, 0, global_id_str, NULL, "collected"); +} + +/*! + * \brief Drops a global ID from the impl collection. + * + * \ingroup wpcollection + * \param self the impl collection object + * \param global_id the global ID to drop + */ +void +wp_impl_collection_drop_global (WpImplCollection * self, guint32 global_id) +{ + g_autofree gchar *global_id_str = NULL; + + g_return_if_fail (WP_IS_IMPL_COLLECTION (self)); + g_return_if_fail (global_id != SPA_ID_INVALID); + + global_id_str = g_strdup_printf ("%u", global_id); + pw_metadata_set_property (self->iface, 0, global_id_str, NULL, NULL); +} + +/*! + * \brief Clears all collected global Ids + * \ingroup wpcollection + * \param self the impl collection object + */ +void +wp_impl_collection_clear (WpImplCollection * self) +{ + g_return_if_fail (WP_IS_IMPL_COLLECTION (self)); + + pw_metadata_clear (self->iface); +} + +struct impl_collection_iterator_data +{ + WpImplCollection *impl_collection; + const struct item *item; +}; + +static void +impl_collection_iterator_reset (WpIterator *it) +{ + struct impl_collection_iterator_data *it_data = + wp_iterator_get_user_data (it); + WpImplCollection *self = it_data->impl_collection; + + it_data->item = pw_array_first (&self->metadata); +} + +static gboolean +impl_collection_iterator_next (WpIterator *it, GValue *item) +{ + struct impl_collection_iterator_data *it_data = + wp_iterator_get_user_data (it); + WpImplCollection *self = it_data->impl_collection; + + while (pw_array_check (&self->metadata, it_data->item)) { + guint global_id = SPA_ID_INVALID; + if (spa_atou32 (it_data->item->key, &global_id, 0)) { + g_value_init (item, G_TYPE_UINT); + g_value_set_uint (item, global_id); + it_data->item++; + return TRUE; + } + it_data->item++; + } + return FALSE; +} + +static void +impl_collection_iterator_finalize (WpIterator *it) +{ + struct impl_collection_iterator_data *it_data = + wp_iterator_get_user_data (it); + g_object_unref (it_data->impl_collection); +} + +static const WpIteratorMethods impl_collection_iterator_methods = { + .version = WP_ITERATOR_METHODS_VERSION, + .reset = impl_collection_iterator_reset, + .next = impl_collection_iterator_next, + .finalize = impl_collection_iterator_finalize, +}; + +/*! + * \brief Iterates over all the collected global IDs. + * + * \ingroup wpcollection + * \param self an impl collection object + * \returns (transfer full): an iterator that iterates over the collected global + * IDs. The type of the iterator item is an unsigned integer. + */ +WpIterator * +wp_impl_collection_new_iterator (WpImplCollection * self) +{ + g_autoptr (WpIterator) it = NULL; + struct impl_collection_iterator_data *it_data; + + g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), NULL); + + it = wp_iterator_new (&impl_collection_iterator_methods, + sizeof (struct impl_collection_iterator_data)); + it_data = wp_iterator_get_user_data (it); + it_data->impl_collection = g_object_ref (self); + it_data->item = pw_array_first (&self->metadata); + return g_steal_pointer (&it); +} diff --git a/lib/wp/collection.h b/lib/wp/collection.h new file mode 100644 index 00000000..83ee9616 --- /dev/null +++ b/lib/wp/collection.h @@ -0,0 +1,102 @@ +/* WirePlumber + * + * Copyright © 2026 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WIREPLUMBER_COLLECTION_H__ +#define __WIREPLUMBER_COLLECTION_H__ + +#include "global-proxy.h" + +G_BEGIN_DECLS + +/*! + * \brief An extension of WpProxyFeatures for WpCollection objects + * \ingroup wpcollection + */ +typedef enum { /*< flags >*/ + /*! caches collection data locally */ + WP_COLLECTION_FEATURE_DATA = (WP_PROXY_FEATURE_CUSTOM_START << 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, + WpGlobalProxy) + +WP_API +const gchar *wp_collection_get_name (WpCollection * self); + +WP_API +gsize wp_collection_get_size (WpCollection * self); + +WP_API +gboolean wp_collection_contains_global (WpCollection * self, guint32 global_id); + +WP_API +void wp_collection_collect_global (WpCollection * self, guint32 global_id); + +WP_API +void wp_collection_drop_global (WpCollection * self, guint32 global_id); + +WP_API +void wp_collection_clear (WpCollection * self); + +WP_API +WpIterator * wp_collection_new_iterator (WpCollection * self); + + +/* WpImplCollection */ + +/*! + * \brief The WpImplCollection GType + * \ingroup wpcollection + */ +#define WP_TYPE_IMPL_COLLECTION (wp_impl_collection_get_type ()) + +WP_API +G_DECLARE_FINAL_TYPE (WpImplCollection, wp_impl_collection, WP, IMPL_COLLECTION, + WpProxy) + +WP_API +WpImplCollection * wp_impl_collection_new (WpCore * core, const gchar *name, + WpProperties *properties); + +WP_API +WpProperties * wp_impl_collection_get_properties (WpImplCollection *self); + +WP_API +const gchar * wp_impl_collection_get_name (WpImplCollection *self); + +WP_API +gsize wp_impl_collection_get_size (WpImplCollection * self); + +WP_API +gboolean wp_impl_collection_contains_global (WpImplCollection * self, + guint32 global_id); + +WP_API +void wp_impl_collection_collect_global (WpImplCollection * self, + guint32 global_id); + +WP_API +void wp_impl_collection_drop_global (WpImplCollection * self, + guint32 global_id); + +WP_API +void wp_impl_collection_clear (WpImplCollection * self); + +WP_API +WpIterator * wp_impl_collection_new_iterator (WpImplCollection * self); + +G_END_DECLS + +#endif diff --git a/lib/wp/meson.build b/lib/wp/meson.build index c3ff7473..4b0bb42f 100644 --- a/lib/wp/meson.build +++ b/lib/wp/meson.build @@ -4,6 +4,7 @@ wp_lib_sources = files( 'component-loader.c', 'conf.c', 'core.c', + 'collection.c', 'device.c', 'error.c', 'event.c', @@ -52,6 +53,7 @@ wp_lib_headers = files( 'component-loader.h', 'conf.h', 'core.h', + 'collection.h', 'defs.h', 'device.h', 'error.h', diff --git a/lib/wp/private/registry.c b/lib/wp/private/registry.c index 9ad99f41..8edd679e 100644 --- a/lib/wp/private/registry.c +++ b/lib/wp/private/registry.c @@ -6,9 +6,12 @@ * SPDX-License-Identifier: MIT */ +#include + #include "registry.h" #include "object-manager.h" #include "log.h" +#include "collection.h" WP_DEFINE_LOCAL_LOG_TOPIC ("wp-registry") @@ -73,16 +76,26 @@ object_manager_destroyed (gpointer data, GObject * om) /* find the subclass of WpPipewireGloabl that can handle the given pipewire interface type of the given version */ static inline GType -find_proxy_instance_type (const char * type, guint32 version) +find_proxy_instance_type (const char * type, guint32 version, + const struct spa_dict *props) { - g_autofree GType *children; + g_autofree GType *children = NULL; guint n_children; + /* Check if this is a collection */ + if (g_str_equal (type, PW_TYPE_INTERFACE_Metadata) && + version == PW_VERSION_METADATA && + props && spa_atob (spa_dict_lookup (props, "wireplumber.collection"))) { + return WP_TYPE_COLLECTION; + } + + /* Otherwise find the matching proxy non-collection type */ children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children); for (guint i = 0; i < n_children; i++) { WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]); - if (g_strcmp0 (klass->pw_iface_type, type) == 0 && + if (children[i] != WP_TYPE_COLLECTION && + g_strcmp0 (klass->pw_iface_type, type) == 0 && klass->pw_iface_version == version) { g_type_class_unref (klass); return children[i]; @@ -100,7 +113,7 @@ registry_global (void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { WpRegistry *self = data; - GType gtype = find_proxy_instance_type (type, version); + GType gtype = find_proxy_instance_type (type, version, props); wp_debug_object (wp_registry_get_core (self), "global:%u perm:0x%x type:%s/%u -> %s", diff --git a/lib/wp/wp.c b/lib/wp/wp.c index 924b8ab1..ec6c18be 100644 --- a/lib/wp/wp.c +++ b/lib/wp/wp.c @@ -52,6 +52,7 @@ wp_init (WpInitFlags flags) g_type_ensure (WP_TYPE_NODE); g_type_ensure (WP_TYPE_PORT); g_type_ensure (WP_TYPE_FACTORY); + g_type_ensure (WP_TYPE_COLLECTION); } /*! diff --git a/lib/wp/wp.h b/lib/wp/wp.h index 7ef4e685..37f6d81b 100644 --- a/lib/wp/wp.h +++ b/lib/wp/wp.h @@ -14,6 +14,7 @@ #include "component-loader.h" #include "conf.h" #include "core.h" +#include "collection.h" #include "device.h" #include "error.h" #include "event-dispatcher.h" diff --git a/tests/wp/collection.c b/tests/wp/collection.c new file mode 100644 index 00000000..d94fadbe --- /dev/null +++ b/tests/wp/collection.c @@ -0,0 +1,281 @@ +/* WirePlumber + * + * Copyright © 2026 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include "../common/base-test-fixture.h" + +typedef struct { + WpBaseTestFixture base; + + WpObjectManager *om; + WpCollection *collection; + + guint32 last_collected_id; + guint32 last_dropped_id; +} TestFixture; + +static void +test_collection_setup (TestFixture *self, gconstpointer user_data) +{ + wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE); + self->om = wp_object_manager_new (); +} + +static void +test_collection_teardown (TestFixture *self, gconstpointer user_data) +{ + g_clear_object (&self->om); + wp_base_test_fixture_teardown (&self->base); +} + +static void +on_collection_added (WpObjectManager *om, WpCollection *collection, + TestFixture *fixture) +{ + g_assert_true (WP_IS_COLLECTION (collection)); + + g_assert_null (fixture->collection); + fixture->collection = WP_COLLECTION (collection); + + g_main_loop_quit (fixture->base.loop); +} + +static void +on_collection_removed (WpObjectManager *om, WpCollection *collection, + TestFixture *fixture) +{ + g_assert_true (WP_IS_COLLECTION (collection)); + + g_assert_nonnull (fixture->collection); + fixture->collection = NULL; + + g_main_loop_quit (fixture->base.loop); +} + +static void +on_global_collected (WpCollection *om, guint32 global_id, TestFixture *fixture) +{ + fixture->last_collected_id = global_id; + g_main_loop_quit (fixture->base.loop); +} + +static void +on_global_dropped (WpCollection *om, guint32 global_id, TestFixture *fixture) +{ + fixture->last_dropped_id = global_id; + g_main_loop_quit (fixture->base.loop); +} + +static void +test_impl_collection_activated (WpObject * impl_collection, GAsyncResult * res, + TestFixture *fixture) +{ + g_autoptr (GError) error = NULL; + + g_assert_true (wp_object_activate_finish (impl_collection, res, &error)); + g_assert_no_error (error); + + g_assert_true (WP_IS_IMPL_COLLECTION (impl_collection)); + + g_main_loop_quit (fixture->base.loop); +} + +static void +test_collection_basic (TestFixture *fixture, gconstpointer data) +{ + g_autoptr (WpImplCollection) impl_collection = NULL; + + /* Install object manager on the client side */ + g_signal_connect (fixture->om, "object-added", + (GCallback) on_collection_added, fixture); + g_signal_connect (fixture->om, "object-removed", + (GCallback) on_collection_removed, fixture); + wp_object_manager_add_interest (fixture->om, WP_TYPE_COLLECTION, NULL); + wp_object_manager_request_object_features (fixture->om, WP_TYPE_COLLECTION, + WP_OBJECT_FEATURES_ALL); + wp_core_install_object_manager (fixture->base.client_core, fixture->om); + + /* Create the collection */ + impl_collection = wp_impl_collection_new (fixture->base.core, "my-collection", + NULL); + g_assert_nonnull (impl_collection); + + /* Export the collection */ + wp_object_activate (WP_OBJECT (impl_collection), WP_OBJECT_FEATURES_ALL, + NULL, (GAsyncReadyCallback) test_impl_collection_activated, fixture); + g_main_loop_run (fixture->base.loop); + + /* Run again so the collection is added in the client object manager */ + g_assert_null (fixture->collection); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (wp_object_manager_get_n_objects (fixture->om), ==, 1); + g_assert_nonnull (fixture->collection); + + /* Check name */ + { + const gchar *name = wp_collection_get_name (fixture->collection); + g_assert_cmpstr (name, ==, "my-collection"); + } + + /* Check properties */ + { + g_autoptr (WpProperties) props = NULL; + props = wp_global_proxy_get_global_properties ( + WP_GLOBAL_PROXY (fixture->collection)); + const gchar *str = wp_properties_get (props, "wireplumber.collection"); + g_assert_cmpstr (str, ==, "true"); + } + + /* Handle collection signals */ + g_signal_connect (fixture->collection, "global-collected", + (GCallback) on_global_collected, fixture); + g_signal_connect (fixture->collection, "global-dropped", + (GCallback) on_global_dropped, fixture); + + /* Make sure collection does not have any globals */ + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 0); + + /* Collect the first global */ + fixture->last_collected_id = 0; + wp_collection_collect_global (fixture->collection, 42); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (fixture->last_collected_id, ==, 42); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1); + g_assert_true (wp_collection_contains_global (fixture->collection, 42)); + g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1); + g_assert_true (wp_impl_collection_contains_global (impl_collection, 42)); + + /* Collect the second global */ + fixture->last_collected_id = 0; + wp_collection_collect_global (fixture->collection, 99); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (fixture->last_collected_id, ==, 99); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 2); + g_assert_true (wp_collection_contains_global (fixture->collection, 99)); + g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 2); + g_assert_true (wp_impl_collection_contains_global (impl_collection, 99)); + + /* Iterate the globals from the client side */ + { + g_autoptr (WpIterator) iter = NULL; + g_auto (GValue) val = G_VALUE_INIT; + gboolean has_first = FALSE; + gboolean has_second = FALSE; + guint count = 0; + + iter = wp_collection_new_iterator (fixture->collection); + g_assert_nonnull (iter); + + while (wp_iterator_next (iter, &val)) { + guint32 global_id = g_value_get_uint (&val); + if (global_id == 42) + has_first = TRUE; + if (global_id == 99) + has_second = TRUE; + count++; + g_value_unset (&val); + } + + g_assert_true (has_first); + g_assert_true (has_second); + g_assert_cmpuint (count, ==, 2); + } + + /* Iterate the globals from the impl side */ + { + g_autoptr (WpIterator) iter = NULL; + g_auto (GValue) val = G_VALUE_INIT; + gboolean has_first = FALSE; + gboolean has_second = FALSE; + guint count = 0; + + iter = wp_impl_collection_new_iterator (impl_collection); + g_assert_nonnull (iter); + + while (wp_iterator_next (iter, &val)) { + guint32 global_id = g_value_get_uint (&val); + if (global_id == 42) + has_first = TRUE; + if (global_id == 99) + has_second = TRUE; + count++; + g_value_unset (&val); + } + + g_assert_true (has_first); + g_assert_true (has_second); + g_assert_cmpuint (count, ==, 2); + } + + /* Drop the first global */ + fixture->last_dropped_id = 0; + wp_collection_drop_global (fixture->collection, 42); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (fixture->last_dropped_id, ==, 42); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1); + g_assert_false (wp_collection_contains_global (fixture->collection, 42)); + g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1); + g_assert_false (wp_impl_collection_contains_global (impl_collection, 42)); + + /* Collect an existing global and make sure nothing happens */ + fixture->last_collected_id = 0; + wp_collection_collect_global (fixture->collection, 99); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1); + g_assert_true (wp_collection_contains_global (fixture->collection, 99)); + g_assert_true (wp_impl_collection_contains_global (impl_collection, 99)); + + /* Drop an unexisting global and make sure nothing happens */ + fixture->last_dropped_id = 0; + wp_collection_drop_global (fixture->collection, 42); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1); + g_assert_false (wp_collection_contains_global (fixture->collection, 42)); + g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1); + g_assert_false (wp_impl_collection_contains_global (impl_collection, 42)); + + /* Drop the second global */ + fixture->last_dropped_id = 0; + wp_collection_drop_global (fixture->collection, 99); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (fixture->last_dropped_id, ==, 99); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 0); + g_assert_false (wp_collection_contains_global (fixture->collection, 99)); + g_assert_false (wp_impl_collection_contains_global (impl_collection, 99)); + + /* Collect a global from the impl side */ + fixture->last_collected_id = 0; + wp_impl_collection_collect_global (impl_collection, 36); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (fixture->last_collected_id, ==, 36); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1); + g_assert_true (wp_collection_contains_global (fixture->collection, 36)); + g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1); + g_assert_true (wp_impl_collection_contains_global (impl_collection, 36)); + + /* Drop the global from the impl size */ + fixture->last_dropped_id = 0; + wp_impl_collection_drop_global (impl_collection, 36); + g_main_loop_run (fixture->base.loop); + g_assert_cmpuint (fixture->last_dropped_id, ==, 36); + g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 0); + g_assert_false (wp_collection_contains_global (fixture->collection, 36)); + g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 0); + g_assert_false (wp_impl_collection_contains_global (impl_collection, 36)); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + wp_init (WP_INIT_ALL); + + g_test_add ("/wp/collection/basic", TestFixture, NULL, + test_collection_setup, test_collection_basic, test_collection_teardown); + + return g_test_run (); +} diff --git a/tests/wp/meson.build b/tests/wp/meson.build index cc0b9ac8..8c4ab9a6 100644 --- a/tests/wp/meson.build +++ b/tests/wp/meson.build @@ -3,6 +3,13 @@ common_env = common_test_env common_env.set('G_TEST_SRCDIR', meson.current_source_dir()) common_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) +test( + 'test-collection', + executable('test-collection', 'collection.c', + dependencies: common_deps), + env: common_env, +) + test( 'test-component-loader', executable('test-component-loader', 'component-loader.c',