/* 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); }