diff --git a/lib/wp/private/internal-comp-loader.c b/lib/wp/private/internal-comp-loader.c index b6435335..0a997da6 100644 --- a/lib/wp/private/internal-comp-loader.c +++ b/lib/wp/private/internal-comp-loader.c @@ -780,6 +780,20 @@ load_settings_instance (GTask * task, WpCore * core, WpSpaJson * args) g_task_return_pointer (task, settings, g_object_unref); } +static void +load_collection_manager_instance (GTask * task, WpCore * core, WpSpaJson * args) +{ + g_autofree gchar *metadata_name = NULL; + if (args) + wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL); + + wp_info_object (core, "loading collection manager instance '%s'...", + metadata_name ? metadata_name : "(default: sm-collection-manager)"); + + WpCollectionManager *cm = wp_collection_manager_new (core, metadata_name); + g_task_return_pointer (task, cm, g_object_unref); +} + static const struct { const gchar * name; void (*load) (GTask *, WpCore *, WpSpaJson *); @@ -787,6 +801,7 @@ static const struct { { "ensure-no-media-session", ensure_no_media_session }, { "export-core", load_export_core }, { "settings-instance", load_settings_instance }, + { "collection-manager-instance", load_collection_manager_instance }, }; /*** WpInternalCompLoader ***/ diff --git a/modules/meson.build b/modules/meson.build index 7308aa4e..3203f641 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -191,3 +191,13 @@ shared_library( install_dir : wireplumber_module_dir, dependencies : [wp_dep], ) + +shared_library( + 'wireplumber-module-collection-manager', + [ + 'module-collection-manager.c', + ], + install : true, + install_dir : wireplumber_module_dir, + dependencies : [wp_dep, pipewire_dep], +) diff --git a/modules/module-collection-manager.c b/modules/module-collection-manager.c new file mode 100644 index 00000000..1326b16f --- /dev/null +++ b/modules/module-collection-manager.c @@ -0,0 +1,217 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +WP_DEFINE_LOCAL_LOG_TOPIC ("m-collection-manager") + +/* + * This module creates the "sm-collection-manager" metadata and also each + * individual collection metadata. + */ + +struct _WpCollectionManagerPlugin +{ + WpPlugin parent; + + /* Props */ + gchar *metadata_name; + + WpImplMetadata *impl_metadata; + GHashTable *collection_metadatas; +}; + +enum { + PROP_0, + PROP_METADATA_NAME, +}; + +G_DECLARE_FINAL_TYPE (WpCollectionManagerPlugin, wp_collection_manager_plugin, + WP, COLLECTION_MANAGER_PLUGIN, WpPlugin) +G_DEFINE_TYPE (WpCollectionManagerPlugin, wp_collection_manager_plugin, + WP_TYPE_PLUGIN) + +static void +wp_collection_manager_plugin_init (WpCollectionManagerPlugin * self) +{ +} + +static void +on_collection_metadata_activated (GObject * o, GAsyncResult * res, gpointer d) +{ + WpCollectionManagerPlugin *self = WP_COLLECTION_MANAGER_PLUGIN (d); + g_autoptr (WpImplMetadata) m = WP_IMPL_METADATA (o); + g_autoptr (WpProperties) props = NULL; + const gchar *name = NULL; + g_autoptr (GError) error = NULL; + + /* Get the metadata name */ + props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (m)); + g_return_if_fail (props); + name = wp_properties_get (props, "metadata.name"); + g_return_if_fail (name); + + /* make sure activation was successful */ + if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) { + wp_warning_object (self, "Failed to activate collection metadata: %s", + name); + return; + } + + /* Add the metadata */ + g_hash_table_insert (self->collection_metadatas, g_strdup (name), + g_object_ref (m)); + wp_info_object (self, "Added collection metadata: %s", name); +} + +static void +on_metadata_changed (WpMetadata *m, guint32 subject, const gchar *key, + const gchar *type, const gchar *value, gpointer d) +{ + WpCollectionManagerPlugin *self = WP_COLLECTION_MANAGER_PLUGIN (d); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + if (key) { + WpImplMetadata *collection_m = g_hash_table_lookup ( + self->collection_metadatas, key); + if (collection_m && !value) { + /* Remove collection metadata */ + g_hash_table_remove (self->collection_metadatas, key); + wp_info_object (self, "Removed collection metadata: %s", key); + } else if (!collection_m && value) { + /* Create new collection metadata */ + wp_info_object (self, "Creating collection metadata: %s", key); + WpImplMetadata *new_collection_m = wp_impl_metadata_new_full (core, key, + wp_properties_new ("wireplumber.collection", "true", NULL)); + wp_object_activate (WP_OBJECT (new_collection_m), WP_OBJECT_FEATURES_ALL, + NULL, on_collection_metadata_activated, self); + } + } else { + /* Remove all collection metadatas */ + g_hash_table_remove_all (self->collection_metadatas); + wp_info_object (self, "Removed all collection metadatas"); + } +} + +static void +on_metadata_activated (WpMetadata * m, GAsyncResult * res, + gpointer user_data) +{ + WpTransition *trans = WP_TRANSITION (user_data); + WpCollectionManagerPlugin *self = wp_transition_get_source_object (trans); + g_autoptr (GError) error = NULL; + + if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) { + wp_transition_return_error (trans, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_OPERATION_FAILED, + "Failed to activate metadata object %s", self->metadata_name)); + return; + } + + /* monitor changes in metadata */ + g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed), self, + 0); + + wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); +} + +static void +wp_collection_manager_plugin_enable (WpPlugin * plugin, + WpTransition * transition) +{ + WpCollectionManagerPlugin * self = WP_COLLECTION_MANAGER_PLUGIN (plugin); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin)); + + /* Create the collection metadatas table */ + self->collection_metadatas = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + + /* Create the collection manager metadata */ + self->impl_metadata = wp_impl_metadata_new_full (core, self->metadata_name, + wp_properties_new ("wireplumber.collection-manager", "true", NULL)); + wp_object_activate (WP_OBJECT (self->impl_metadata), WP_OBJECT_FEATURES_ALL, + NULL, + (GAsyncReadyCallback)on_metadata_activated, transition); +} + +static void +wp_collection_manager_plugin_disable (WpPlugin * plugin) +{ + WpCollectionManagerPlugin * self = WP_COLLECTION_MANAGER_PLUGIN (plugin); + + g_clear_object (&self->impl_metadata); + g_clear_pointer (&self->collection_metadatas, g_hash_table_unref); + + g_clear_pointer (&self->metadata_name, g_free); +} + +static void +wp_collection_manager_plugin_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpCollectionManagerPlugin *self = WP_COLLECTION_MANAGER_PLUGIN (object); + + switch (property_id) { + case PROP_METADATA_NAME: + g_clear_pointer (&self->metadata_name, g_free); + self->metadata_name = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_collection_manager_plugin_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpCollectionManagerPlugin *self = WP_COLLECTION_MANAGER_PLUGIN (object); + + switch (property_id) { + case PROP_METADATA_NAME: + g_value_set_string (value, self->metadata_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_collection_manager_plugin_class_init (WpCollectionManagerPluginClass * klass) +{ + WpPluginClass *plugin_class = (WpPluginClass *) klass; + GObjectClass *object_class = (GObjectClass *) klass; + + plugin_class->enable = wp_collection_manager_plugin_enable; + plugin_class->disable = wp_collection_manager_plugin_disable; + + object_class->set_property = wp_collection_manager_plugin_set_property; + object_class->get_property = wp_collection_manager_plugin_get_property; + + g_object_class_install_property (object_class, PROP_METADATA_NAME, + g_param_spec_string ("metadata-name", "metadata-name", + "The metadata object to look after", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} + +WP_PLUGIN_EXPORT GObject * +wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error) +{ + g_autofree gchar *metadata_name = NULL; + if (args) + wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL); + + return G_OBJECT (g_object_new (wp_collection_manager_plugin_get_type (), + "name", "collection-manager", + "core", core, + "metadata-name", metadata_name ? metadata_name : "sm-collection-manager", + NULL)); +} diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index ee1aa1d6..ac3d422b 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -68,6 +68,7 @@ wireplumber.profiles = { inherits = [ base ] metadata.sm-settings = required + metadata.sm-collection-manager = required metadata.sm-objects = required policy.standard = required @@ -101,6 +102,7 @@ wireplumber.profiles = { policy = { inherits = [ base ] metadata.sm-settings = required + metadata.sm-collection-manager = required metadata.sm-objects = required policy.standard = required } @@ -128,6 +130,7 @@ wireplumber.profiles = { base = { check.no-media-session = required support.settings = required + support.collection-manager = required support.log-settings = required support.session-services = required } @@ -233,6 +236,13 @@ wireplumber.components = [ provides = metadata.sm-settings } + ## Provides the "sm-collection-manager" metadata object + { + name = libwireplumber-module-collection-manager, type = module + arguments = { metadata.name = sm-collection-manager } + provides = metadata.sm-collection-manager + } + ## Activates a global WpSettings instance, providing settings from ## the sm-settings metadata object. Note that this blocks and waits for the ## sm-settings metadata object to become available, so one instance must @@ -244,6 +254,18 @@ wireplumber.components = [ after = [ metadata.sm-settings ] } + ## Activates a global WpCollectionManager instance, allowing creating + ## collections for global objects. Note that this blocks and waits for the + ## sm-collection-manager metadata object to become available, so one instance + ## must provide that, while others should only load this to access the + ## collection manager. + { + name = collection-manager-instance, type = built-in + arguments = { metadata.name = sm-collection-manager } + provides = support.collection-manager + after = [ metadata.sm-collection-manager ] + } + ## Log level settings { name = libwireplumber-module-log-settings, type = module