mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-02-04 14:20:27 +01:00
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.
462 lines
13 KiB
C
462 lines
13 KiB
C
/* 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);
|
|
}
|