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.
This commit is contained in:
Julian Bouzas 2025-12-04 10:53:22 -05:00
parent 11af177902
commit c3ebd79f00
5 changed files with 541 additions and 0 deletions

462
lib/wp/collection.c Normal file
View file

@ -0,0 +1,462 @@
/* WirePlumber
*
* Copyright © 2025 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <pipewire/pipewire.h>
#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);
}

49
lib/wp/collection.h Normal file
View file

@ -0,0 +1,49 @@
/* WirePlumber
*
* Copyright © 2025 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@ollabora.com>
*
* 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

View file

@ -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',

View file

@ -0,0 +1,27 @@
/* WirePlumber
*
* Copyright © 2025 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* 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

View file

@ -47,6 +47,7 @@
#include "wpversion.h"
#include "factory.h"
#include "settings.h"
#include "collection.h"
G_BEGIN_DECLS