mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-09 05:08:04 +02:00
Merge branch 'collection' into 'master'
Add new collections API and a configuration to create ALSA loopbacks See merge request pipewire/wireplumber!779
This commit is contained in:
commit
7ecd6b553d
40 changed files with 3683 additions and 74 deletions
907
lib/wp/collection-manager.c
Normal file
907
lib/wp/collection-manager.c
Normal file
|
|
@ -0,0 +1,907 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2025 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#include "error.h"
|
||||
#include "core.h"
|
||||
#include "collection-manager.h"
|
||||
#include "metadata.h"
|
||||
#include "log.h"
|
||||
#include "object-manager.h"
|
||||
|
||||
#include "private/collection.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-collection-manager")
|
||||
|
||||
/*! \defgroup wpcollectionmanager WpCollectionManager */
|
||||
/*!
|
||||
* \struct WpCollectionManager
|
||||
*
|
||||
* The WpCollectionManager class is the object charged to create and destroy
|
||||
* the collections in WirePlumber.
|
||||
*
|
||||
* WpCollection API.
|
||||
*/
|
||||
|
||||
struct _WpCollectionManager
|
||||
{
|
||||
WpObject parent;
|
||||
|
||||
/* Props */
|
||||
gchar *metadata_name;
|
||||
|
||||
/* Metadata */
|
||||
WpObjectManager *metadata_om;
|
||||
GWeakRef metadata;
|
||||
|
||||
/* Collections info */
|
||||
guint pending_collections;
|
||||
guint failed_collections;
|
||||
GHashTable *collections;
|
||||
GHashTable *create_collection_tasks;
|
||||
|
||||
/* Globals info */
|
||||
WpObjectManager *global_proxy_om;
|
||||
GHashTable *collected_globals;
|
||||
};
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_METADATA_NAME,
|
||||
};
|
||||
|
||||
enum {
|
||||
SIGNAL_GLOBAL_COLLECTED,
|
||||
SIGNAL_GLOBAL_DROPPED,
|
||||
LAST_SIGNAL,
|
||||
};
|
||||
|
||||
static guint signals[LAST_SIGNAL] = { 0 };
|
||||
|
||||
G_DEFINE_TYPE (WpCollectionManager, wp_collection_manager, WP_TYPE_OBJECT)
|
||||
|
||||
static void
|
||||
wp_collection_manager_init (WpCollectionManager * self)
|
||||
{
|
||||
self->collections = g_hash_table_new_full (g_str_hash, g_str_equal,
|
||||
g_free, g_object_unref);
|
||||
self->create_collection_tasks = g_hash_table_new_full (g_str_hash,
|
||||
g_str_equal, g_free, g_object_unref);
|
||||
self->collected_globals = g_hash_table_new_full (g_direct_hash,
|
||||
g_direct_equal, NULL, g_free);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_manager_set_property (GObject * object, guint property_id,
|
||||
const GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
WpCollectionManager *self = WP_COLLECTION_MANAGER (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_get_property (GObject * object, guint property_id,
|
||||
GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
WpCollectionManager *self = WP_COLLECTION_MANAGER (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;
|
||||
}
|
||||
}
|
||||
|
||||
enum {
|
||||
STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
|
||||
};
|
||||
|
||||
static WpObjectFeatures
|
||||
wp_collection_manager_get_supported_features (WpObject * self)
|
||||
{
|
||||
return WP_COLLECTION_MANAGER_LOADED;
|
||||
}
|
||||
|
||||
static guint
|
||||
wp_collection_manager_activate_get_next_step (WpObject * self,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
g_return_val_if_fail (missing == WP_COLLECTION_MANAGER_LOADED,
|
||||
WP_TRANSITION_STEP_ERROR);
|
||||
|
||||
return STEP_LOAD;
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_proxy_removed (WpObjectManager *om, WpGlobalProxy *global, gpointer d)
|
||||
{
|
||||
WpCollectionManager * self = WP_COLLECTION_MANAGER (d);
|
||||
|
||||
/* Drop global from the collection if global is removed */
|
||||
wp_collection_manager_drop_global (self, global);
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_activated (GObject * o, GAsyncResult *res,
|
||||
WpTransition * transition)
|
||||
{
|
||||
WpCollectionManager * self = wp_transition_get_source_object (transition);
|
||||
g_autoptr (WpCollection) collection = WP_COLLECTION (o);
|
||||
g_autoptr (GError) error = NULL;
|
||||
const gchar *name;
|
||||
|
||||
/* Get the task name */
|
||||
name = wp_collection_get_name (collection);
|
||||
g_return_if_fail (name);
|
||||
|
||||
/* Add collection to hash table if it was activated successfully */
|
||||
if (!wp_object_activate_finish (WP_OBJECT (collection), res, &error)) {
|
||||
self->failed_collections++;
|
||||
wp_warning_object (self, "Failed to activate collection '%s': %s", name,
|
||||
error->message);
|
||||
} else {
|
||||
g_autoptr (WpMetadata) m = wp_collection_get_metadata (collection);
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
/* Collect the globals */
|
||||
it = wp_metadata_new_iterator (m, PW_ID_ANY);
|
||||
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);
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
g_autoptr (WpGlobalProxy) global = wp_object_manager_lookup (
|
||||
self->global_proxy_om, WP_TYPE_GLOBAL_PROXY,
|
||||
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", global_id, NULL);
|
||||
if (global)
|
||||
g_hash_table_insert (self->collected_globals, global, g_strdup (name));
|
||||
else
|
||||
wp_warning_object (self,
|
||||
"Could not find global with Id %u for collection %s. Not adding...",
|
||||
global_id, name);
|
||||
}
|
||||
|
||||
/* Add the collection */
|
||||
g_hash_table_insert (self->collections, g_strdup (name),
|
||||
g_object_ref (collection));
|
||||
wp_info_object (self, "Added collection '%s' successfully", name);
|
||||
}
|
||||
|
||||
g_return_if_fail (self->pending_collections > 0);
|
||||
self->pending_collections--;
|
||||
|
||||
/* Set loaded feature if no pending collections */
|
||||
if (self->pending_collections == 0) {
|
||||
/* Check for errors */
|
||||
if (self->failed_collections > 0) {
|
||||
wp_transition_return_error (transition, g_error_new (
|
||||
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
||||
"%d collections failed to activate",
|
||||
self->failed_collections));
|
||||
} else {
|
||||
wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_MANAGER_LOADED, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_ready (GObject * o, GAsyncResult *res, gpointer d)
|
||||
{
|
||||
WpCollectionManager * self = WP_COLLECTION_MANAGER (d);
|
||||
g_autoptr (WpCollection) collection = WP_COLLECTION (o);
|
||||
g_autoptr (GError) error = NULL;
|
||||
g_autoptr (GTask) t = NULL;
|
||||
g_autofree gchar *k = NULL;
|
||||
const gchar *name;
|
||||
|
||||
/* Get the task name */
|
||||
name = wp_collection_get_name (collection);
|
||||
g_return_if_fail (name);
|
||||
|
||||
/* Get the pending task if any */
|
||||
g_hash_table_steal_extended (self->create_collection_tasks, name,
|
||||
(gpointer *)&k, (gpointer *)&t);
|
||||
|
||||
/* Add collection to hash table if it was activated successfully */
|
||||
name = wp_collection_get_name (collection);
|
||||
if (!wp_object_activate_finish (WP_OBJECT (collection), res, &error)) {
|
||||
wp_warning_object (self, "Failed to activate collection '%s': %s", name,
|
||||
error->message);
|
||||
if (t)
|
||||
g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
|
||||
"Could not add collection '%s' as its activation failed: %s", name,
|
||||
error->message);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Add collection to the hash table */
|
||||
g_hash_table_insert (self->collections, g_strdup (name),
|
||||
g_object_ref (collection));
|
||||
wp_info_object (self, "Added collection '%s' successfully", name);
|
||||
|
||||
/* Notify pending task */
|
||||
if (t)
|
||||
g_task_return_pointer (t, collection, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
on_metadata_changed (WpMetadata *m, guint32 subject,
|
||||
const gchar *key, const gchar *type, const gchar *value, gpointer d)
|
||||
{
|
||||
WpCollectionManager *self = WP_COLLECTION_MANAGER (d);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
|
||||
if (key) {
|
||||
if (value) {
|
||||
WpCollection *collection;
|
||||
wp_info_object (self, "Adding collection '%s'", key);
|
||||
collection = wp_collection_new (core, key);
|
||||
wp_object_activate (WP_OBJECT (collection), WP_OBJECT_FEATURES_ALL, NULL,
|
||||
on_collection_ready, self);
|
||||
} else {
|
||||
g_hash_table_remove (self->collections, key);
|
||||
wp_info_object (self, "Removed collection '%s'", key);
|
||||
}
|
||||
} else {
|
||||
g_hash_table_remove_all (self->collections);
|
||||
wp_info_object (self, "Removed all collections");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_metadata_added (WpObjectManager *om, WpMetadata *m, gpointer d)
|
||||
{
|
||||
WpTransition * transition = WP_TRANSITION (d);
|
||||
WpCollectionManager * self = wp_transition_get_source_object (transition);
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
const gchar *metadata_name = NULL;
|
||||
g_autoptr (WpCore) core = 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->metadata_name));
|
||||
|
||||
/* Set the metadata weak ref */
|
||||
g_weak_ref_set (&self->metadata, m);
|
||||
|
||||
/* Listen for metadata changes */
|
||||
g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed),
|
||||
self, 0);
|
||||
|
||||
/* Check if there are collections, and activate them if so */
|
||||
self->pending_collections = 0;
|
||||
self->failed_collections = 0;
|
||||
core = wp_object_get_core (WP_OBJECT (self));
|
||||
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);
|
||||
WpCollection *collection = wp_collection_new (core, key);
|
||||
self->pending_collections++;
|
||||
wp_object_activate_closure (WP_OBJECT (collection),
|
||||
WP_OBJECT_FEATURES_ALL, NULL,
|
||||
g_cclosure_new_object (
|
||||
(GCallback) on_collection_activated, G_OBJECT (transition)));
|
||||
}
|
||||
|
||||
/* Otherwise just set loaded feature */
|
||||
if (self->pending_collections == 0)
|
||||
wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_MANAGER_LOADED, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_proxy_om_installed (WpObjectManager *om, gpointer d)
|
||||
{
|
||||
WpTransition * transition = WP_TRANSITION (d);
|
||||
WpCollectionManager * self = wp_transition_get_source_object (transition);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
|
||||
/* Install metadata object manager */
|
||||
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->metadata_name,
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
|
||||
"wireplumber.collection-manager", "=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->metadata_name);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_manager_activate_execute_step (WpObject * object,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
WpCollectionManager *self = WP_COLLECTION_MANAGER (object);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (object);
|
||||
|
||||
switch (step) {
|
||||
case STEP_LOAD: {
|
||||
/* Install global proxy object manager */
|
||||
g_clear_object (&self->global_proxy_om);
|
||||
self->global_proxy_om = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->global_proxy_om,
|
||||
WP_TYPE_GLOBAL_PROXY, NULL);
|
||||
wp_object_manager_request_object_features (self->global_proxy_om,
|
||||
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
|
||||
g_signal_connect_object (self->global_proxy_om, "object-removed",
|
||||
G_CALLBACK (on_global_proxy_removed), self, 0);
|
||||
g_signal_connect_object (self->global_proxy_om, "installed",
|
||||
G_CALLBACK (on_global_proxy_om_installed), transition, 0);
|
||||
wp_core_install_object_manager (core, self->global_proxy_om);
|
||||
break;
|
||||
}
|
||||
case WP_TRANSITION_STEP_ERROR:
|
||||
break;
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
finish_task (gpointer key, gpointer value, gpointer user_data)
|
||||
{
|
||||
const char *collection_name = key;
|
||||
GTask *t = G_TASK (value);
|
||||
g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
|
||||
"Deactivated collection manager before collection '%s' was created",
|
||||
collection_name);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_manager_deactivate (WpObject * object, WpObjectFeatures features)
|
||||
{
|
||||
WpCollectionManager *self = WP_COLLECTION_MANAGER (object);
|
||||
|
||||
/* Globals info */
|
||||
g_hash_table_remove_all (self->collected_globals);
|
||||
g_clear_object (&self->global_proxy_om);
|
||||
|
||||
/* Collections info */
|
||||
g_hash_table_foreach (self->create_collection_tasks, finish_task, NULL);
|
||||
g_hash_table_remove_all (self->create_collection_tasks);
|
||||
g_hash_table_remove_all (self->collections);
|
||||
|
||||
/* Metadata */
|
||||
g_weak_ref_set (&self->metadata, NULL);
|
||||
g_clear_object (&self->metadata_om);
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_manager_finalize (GObject * object)
|
||||
{
|
||||
WpCollectionManager *self = WP_COLLECTION_MANAGER (object);
|
||||
|
||||
/* Globals info */
|
||||
g_clear_pointer (&self->collected_globals, g_hash_table_unref);
|
||||
g_clear_object (&self->global_proxy_om);
|
||||
|
||||
/* Collections info */
|
||||
g_hash_table_foreach (self->create_collection_tasks, finish_task, NULL);
|
||||
g_clear_pointer (&self->create_collection_tasks, g_hash_table_unref);
|
||||
g_clear_pointer (&self->collections, g_hash_table_unref);
|
||||
|
||||
/* Metadata */
|
||||
g_weak_ref_clear (&self->metadata);
|
||||
g_clear_object (&self->metadata_om);
|
||||
|
||||
/* Props */
|
||||
g_clear_pointer (&self->metadata_name, g_free);
|
||||
|
||||
G_OBJECT_CLASS (wp_collection_manager_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_manager_class_init (WpCollectionManagerClass * klass)
|
||||
{
|
||||
GObjectClass * object_class = (GObjectClass *) klass;
|
||||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
||||
|
||||
object_class->finalize = wp_collection_manager_finalize;
|
||||
object_class->set_property = wp_collection_manager_set_property;
|
||||
object_class->get_property = wp_collection_manager_get_property;
|
||||
|
||||
wpobject_class->get_supported_features =
|
||||
wp_collection_manager_get_supported_features;
|
||||
wpobject_class->activate_get_next_step =
|
||||
wp_collection_manager_activate_get_next_step;
|
||||
wpobject_class->activate_execute_step =
|
||||
wp_collection_manager_activate_execute_step;
|
||||
wpobject_class->deactivate = wp_collection_manager_deactivate;
|
||||
|
||||
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));
|
||||
|
||||
signals[SIGNAL_GLOBAL_COLLECTED] = g_signal_new (
|
||||
"global-collected", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
|
||||
0, NULL, NULL, NULL, G_TYPE_NONE, 2, WP_TYPE_GLOBAL_PROXY, G_TYPE_STRING);
|
||||
|
||||
signals[SIGNAL_GLOBAL_DROPPED] = g_signal_new (
|
||||
"global-dropped", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
|
||||
0, NULL, NULL, NULL, G_TYPE_NONE, 2, WP_TYPE_GLOBAL_PROXY, G_TYPE_STRING);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Creates a new WpCollectionManager object
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param core the WpCore
|
||||
* \param metadata_name (nullable): the name of the metadata object to
|
||||
* associate with the collection manager object; NULL means the default
|
||||
* "sm-collection-manager"
|
||||
* \returns (transfer full): a new WpCollectionManager object
|
||||
*/
|
||||
WpCollectionManager *
|
||||
wp_collection_manager_new (WpCore * core, const gchar * metadata_name)
|
||||
{
|
||||
return g_object_new (WP_TYPE_COLLECTION_MANAGER,
|
||||
"core", core,
|
||||
"metadata-name", metadata_name ? metadata_name : "sm-collection-manager",
|
||||
NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
find_collection_manager_func (gpointer g_object, gpointer metadata_name)
|
||||
{
|
||||
if (!WP_IS_COLLECTION_MANAGER (g_object))
|
||||
return FALSE;
|
||||
|
||||
if (metadata_name)
|
||||
return g_str_equal (((WpCollectionManager *) g_object)->metadata_name,
|
||||
(gchar *) metadata_name);
|
||||
else
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finds a registered WpCollectionManager object by its metadata name
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param core the WpCore
|
||||
* \param metadata_name (nullable): the name of the metadata object that the
|
||||
* collection manager object is associated with; NULL returns the first
|
||||
* collection manager object that is found
|
||||
* \returns (transfer full) (nullable): the WpCollectionManager object, or NULL
|
||||
* if not found
|
||||
*/
|
||||
WpCollectionManager *
|
||||
wp_collection_manager_find (WpCore * core, const gchar *metadata_name)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_CORE (core), NULL);
|
||||
|
||||
GObject *cm = wp_core_find_object (core,
|
||||
(GEqualFunc) find_collection_manager_func, metadata_name);
|
||||
return cm ? WP_COLLECTION_MANAGER (cm) : NULL;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Callback version of wp_collection_manager_create_collection_closure()
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param collection_name the new collection name to create
|
||||
* \param cancellable (nullable): a cancellable for the async operation
|
||||
* \param callback (scope async)(closure user_data): a function to call when
|
||||
* the collection was created
|
||||
* \param user_data data for \a callback
|
||||
*/
|
||||
void
|
||||
wp_collection_manager_create_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name, GCancellable * cancellable,
|
||||
GAsyncReadyCallback callback, gpointer user_data)
|
||||
{
|
||||
g_return_if_fail (WP_IS_COLLECTION_MANAGER (self));
|
||||
|
||||
GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);
|
||||
|
||||
wp_collection_manager_create_collection_closure (self, collection_name,
|
||||
cancellable, closure);
|
||||
}
|
||||
|
||||
static void
|
||||
invoke_closure (GObject * obj, GAsyncResult * res, gpointer data)
|
||||
{
|
||||
GClosure *closure = data;
|
||||
GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };
|
||||
g_value_init (&values[0], G_TYPE_OBJECT);
|
||||
g_value_init (&values[1], G_TYPE_OBJECT);
|
||||
g_value_set_object (&values[0], obj);
|
||||
g_value_set_object (&values[1], res);
|
||||
g_closure_invoke (closure, NULL, 2, values, NULL);
|
||||
g_value_unset (&values[0]);
|
||||
g_value_unset (&values[1]);
|
||||
g_closure_unref (closure);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Creates a new collection into the manager
|
||||
*
|
||||
* \note \a closure may be invoked in sync while this method is being called,
|
||||
* if the collection already exists.
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param collection_name the new collection name to create
|
||||
* \param cancellable (nullable): a cancellable for the async operation
|
||||
* \param closure (transfer full): the closure to use when the collection was
|
||||
* created
|
||||
*/
|
||||
void
|
||||
wp_collection_manager_create_collection_closure (WpCollectionManager *self,
|
||||
const gchar *collection_name, GCancellable * cancellable, GClosure *closure)
|
||||
{
|
||||
g_autoptr (GTask) t = NULL;
|
||||
g_autoptr (WpMetadata) m = NULL;
|
||||
WpCollection *c = NULL;
|
||||
g_autoptr (WpSpaJson) json_value = NULL;
|
||||
|
||||
g_return_if_fail (WP_IS_COLLECTION_MANAGER (self));
|
||||
|
||||
/* Create the task from the closure */
|
||||
if (G_CLOSURE_NEEDS_MARSHAL (closure))
|
||||
g_closure_set_marshal (closure, g_cclosure_marshal_VOID__OBJECT);
|
||||
t = g_task_new (self, cancellable, invoke_closure, closure);
|
||||
|
||||
/* Just return if the collection already exists */
|
||||
c = g_hash_table_lookup (self->collections, collection_name);
|
||||
if (c) {
|
||||
g_task_return_pointer (t, c, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Make sure the new collection is not being created already */
|
||||
if (g_hash_table_contains (self->create_collection_tasks, collection_name)) {
|
||||
g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
|
||||
"Cannot create collection '%s' as another task is already creating it",
|
||||
collection_name);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get the metadata */
|
||||
m = g_weak_ref_get (&self->metadata);
|
||||
if (!m) {
|
||||
g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
|
||||
"Collection manager metadata not found, cannot create collection '%s'",
|
||||
collection_name);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Request new collection */
|
||||
g_hash_table_insert (self->create_collection_tasks,
|
||||
g_strdup (collection_name), g_object_ref (t));
|
||||
json_value = wp_spa_json_new_boolean (TRUE);
|
||||
wp_metadata_set (m, 0, collection_name, "Spa:String:JSON",
|
||||
wp_spa_json_get_data (json_value));
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finishes the async operation that was started with
|
||||
* wp_collection_manager_create_collection()
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param res the async operation result
|
||||
* \param error (out) (optional): the error of the operation, if any
|
||||
* \returns (nullable) (transfer none): The newly created WpCollection, or NULL
|
||||
* if an error happened.
|
||||
*/
|
||||
WpCollection *
|
||||
wp_collection_manager_create_collection_finish (WpCollectionManager * self,
|
||||
GAsyncResult * res, GError ** error)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE);
|
||||
g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
|
||||
|
||||
return g_task_propagate_pointer (G_TASK (res), error);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys a collection from the manager
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param collection_name the collection name to destroy
|
||||
* \returns TRUE if the collection was destroyed, FALSE otherwise
|
||||
*/
|
||||
gboolean
|
||||
wp_collection_manager_destroy_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name)
|
||||
{
|
||||
g_autoptr (WpMetadata) m = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE);
|
||||
|
||||
m = g_weak_ref_get (&self->metadata);
|
||||
if (!m)
|
||||
return FALSE;
|
||||
|
||||
g_hash_table_remove (self->collections, collection_name);
|
||||
wp_metadata_set (m, 0, collection_name, "Spa:String:JSON", NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks whether the collection manager has a collection or not
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param collection_name the collection name to check
|
||||
* \returns TRUE if the collection exists, FALSE otherwise
|
||||
*/
|
||||
gboolean
|
||||
wp_collection_manager_has_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE);
|
||||
|
||||
return g_hash_table_contains (self->collections, collection_name);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Get the collection for the given collection name
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param collection_name the collection name to get
|
||||
* \returns (nullable) (transfer full): the collection for the given name
|
||||
*/
|
||||
WpCollection *
|
||||
wp_collection_manager_get_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name)
|
||||
{
|
||||
WpCollection *c = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL);
|
||||
|
||||
c = g_hash_table_lookup (self->collections, collection_name);
|
||||
return c ? g_object_ref (c) : NULL;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the total number of collections the manager has
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \returns the total number of collections the manager has
|
||||
*/
|
||||
guint
|
||||
wp_collection_manager_get_collection_count (WpCollectionManager *self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), 0);
|
||||
|
||||
return g_hash_table_size (self->collections);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Iterates over all the collections that the manager has
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \returns (transfer full): an iterator that iterates over the collections
|
||||
*/
|
||||
WpIterator *
|
||||
wp_collection_manager_new_collection_iterator (WpCollectionManager *self)
|
||||
{
|
||||
GPtrArray *collections;
|
||||
GHashTableIter iter;
|
||||
WpCollection *c;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL);
|
||||
|
||||
collections = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
g_hash_table_iter_init (&iter, self->collections);
|
||||
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&c))
|
||||
g_ptr_array_add (collections, g_object_ref (c));
|
||||
|
||||
return wp_iterator_new_ptr_array (collections, WP_TYPE_COLLECTION);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Collects the bound global into a collection.
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param global (transfer none): the global to collect
|
||||
* \param collection_name: the name of the collection to collect the global into
|
||||
* \returns TRUE if the global could be collected, FALSE otherwise
|
||||
*/
|
||||
gboolean
|
||||
wp_collection_manager_collect_global (WpCollectionManager *self,
|
||||
WpGlobalProxy *global, const gchar *collection_name)
|
||||
{
|
||||
const gchar *cn;
|
||||
WpCollection *c;
|
||||
g_autoptr (WpMetadata) c_meta = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE);
|
||||
g_return_val_if_fail (wp_object_test_active_features (
|
||||
WP_OBJECT (global), WP_PROXY_FEATURE_BOUND), FALSE);
|
||||
g_return_val_if_fail (collection_name, FALSE);
|
||||
|
||||
/* Just return if this global is already collected */
|
||||
cn = g_hash_table_lookup (self->collected_globals, global);
|
||||
if (cn)
|
||||
return g_str_equal (cn, collection_name);
|
||||
|
||||
/* Get the collection */
|
||||
c = g_hash_table_lookup (self->collections, collection_name);
|
||||
if (!c)
|
||||
return FALSE;
|
||||
c_meta = wp_collection_get_metadata (c);
|
||||
g_return_val_if_fail (c_meta, FALSE);
|
||||
|
||||
/* Make sure we are not collecting a collection into itself */
|
||||
if (WP_GLOBAL_PROXY (c_meta) == global) {
|
||||
wp_warning_object (self, "Cannot collect a collection into itself");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!wp_collection_collect_global (c, global))
|
||||
return FALSE;
|
||||
|
||||
g_hash_table_insert (self->collected_globals, global,
|
||||
g_strdup (collection_name));
|
||||
|
||||
g_signal_emit (self, signals[SIGNAL_GLOBAL_COLLECTED], 0, global,
|
||||
collection_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Drops the bound global from its collection
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param global (transfer none): the global to drop
|
||||
* \returns TRUE if the global could be dropped, FALSE otherwise
|
||||
*/
|
||||
gboolean
|
||||
wp_collection_manager_drop_global (WpCollectionManager *self,
|
||||
WpGlobalProxy *global)
|
||||
{
|
||||
g_autofree gchar *cn = NULL;
|
||||
WpCollection *c;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE);
|
||||
g_return_val_if_fail (wp_object_test_active_features (
|
||||
WP_OBJECT (global), WP_PROXY_FEATURE_BOUND), FALSE);
|
||||
|
||||
/* Just return if this global is not collected */
|
||||
if (!g_hash_table_steal_extended (self->collected_globals, global, NULL,
|
||||
(gpointer *)&cn))
|
||||
return TRUE;
|
||||
|
||||
/* Get the collection */
|
||||
c = g_hash_table_lookup (self->collections, cn);
|
||||
g_return_val_if_fail (c, FALSE);
|
||||
|
||||
if (!wp_collection_drop_global (c, global))
|
||||
return FALSE;
|
||||
|
||||
/* Destroy the collection if it is empty */
|
||||
if (wp_collection_get_global_count (c) == 0)
|
||||
wp_collection_manager_destroy_collection (self, cn);
|
||||
|
||||
g_signal_emit (self, signals[SIGNAL_GLOBAL_DROPPED], 0, global, cn);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the collection that a given global belongs to
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param global (transfer none): the global to get the collection from
|
||||
* \returns (nullable) (transfer full): the collection that the given global
|
||||
* belongs too
|
||||
*/
|
||||
WpCollection *
|
||||
wp_collection_manager_get_global_collection (WpCollectionManager *self,
|
||||
WpGlobalProxy *global)
|
||||
{
|
||||
const gchar *cn;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL);
|
||||
|
||||
cn = g_hash_table_lookup (self->collected_globals, global);
|
||||
return cn ? wp_collection_manager_get_collection (self, cn) : NULL;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Convenience method to checks whether a global is collected into a
|
||||
* collection or not
|
||||
*
|
||||
* \ingroup wpcollectionmanager
|
||||
* \param self the WpCollectionManager
|
||||
* \param global (transfer none): the global to check if it is collected or not
|
||||
* \param collection_name (nullable): the collection name
|
||||
* \returns TRUE if the global is collected, FALSE otherwise
|
||||
* belongs too
|
||||
*/
|
||||
gboolean
|
||||
wp_collection_manager_is_global_collected (WpCollectionManager *self,
|
||||
WpGlobalProxy *global, const gchar *collection_name)
|
||||
{
|
||||
const gchar *cn;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), FALSE);
|
||||
|
||||
cn = g_hash_table_lookup (self->collected_globals, global);
|
||||
if (!cn)
|
||||
return FALSE;
|
||||
|
||||
return collection_name ? g_str_equal (cn, collection_name) : TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Iterates over all globals that a collection has
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the WpCollection
|
||||
* \param collection_name (nullable): the collection name
|
||||
* \returns (transfer full): an iterator that iterates over the globals.
|
||||
*/
|
||||
WpIterator *
|
||||
wp_collection_manager_new_global_iterator (WpCollectionManager *self,
|
||||
const gchar *collection_name)
|
||||
{
|
||||
GPtrArray *globals;
|
||||
GHashTableIter iter;
|
||||
WpGlobalProxy *g = NULL;
|
||||
const gchar *cn = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION_MANAGER (self), NULL);
|
||||
|
||||
globals = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
g_hash_table_iter_init (&iter, self->collected_globals);
|
||||
while (g_hash_table_iter_next (&iter, (gpointer *)&g, (gpointer *)&cn))
|
||||
if (!collection_name || g_str_equal (cn, collection_name))
|
||||
g_ptr_array_add (globals, g_object_ref (g));
|
||||
|
||||
return wp_iterator_new_ptr_array (globals, WP_TYPE_GLOBAL_PROXY);
|
||||
}
|
||||
105
lib/wp/collection-manager.h
Normal file
105
lib/wp/collection-manager.h
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2025 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@ollabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef __WIREPLUMBER_COLLECTION_MANAGER_H__
|
||||
#define __WIREPLUMBER_COLLECTION_MANAGER_H__
|
||||
|
||||
#include "object.h"
|
||||
#include "collection.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief Flags to be used as WpObjectFeatures for WpCollectionManager.
|
||||
* \ingroup wpcollectionmanager
|
||||
*/
|
||||
typedef enum { /*< flags >*/
|
||||
/*! Loads the collection manager */
|
||||
WP_COLLECTION_MANAGER_LOADED = (1 << 0),
|
||||
} WpCollectionManagerFeatures;
|
||||
|
||||
/*!
|
||||
* \brief The WpCollectionManager GType
|
||||
* \ingroup wpcollectionmanager
|
||||
*/
|
||||
#define WP_TYPE_COLLECTION_MANAGER (wp_collection_manager_get_type ())
|
||||
|
||||
WP_API
|
||||
G_DECLARE_FINAL_TYPE (WpCollectionManager, wp_collection_manager, WP,
|
||||
COLLECTION_MANAGER, WpObject)
|
||||
|
||||
WP_API
|
||||
WpCollectionManager * wp_collection_manager_new (WpCore * core,
|
||||
const gchar * metadata_name);
|
||||
|
||||
WP_API
|
||||
WpCollectionManager * wp_collection_manager_find (WpCore * core,
|
||||
const gchar *metadata_name);
|
||||
|
||||
|
||||
/* Collection */
|
||||
|
||||
WP_API
|
||||
void wp_collection_manager_create_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name, GCancellable * cancellable,
|
||||
GAsyncReadyCallback callback, gpointer user_data);
|
||||
|
||||
WP_API
|
||||
void wp_collection_manager_create_collection_closure (WpCollectionManager *self,
|
||||
const gchar *collection_name, GCancellable * cancellable,
|
||||
GClosure *closure);
|
||||
|
||||
WP_API
|
||||
WpCollection *wp_collection_manager_create_collection_finish (
|
||||
WpCollectionManager * self, GAsyncResult * res, GError ** error);
|
||||
|
||||
WP_API
|
||||
gboolean wp_collection_manager_destroy_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name);
|
||||
|
||||
WP_API
|
||||
gboolean wp_collection_manager_has_collection (WpCollectionManager *self,
|
||||
const gchar *collection_name);
|
||||
|
||||
WP_API
|
||||
WpCollection * wp_collection_manager_get_collection (
|
||||
WpCollectionManager *self, const gchar *collection_name);
|
||||
|
||||
WP_API
|
||||
guint wp_collection_manager_get_collection_count (WpCollectionManager *self);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_collection_manager_new_collection_iterator (
|
||||
WpCollectionManager *self);
|
||||
|
||||
|
||||
/* Global */
|
||||
|
||||
WP_API
|
||||
gboolean wp_collection_manager_collect_global (WpCollectionManager *self,
|
||||
WpGlobalProxy *global, const gchar *collection_name);
|
||||
|
||||
WP_API
|
||||
gboolean wp_collection_manager_drop_global (WpCollectionManager *self,
|
||||
WpGlobalProxy *global);
|
||||
|
||||
WP_API
|
||||
WpCollection * wp_collection_manager_get_global_collection (
|
||||
WpCollectionManager *self, WpGlobalProxy *global);
|
||||
|
||||
WP_API
|
||||
gboolean wp_collection_manager_is_global_collected (WpCollectionManager *self,
|
||||
WpGlobalProxy *global, const gchar *collection_name);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_collection_manager_new_global_iterator (
|
||||
WpCollectionManager *self, const gchar *collection_name);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
462
lib/wp/collection.c
Normal file
462
lib/wp/collection.c
Normal 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
49
lib/wp/collection.h
Normal 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
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
#include "event-hook.h"
|
||||
#include "log.h"
|
||||
#include "proxy.h"
|
||||
#include "collection-manager.h"
|
||||
|
||||
#include <spa/utils/defs.h>
|
||||
#include <spa/utils/list.h>
|
||||
|
|
@ -104,6 +105,23 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
|
|||
wp_properties_update (self->properties, subj_props);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add collection name */
|
||||
if (WP_IS_GLOBAL_PROXY (self->subject)) {
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self->subject));
|
||||
if (core) {
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
cm = wp_collection_manager_find (core, NULL);
|
||||
if (cm) {
|
||||
g_autoptr (WpCollection) c =
|
||||
wp_collection_manager_get_global_collection (cm,
|
||||
WP_GLOBAL_PROXY (self->subject));
|
||||
if (c)
|
||||
wp_properties_set (self->properties, "collection.name",
|
||||
wp_collection_get_name (c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_properties_set (self->properties, "event.type", type);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
wp_lib_sources = files(
|
||||
'base-dirs.c',
|
||||
'client.c',
|
||||
'collection.c',
|
||||
'collection-manager.c',
|
||||
'component-loader.c',
|
||||
'conf.c',
|
||||
'core.c',
|
||||
|
|
@ -48,6 +50,8 @@ wp_lib_priv_sources = files(
|
|||
wp_lib_headers = files(
|
||||
'base-dirs.h',
|
||||
'client.h',
|
||||
'collection.h',
|
||||
'collection-manager.h',
|
||||
'component-loader.h',
|
||||
'conf.h',
|
||||
'core.h',
|
||||
|
|
|
|||
27
lib/wp/private/collection.h
Normal file
27
lib/wp/private/collection.h
Normal 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
|
||||
|
|
@ -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 ***/
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@
|
|||
#include "wpversion.h"
|
||||
#include "factory.h"
|
||||
#include "settings.h"
|
||||
#include "collection.h"
|
||||
#include "collection-manager.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
)
|
||||
|
|
|
|||
217
modules/module-collection-manager.c
Normal file
217
modules/module-collection-manager.c
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2025 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <wp/wp.h>
|
||||
#include <spa/utils/defs.h>
|
||||
|
||||
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));
|
||||
}
|
||||
|
|
@ -2167,6 +2167,53 @@ static const luaL_Reg properties_funcs[] = {
|
|||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* WpCollection */
|
||||
|
||||
static int
|
||||
collection_get_name (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
lua_pushstring (L, wp_collection_get_name (c));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_get_metadata (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
WpMetadata *m = wp_collection_get_metadata (c);
|
||||
if (m)
|
||||
wplua_pushobject (L, m);
|
||||
else
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_contains_global (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
WpGlobalProxy *g = wplua_checkobject (L, 2, WP_TYPE_GLOBAL_PROXY);
|
||||
lua_pushboolean (L, wp_collection_contains_global (c, g));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_get_global_count (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
lua_pushinteger (L, wp_collection_get_global_count (c));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const luaL_Reg collection_funcs[] = {
|
||||
{ "get_name", collection_get_name },
|
||||
{ "get_metadata", collection_get_metadata },
|
||||
{ "contains_global", collection_contains_global },
|
||||
{ "get_global_count", collection_get_global_count },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* WpSettings */
|
||||
|
||||
static int
|
||||
|
|
@ -2617,6 +2664,252 @@ static const luaL_Reg event_dispatcher_funcs[] = {
|
|||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* WpCollectionManager */
|
||||
|
||||
static void
|
||||
on_collection_created (WpCollectionManager *cm, GAsyncResult * res,
|
||||
GClosure * closure)
|
||||
{
|
||||
g_autoptr (GError) error = NULL;
|
||||
WpCollection *c;
|
||||
GValue val[2] = { G_VALUE_INIT, G_VALUE_INIT };
|
||||
int n_vals = 1;
|
||||
|
||||
c = wp_collection_manager_create_collection_finish (cm, res, &error);
|
||||
if (!c) {
|
||||
g_value_init (&val[1], G_TYPE_STRING);
|
||||
g_value_set_string (&val[1], error->message);
|
||||
n_vals = 2;
|
||||
}
|
||||
|
||||
g_value_init (&val[0], WP_TYPE_COLLECTION);
|
||||
g_value_set_object (&val[0], c);
|
||||
g_closure_invoke (closure, NULL, n_vals, val, NULL);
|
||||
g_value_unset (&val[0]);
|
||||
g_value_unset (&val[1]);
|
||||
g_closure_invalidate (closure);
|
||||
g_closure_unref (closure);
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_create_collection (lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring (L, 1);
|
||||
GClosure * closure = wplua_function_to_closure (L, 2);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
GValue val[2] = { G_VALUE_INIT, G_VALUE_INIT };
|
||||
g_value_init (&val[0], WP_TYPE_METADATA);
|
||||
g_value_set_object (&val[0], NULL);
|
||||
g_value_init (&val[1], G_TYPE_STRING);
|
||||
g_value_set_string (&val[1], "Could not find collection manager");
|
||||
g_closure_invoke (closure, NULL, 2, val, NULL);
|
||||
g_value_unset (&val[0]);
|
||||
g_value_unset (&val[1]);
|
||||
g_closure_invalidate (closure);
|
||||
g_closure_unref (closure);
|
||||
return 0;
|
||||
}
|
||||
|
||||
wp_collection_manager_create_collection (cm, name, NULL,
|
||||
(GAsyncReadyCallback) on_collection_created, closure);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_destroy_collection (lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring (L, 1);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushboolean (L, FALSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushboolean (L, wp_collection_manager_destroy_collection (cm, name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_has_collection (lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring (L, 1);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushboolean (L, FALSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushboolean (L, wp_collection_manager_has_collection (cm, name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_get_collection (lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring (L, 1);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
WpCollection *c = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
c = wp_collection_manager_get_collection (cm, name);
|
||||
if (c)
|
||||
wplua_pushobject (L, c);
|
||||
else
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_get_collection_count (lua_State *L)
|
||||
{
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushinteger (L, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushinteger (L, wp_collection_manager_get_collection_count (cm));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_iterate_collections (lua_State *L)
|
||||
{
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
it = wp_collection_manager_new_collection_iterator (cm);
|
||||
return push_wpiterator (L, it);
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_collect_global (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy *global = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
const char *name = luaL_checkstring (L, 2);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushboolean (L, FALSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushboolean (L, wp_collection_manager_collect_global (cm, global, name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_drop_global (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy *global = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushboolean (L, FALSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushboolean (L, wp_collection_manager_drop_global (cm, global));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_get_global_collection (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy *global = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
WpCollection *c = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
c = wp_collection_manager_get_global_collection (cm, global);
|
||||
if (c)
|
||||
wplua_pushobject (L, c);
|
||||
else
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_is_global_collected (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy *global = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
const gchar *collection_name = NULL;
|
||||
if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
||||
collection_name = luaL_checkstring (L, 2);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushboolean (L, FALSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_pushboolean (L, wp_collection_manager_is_global_collected (cm, global,
|
||||
collection_name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_manager_iterate_globals (lua_State *L)
|
||||
{
|
||||
const gchar *collection_name = NULL;
|
||||
if (!lua_isnone (L, 1) && !lua_isnil (L, 1))
|
||||
collection_name = luaL_checkstring (L, 1);
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
|
||||
cm = wp_collection_manager_find (get_wp_core (L), NULL);
|
||||
if (!cm) {
|
||||
lua_pushnil (L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
it = wp_collection_manager_new_global_iterator (cm, collection_name);
|
||||
return push_wpiterator (L, it);
|
||||
}
|
||||
|
||||
static const luaL_Reg collection_manager_funcs[] = {
|
||||
{ "create_collection", collection_manager_create_collection },
|
||||
{ "destroy_collection", collection_manager_destroy_collection },
|
||||
{ "has_collection", collection_manager_has_collection },
|
||||
{ "get_collection", collection_manager_get_collection },
|
||||
{ "get_collection_count", collection_manager_get_collection_count },
|
||||
{ "iterate_collections", collection_manager_iterate_collections },
|
||||
{ "collect_global", collection_manager_collect_global },
|
||||
{ "drop_global", collection_manager_drop_global },
|
||||
{ "get_global_collection", collection_manager_get_global_collection },
|
||||
{ "is_global_collected", collection_manager_is_global_collected },
|
||||
{ "iterate_globals", collection_manager_iterate_globals },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* WpEventHook */
|
||||
|
||||
static int
|
||||
|
|
@ -3083,6 +3376,9 @@ wp_lua_scripting_api_init (lua_State *L)
|
|||
luaL_newlib (L, event_dispatcher_funcs);
|
||||
lua_setglobal (L, "WpEventDispatcher");
|
||||
|
||||
luaL_newlib (L, collection_manager_funcs);
|
||||
lua_setglobal (L, "WpCollectionManager");
|
||||
|
||||
wp_lua_scripting_pod_init (L);
|
||||
wp_lua_scripting_json_init (L);
|
||||
|
||||
|
|
@ -3144,6 +3440,8 @@ wp_lua_scripting_api_init (lua_State *L)
|
|||
NULL, iterator_funcs);
|
||||
wplua_register_type_methods (L, WP_TYPE_PROPERTIES,
|
||||
properties_new, properties_funcs);
|
||||
wplua_register_type_methods (L, WP_TYPE_COLLECTION,
|
||||
NULL, collection_funcs);
|
||||
|
||||
if (!wplua_load_uri (L, URI_API, &error) ||
|
||||
!wplua_pcall (L, 0, 0, &error)) {
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ SANDBOX_EXPORT = {
|
|||
LocalModule = WpImplModule_new,
|
||||
ImplMetadata = WpImplMetadata_new,
|
||||
Settings = WpSettings,
|
||||
CollectionManager = WpCollectionManager,
|
||||
Conf = WpConf,
|
||||
JsonUtils = JsonUtils,
|
||||
ProcUtils = ProcUtils,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ struct _WpStandardEventSource
|
|||
WpEventHook *rescan_done_hook;
|
||||
gboolean rescan_scheduled[N_RESCAN_CONTEXTS];
|
||||
gint n_oms_installed;
|
||||
WpCollectionManager *cm;
|
||||
};
|
||||
|
||||
static guint signals[N_SIGNALS] = {0};
|
||||
|
|
@ -157,7 +158,8 @@ static gint
|
|||
get_default_event_priority (const gchar *event_type)
|
||||
{
|
||||
if (g_str_has_prefix(event_type, "select-") ||
|
||||
g_str_has_prefix(event_type, "create-"))
|
||||
g_str_has_prefix(event_type, "create-") ||
|
||||
g_str_has_prefix(event_type, "destroy-"))
|
||||
return 500;
|
||||
else if (!g_strcmp0 (event_type, "rescan-for-default-nodes"))
|
||||
return -490;
|
||||
|
|
@ -209,7 +211,8 @@ static gboolean
|
|||
is_it_local_event (const gchar *event_type)
|
||||
{
|
||||
if (g_str_has_prefix(event_type, "select-") ||
|
||||
g_str_has_prefix(event_type, "create-"))
|
||||
g_str_has_prefix(event_type, "create-") ||
|
||||
g_str_has_prefix(event_type, "destroy-"))
|
||||
return TRUE;
|
||||
|
||||
return FALSE;
|
||||
|
|
@ -307,6 +310,26 @@ wp_standard_event_source_schedule_rescan (WpStandardEventSource *self,
|
|||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_collected (WpObjectManager *om, WpGlobalProxy *global,
|
||||
const gchar *collection_name, WpStandardEventSource *self)
|
||||
{
|
||||
g_autoptr (WpProperties) properties = wp_properties_new_empty ();
|
||||
wp_properties_set (properties, "event.subject.collection", collection_name);
|
||||
|
||||
wp_standard_event_source_push_event (self, "collected", global, properties);
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_dropped (WpObjectManager *om, WpGlobalProxy *global,
|
||||
const gchar *collection_name, WpStandardEventSource *self)
|
||||
{
|
||||
g_autoptr (WpProperties) properties = wp_properties_new_empty ();
|
||||
wp_properties_set (properties, "event.subject.collection", collection_name);
|
||||
|
||||
wp_standard_event_source_push_event (self, "dropped", global, properties);
|
||||
}
|
||||
|
||||
static void
|
||||
on_metadata_changed (WpMetadata *obj, guint32 subject,
|
||||
const gchar *key, const gchar *spa_type, const gchar *value,
|
||||
|
|
@ -350,6 +373,7 @@ on_node_state_changed (WpNode *obj, WpNodeState old_state,
|
|||
static void
|
||||
on_object_added (WpObjectManager *om, WpObject *obj, WpStandardEventSource *self)
|
||||
{
|
||||
wp_standard_event_source_push_event (self, "select-collection", obj, NULL);
|
||||
wp_standard_event_source_push_event (self, "added", obj, NULL);
|
||||
|
||||
if (WP_IS_PIPEWIRE_OBJECT (obj)) {
|
||||
|
|
@ -405,6 +429,15 @@ wp_standard_event_source_enable (WpPlugin * plugin, WpTransition * transition)
|
|||
wp_event_dispatcher_get_instance (core);
|
||||
g_return_if_fail (dispatcher);
|
||||
|
||||
/* Get the collection manager if any */
|
||||
self->cm = wp_collection_manager_find (core, NULL);
|
||||
if (self->cm) {
|
||||
g_signal_connect_object (self->cm, "global-collected",
|
||||
G_CALLBACK (on_global_collected), self, 0);
|
||||
g_signal_connect_object (self->cm, "global-dropped",
|
||||
G_CALLBACK (on_global_dropped), self, 0);
|
||||
}
|
||||
|
||||
/* install object managers */
|
||||
self->n_oms_installed = 0;
|
||||
for (gint i = 0; i < N_OBJECT_TYPES; i++) {
|
||||
|
|
@ -447,6 +480,8 @@ wp_standard_event_source_disable (WpPlugin * plugin)
|
|||
if (dispatcher)
|
||||
wp_event_dispatcher_unregister_hook (dispatcher, self->rescan_done_hook);
|
||||
g_clear_object (&self->rescan_done_hook);
|
||||
|
||||
g_clear_object (&self->cm);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -534,10 +556,15 @@ wireplumber.components = [
|
|||
name = device/autoswitch-bluetooth-profile.lua, type = script/lua
|
||||
provides = hooks.device.profile.autoswitch-bluetooth
|
||||
}
|
||||
{
|
||||
name = device/create-alsa-loopback.lua, type = script/lua
|
||||
provides = hooks.device.profile.create-alsa-loopback
|
||||
}
|
||||
{
|
||||
type = virtual, provides = policy.device.profile
|
||||
requires = [ hooks.device.profile.select,
|
||||
hooks.device.profile.autoswitch-bluetooth,
|
||||
hooks.device.profile.create-alsa-loopback,
|
||||
hooks.device.profile.apply ]
|
||||
wants = [ hooks.device.profile.find-voice-call,
|
||||
hooks.device.profile.find-best,
|
||||
|
|
@ -654,38 +681,46 @@ wireplumber.components = [
|
|||
provides = hooks.linking.rescan
|
||||
}
|
||||
{
|
||||
name = linking/find-media-role-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-media-role
|
||||
name = linking/default/find-media-role-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-media-role
|
||||
}
|
||||
{
|
||||
name = linking/find-defined-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-defined
|
||||
name = linking/default/find-defined-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-defined
|
||||
}
|
||||
{
|
||||
name = linking/find-audio-group-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-audio-group
|
||||
name = linking/default/find-audio-group-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-audio-group
|
||||
requires = [ node.audio-group ]
|
||||
}
|
||||
{
|
||||
name = linking/find-filter-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-filter
|
||||
name = linking/default/find-filter-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-filter
|
||||
requires = [ metadata.filters ]
|
||||
}
|
||||
{
|
||||
name = linking/find-default-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-default
|
||||
name = linking/default/find-default-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-default
|
||||
requires = [ api.default-nodes ]
|
||||
}
|
||||
{
|
||||
name = linking/find-best-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-best
|
||||
name = linking/default/find-best-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-best
|
||||
requires = [ metadata.filters ]
|
||||
}
|
||||
{
|
||||
name = linking/get-filter-from-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.get-filter-from
|
||||
name = linking/default/get-filter-from-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.get-filter-from
|
||||
requires = [ metadata.filters ]
|
||||
}
|
||||
{
|
||||
name = linking/alsa-loopback/find-filter-target.lua, type = script/lua
|
||||
provides = hooks.linking.alsa-loopback.target.find-filter
|
||||
}
|
||||
{
|
||||
name = linking/alsa-loopback/find-alsa-target.lua, type = script/lua
|
||||
provides = hooks.linking.alsa-loopback.target.find-alsa
|
||||
}
|
||||
{
|
||||
name = linking/prepare-link.lua, type = script/lua
|
||||
provides = hooks.linking.target.prepare-link
|
||||
|
|
@ -706,13 +741,15 @@ wireplumber.components = [
|
|||
requires = [ hooks.linking.rescan,
|
||||
hooks.linking.target.prepare-link,
|
||||
hooks.linking.target.link ]
|
||||
wants = [ hooks.linking.target.find-media-role,
|
||||
hooks.linking.target.find-defined,
|
||||
hooks.linking.target.find-audio-group,
|
||||
hooks.linking.target.find-filter,
|
||||
hooks.linking.target.find-default,
|
||||
hooks.linking.target.find-best,
|
||||
hooks.linking.target.get-filter-from,
|
||||
wants = [ hooks.linking.default.target.find-media-role,
|
||||
hooks.linking.default.target.find-defined,
|
||||
hooks.linking.default.target.find-audio-group,
|
||||
hooks.linking.default.target.find-filter,
|
||||
hooks.linking.default.target.find-default,
|
||||
hooks.linking.default.target.find-best,
|
||||
hooks.linking.default.target.get-filter-from,
|
||||
hooks.linking.alsa-loopback.target.find-filter,
|
||||
hooks.linking.alsa-loopback.target.find-alsa,
|
||||
hooks.linking.pause-playback ]
|
||||
}
|
||||
|
||||
|
|
@ -728,15 +765,15 @@ wireplumber.components = [
|
|||
requires = [ hooks.linking.role-based.rescan ]
|
||||
}
|
||||
{
|
||||
name = linking/find-media-role-sink-target.lua, type = script/lua
|
||||
provides = hooks.linking.target.find-media-role-sink
|
||||
name = linking/default/find-media-role-sink-target.lua, type = script/lua
|
||||
provides = hooks.linking.default.target.find-media-role-sink
|
||||
}
|
||||
{
|
||||
type = virtual, provides = policy.linking.role-based
|
||||
requires = [ policy.linking.standard,
|
||||
hooks.linking.role-based.rescan,
|
||||
hooks.node.role-based.default-volume,
|
||||
hooks.linking.target.find-media-role-sink ]
|
||||
hooks.linking.default.target.find-media-role-sink ]
|
||||
}
|
||||
|
||||
## Standard policy definition
|
||||
|
|
|
|||
|
|
@ -129,3 +129,71 @@ monitor.alsa.rules = [
|
|||
# }
|
||||
# }
|
||||
]
|
||||
|
||||
loopback.alsa.rules = [
|
||||
## The list of ALSA loopback rules
|
||||
|
||||
## This rule example allows creating a loopback for the route device 3 of
|
||||
## all ALSA devices.
|
||||
# {
|
||||
# matches = [
|
||||
# {
|
||||
# ## This matches all ALSA cards
|
||||
# device.name = "~alsa_card.*"
|
||||
# }
|
||||
# ]
|
||||
# actions = {
|
||||
# ## MANDATORY: This action is used to create a loopback for a particular
|
||||
# ## route device number. An ALSA loopback collection for this route
|
||||
# ## device will also be created so that it has its own policy.
|
||||
# create-loopback = {
|
||||
# ## This creates a loopback for sub-device 3, which is the ALSA node
|
||||
# ## that represents the route with device number 3.
|
||||
# [
|
||||
# ## MANDATORY: The route device this loopback will be used with
|
||||
# route-device = 3
|
||||
#
|
||||
# ## OPTIONAL: Whether the loopback is dynamic or not. Dynamic loopbacks
|
||||
# ## are only available if the current ALSA profile supports the given
|
||||
# ## route device.
|
||||
# dynamic = false
|
||||
#
|
||||
# ## OPTIONAL: Extra ALSA loopback node properties
|
||||
# device-node-props = {
|
||||
# card.profile.device = 3
|
||||
# node.virtual = false
|
||||
# priority.session = 1000
|
||||
# }
|
||||
#
|
||||
# ## OPTIONAL: Extra stream loopback node properties
|
||||
# stream-node-props = {
|
||||
# node.virtual = false
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# ## OPTIONAL: This action is used to collect extra nodes into the ALSA
|
||||
# ## loopback collection, which can be useful if we want to include extra
|
||||
# ## filter nodes.
|
||||
# collect-nodes = [
|
||||
# {
|
||||
# matches = [
|
||||
# {
|
||||
# ## This matches an ALSA device filter node
|
||||
# node.name = "filter-evice-node-name"
|
||||
# }
|
||||
# {
|
||||
# ## This matches an ALSA stream filter node
|
||||
# node.name = "filter-stream-node-name"
|
||||
# }
|
||||
# ]
|
||||
# actions = {
|
||||
# ## MANDATORY: This action is used to specify where to collect the
|
||||
# ## matched nodes
|
||||
# select-route-device = 3
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ SimpleEventHook {
|
|||
local selected_node = event:get_data ("selected-node")
|
||||
|
||||
local om = source:call ("get-object-manager", "metadata")
|
||||
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||
local metadata = om:lookup {
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
}
|
||||
if metadata == nil then
|
||||
return
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ SimpleEventHook {
|
|||
local props = event:get_properties ()
|
||||
local def_node_type = props ["default-node.type"]
|
||||
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||
local metadata = metadata_om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||
local metadata = metadata_om:lookup {
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
}
|
||||
local obj = metadata:find (0, "default.configured." .. def_node_type)
|
||||
|
||||
if not obj then
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@ SimpleEventHook {
|
|||
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||
Constraint { "media.class", "#", "Audio/*" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||
Constraint { "media.class", "#", "Video/*" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
|
|
@ -32,6 +34,7 @@ SimpleEventHook {
|
|||
Constraint { "event.subject.key", "c", "default.configured.audio.sink",
|
||||
"default.configured.audio.source", "default.configured.video.source"
|
||||
},
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed"},
|
||||
|
|
@ -96,13 +99,14 @@ function collectAvailableNodes (si_om, devices_om, port_direction, media_classes
|
|||
for linkable in si_om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "media.class", "c", table.unpack (media_classes) },
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
local node = linkable:get_associated_proxy ("node")
|
||||
local node_props = node.properties
|
||||
|
||||
-- check that the node has ports in the requested direction
|
||||
if not node:lookup_port {
|
||||
Constraint { "port.direction", "=", port_direction }
|
||||
Constraint { "port.direction", "=", port_direction },
|
||||
} then
|
||||
goto next_linkable
|
||||
end
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ store_configured_default_nodes_hook = SimpleEventHook {
|
|||
Constraint { "event.subject.key", "c", "default.configured.audio.sink",
|
||||
"default.configured.audio.source", "default.configured.video.source"
|
||||
},
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -119,6 +120,7 @@ metadata_added_hook = SimpleEventHook {
|
|||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-added" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
|
|||
478
src/scripts/device/create-alsa-loopback.lua
Normal file
478
src/scripts/device/create-alsa-loopback.lua
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2025 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- This collect-global.lua script collects globals into collections.
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
|
||||
config = {}
|
||||
config.rules = Conf.get_section_as_json ("loopback.alsa.rules", Json.Array {})
|
||||
|
||||
local alsa_loopback_ids = {}
|
||||
local alsa_loopbacks = {}
|
||||
|
||||
function getProfileLoopbackIds (profile)
|
||||
local loopback_ids = {}
|
||||
if type (profile.classes) == "table" and profile.classes.pod_type == "Struct" then
|
||||
for _, p in ipairs (profile.classes) do
|
||||
if type (p) == "table" and p.pod_type == "Struct" then
|
||||
local i = 1
|
||||
while true do
|
||||
local k, v = p [i], p [i+1]
|
||||
i = i + 2
|
||||
if not k or not v then
|
||||
break
|
||||
end
|
||||
if k == "card.profile.devices" and
|
||||
type (v) == "table" and v.pod_type == "Array" then
|
||||
for _, dev_id in ipairs (v) do
|
||||
loopback_ids [dev_id + 1] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return loopback_ids
|
||||
end
|
||||
|
||||
AsyncEventHook {
|
||||
name = "device/evaluate-loopbacks",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Profile" },
|
||||
Constraint { "device.api", "=", "alsa" },
|
||||
},
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local source = event:get_source ()
|
||||
local device = event:get_subject ()
|
||||
local create_loopback_info = nil
|
||||
local collect_node_rules = Json.Array {}
|
||||
local loopback_info = {}
|
||||
|
||||
-- Check if there are matching rules for this device
|
||||
JsonUtils.match_rules (config.rules, device.properties, function (action, value)
|
||||
if action == "create-loopback" then
|
||||
create_loopback_info = value:parse ()
|
||||
elseif action == "collect-nodes" then
|
||||
collect_node_rules = value
|
||||
end
|
||||
return true
|
||||
end)
|
||||
if create_loopback_info == nil then
|
||||
transition:advance ()
|
||||
return
|
||||
end
|
||||
|
||||
-- Populate the loopback_info table from the create_loopback_info table
|
||||
for _, info in pairs(create_loopback_info) do
|
||||
local id = info ["route-device"]
|
||||
if id ~= nil and tonumber (id) >= 0 then
|
||||
loopback_info [tonumber (id) + 1] = {}
|
||||
loopback_info [tonumber (id) + 1].dynamic = info ["dynamic"] or false
|
||||
loopback_info [tonumber (id) + 1].device_node_props = info ["device-node-props"] or {}
|
||||
loopback_info [tonumber (id) + 1].stream_node_props = info ["stream-node-props"] or {}
|
||||
else
|
||||
log:warning (device,
|
||||
"Found create loopback info without valid route-device property. Ignoring...")
|
||||
end
|
||||
end
|
||||
|
||||
-- Get routes information
|
||||
device:enum_params ("EnumRoute", function (enum_route_it, e)
|
||||
-- check for error
|
||||
if e then
|
||||
transition:return_error ("failed to enum routes: "
|
||||
.. tostring (e));
|
||||
return
|
||||
end
|
||||
|
||||
-- Make sure the device is still valid
|
||||
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
|
||||
transition:advance ()
|
||||
return
|
||||
end
|
||||
|
||||
-- Get device routes info
|
||||
local routes_info = {}
|
||||
for p in enum_route_it:iterate() do
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
if not route then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
for _, id in ipairs(route.devices) do
|
||||
if routes_info [id + 1] == nil then
|
||||
routes_info [id + 1] = {}
|
||||
end
|
||||
routes_info [id + 1].available = route.available
|
||||
routes_info [id + 1].priority = route.priority
|
||||
routes_info [id + 1].direction = route.direction
|
||||
routes_info [id + 1].name = route.name
|
||||
routes_info [id + 1].media_class =
|
||||
route.direction == "Input" and "Audio/Source" or "Audio/Sink"
|
||||
end
|
||||
|
||||
::skip_enum_route::
|
||||
end
|
||||
|
||||
-- Make sure we found route info for all configured loopbacks
|
||||
local all_loopback_info_found = true
|
||||
for id, _ in pairs(loopback_info) do
|
||||
if routes_info [id] == nil then
|
||||
all_loopback_info_found = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if not all_loopback_info_found then
|
||||
transition:return_error (
|
||||
"Could not find route info for configured ALSA loopback IDs");
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the current profile
|
||||
local profile = nil
|
||||
for p in device:iterate_params ("Profile") do
|
||||
profile = cutils.parseParam (p, "Profile")
|
||||
break
|
||||
end
|
||||
assert (profile)
|
||||
|
||||
-- Get current and old loopback Ids
|
||||
local new_loopback_ids = getProfileLoopbackIds (profile)
|
||||
local old_loopback_ids = alsa_loopback_ids [device.id] ~= nil and
|
||||
alsa_loopback_ids [device.id] or {}
|
||||
alsa_loopback_ids [device.id] = new_loopback_ids
|
||||
|
||||
-- Create dynamic loopbacks
|
||||
for id, _ in ipairs(new_loopback_ids) do
|
||||
local info = loopback_info [id]
|
||||
if old_loopback_ids [id] == nil and info ~= nil and info.dynamic then
|
||||
local e = source:call ("create-event", "create-loopback", device, nil)
|
||||
e:set_data ("loopback-id", id - 1)
|
||||
e:set_data ("media-class", routes_info [id].media_class)
|
||||
e:set_data ("device-node-props", info.device_node_props)
|
||||
e:set_data ("stream-node-props", info.stream_node_props)
|
||||
e:set_data ("collect-node-rules", collect_node_rules)
|
||||
EventDispatcher.push_event (e)
|
||||
end
|
||||
end
|
||||
|
||||
-- Destroy dynamic loopbacks
|
||||
for id, _ in ipairs(old_loopback_ids) do
|
||||
local info = loopback_info [id]
|
||||
if new_loopback_ids [id] == nil and info ~= nil and info.dynamic then
|
||||
local e = source:call ("create-event", "destroy-loopback", device, nil)
|
||||
e:set_data ("loopback-id", id - 1)
|
||||
e:set_data ("media-class", routes_info [id].media_class)
|
||||
e:set_data ("device-node-props", info.device_node_props)
|
||||
e:set_data ("stream-node-props", info.stream_node_props)
|
||||
e:set_data ("collect-node-rules", collect_node_rules)
|
||||
EventDispatcher.push_event (e)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create static loopbacks if never created before
|
||||
for id, info in pairs(loopback_info) do
|
||||
if not info.dynamic and
|
||||
(alsa_loopbacks [device.id] == nil or alsa_loopbacks [device.id][id] == nil) then
|
||||
local e = source:call ("create-event", "create-loopback", device, nil)
|
||||
e:set_data ("loopback-id", id - 1)
|
||||
e:set_data ("media-class", routes_info [id].media_class)
|
||||
e:set_data ("device-node-props", info.device_node_props)
|
||||
e:set_data ("stream-node-props", info.stream_node_props)
|
||||
e:set_data ("collect-node-rules", collect_node_rules)
|
||||
EventDispatcher.push_event (e)
|
||||
end
|
||||
end
|
||||
|
||||
transition:advance ()
|
||||
end)
|
||||
end
|
||||
}
|
||||
}
|
||||
}:register ()
|
||||
|
||||
function CreateLoopback (device, device_media_class, loopback_id, conf_device_node_props, conf_stream_node_props)
|
||||
local devide_id = device["bound-id"]
|
||||
local device_props = device.properties
|
||||
local loopback_name = device_props ["device.name"] .. "." .. tostring (loopback_id)
|
||||
local stream_media_class = nil
|
||||
local args = nil
|
||||
|
||||
-- Set stream media class
|
||||
if device_media_class == "Audio/Sink" then
|
||||
stream_media_class = "Stream/Output/Audio"
|
||||
elseif device_media_class == "Audio/Source" then
|
||||
stream_media_class = "Stream/Input/Audio"
|
||||
else
|
||||
log:warning (device, "Device media class '" .. device_media_class ..
|
||||
"' is not valid")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Set stream props
|
||||
local stream_raw_props = {
|
||||
["node.name"] = string.format ("alsa_loopback_stream.%s", loopback_name),
|
||||
["node.description"] = string.format ("ALSA Loopback stream for %s", loopback_name),
|
||||
["media.class"] = stream_media_class,
|
||||
["alsa.loopback.id"] = loopback_id,
|
||||
["device.id"] = devide_id,
|
||||
["node.passive"] = true,
|
||||
["node.dont-fallback"] = true,
|
||||
["node.linger"] = true,
|
||||
}
|
||||
for k, v in pairs (conf_stream_node_props) do
|
||||
stream_raw_props [k] = v
|
||||
end
|
||||
local stream_props = Json.Object (stream_raw_props)
|
||||
|
||||
-- Set device props
|
||||
local devices_raw_props = {
|
||||
["node.name"] = string.format ("alsa_loopback_device.%s", loopback_name),
|
||||
["node.description"] = string.format ("ALSA Loopback device for %s", loopback_name),
|
||||
["media.class"] = device_media_class,
|
||||
["alsa.loopback.id"] = loopback_id,
|
||||
["device.id"] = devide_id,
|
||||
}
|
||||
for k, v in pairs (conf_device_node_props) do
|
||||
devices_raw_props [k] = v
|
||||
end
|
||||
local device_props = Json.Object (devices_raw_props)
|
||||
|
||||
-- Set args
|
||||
if device_media_class == "Audio/Sink" then
|
||||
args = Json.Object {
|
||||
["playback.props"] = stream_props,
|
||||
["capture.props"] = device_props
|
||||
}
|
||||
elseif device_media_class == "Audio/Source" then
|
||||
args = Json.Object {
|
||||
["playback.props"] = device_props,
|
||||
["capture.props"] = stream_props
|
||||
}
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Load loopback
|
||||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||
end
|
||||
|
||||
function getCollectionName (device_name, loopback_id)
|
||||
return "alsa_loopback." .. device_name .. "." .. tostring (loopback_id)
|
||||
end
|
||||
|
||||
AsyncEventHook {
|
||||
name = "device/create-loopback",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "create-loopback" },
|
||||
Constraint { "device.api", "=", "alsa" },
|
||||
},
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local source = event:get_source ()
|
||||
local nodes_om = source:call ("get-object-manager", "node")
|
||||
local device = event:get_subject ()
|
||||
local device_name = device:get_property ("device.name")
|
||||
|
||||
-- Get the loopback ID
|
||||
local loopback_id = event:get_data ("loopback-id")
|
||||
if loopback_id == nil then
|
||||
transition:return_error ("Event data does not have loopback ID");
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the media class property
|
||||
local media_class = event:get_data ("media-class")
|
||||
if media_class == nil then
|
||||
transition:return_error ("Event data does not have media class property");
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the device properties
|
||||
local device_node_props = event:get_data ("device-node-props")
|
||||
if device_node_props == nil then
|
||||
transition:return_error ("Event data does not have device properties");
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the stream properties
|
||||
local stream_node_props = event:get_data ("stream-node-props")
|
||||
if stream_node_props == nil then
|
||||
transition:return_error ("Event data does not have stream properties");
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the collect node rules
|
||||
local collect_node_rules = event:get_data ("collect-node-rules")
|
||||
if collect_node_rules == nil then
|
||||
transition:return_error ("Event data does not have collect node rules");
|
||||
return
|
||||
end
|
||||
|
||||
-- Create the collection for this loopback
|
||||
local collection_name = getCollectionName (device_name, loopback_id)
|
||||
CollectionManager.create_collection (collection_name, function (_, e)
|
||||
-- Make sure the collection was created
|
||||
if e ~= nil then
|
||||
transition:return_error ("Failed to create collection '" ..
|
||||
collection_name .. "': " .. tostring (e));
|
||||
return
|
||||
end
|
||||
log:info (device, "Created ALSA loopback collection for ID " ..
|
||||
tostring (loopback_id) .. ": " .. collection_name)
|
||||
|
||||
-- Create the loopback module
|
||||
if alsa_loopbacks [device.id] == nil then
|
||||
alsa_loopbacks [device.id] = {}
|
||||
end
|
||||
alsa_loopbacks [device.id][loopback_id] = CreateLoopback (device,
|
||||
media_class, loopback_id, device_node_props, stream_node_props)
|
||||
log:info (device, "Created loopback module for ID " ..
|
||||
tostring (loopback_id))
|
||||
|
||||
-- Collect nodes to collection if any there are rules for them
|
||||
for node in nodes_om:iterate () do
|
||||
local node_name = node:get_property ("node.name")
|
||||
JsonUtils.match_rules (collect_node_rules, node.properties, function (action, value)
|
||||
if action == "select-route-device" and value:is_int () and value:parse () == loopback_id then
|
||||
if not CollectionManager.collect_global (node, collection_name) then
|
||||
log:warning (device, "Failed to collect node '" .. node_name ..
|
||||
"' into '" .. collection_name .. "'")
|
||||
else
|
||||
log:info (device, "Collected node '" .. node_name .. "' into '" ..
|
||||
collection_name .. "'")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
transition:advance ()
|
||||
end)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/destroy-loopback",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "destroy-loopback" },
|
||||
Constraint { "device.api", "=", "alsa" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
|
||||
-- Get the loopback ID
|
||||
local loopback_id = event:get_data ("loopback-id")
|
||||
if loopback_id == nil then
|
||||
log:warning (device, "Event data does not have loopback ID")
|
||||
return
|
||||
end
|
||||
|
||||
-- Destroy loopback module
|
||||
if alsa_loopbacks [device.id] == nil then
|
||||
alsa_loopbacks [device.id] = {}
|
||||
end
|
||||
alsa_loopbacks [device.id][loopback_id] = nil
|
||||
log:info (device, "Destroyed loopback module for ID " ..
|
||||
tostring (loopback_id))
|
||||
end
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/destroy-loopbacks",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-removed" },
|
||||
Constraint { "device.api", "=", "alsa" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
|
||||
-- Remove all loopbacks associated with this device
|
||||
alsa_loopbacks [device.id] = nil
|
||||
end
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/collect-node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-collection" },
|
||||
Constraint { "event.subject.type", "=", "node" },
|
||||
},
|
||||
},
|
||||
execute = function (event, transition)
|
||||
local source = event:get_source ()
|
||||
local node = event:get_subject ()
|
||||
|
||||
-- Get the node device Id
|
||||
local device_id = node.properties:get_int ("device.id")
|
||||
if device_id == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the associated device
|
||||
local devices_om = source:call ("get-object-manager", "device")
|
||||
local device = devices_om:lookup {
|
||||
Constraint { "bound-id", "=", device_id, type = "gobject" },
|
||||
}
|
||||
if device == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the loopback ID
|
||||
local loopback_id = node.properties:get_int ("card.profile.device")
|
||||
if loopback_id == nil then
|
||||
loopback_id = node.properties:get_int ("alsa.loopback.id")
|
||||
if loopback_id == nil then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Don't collect the ALSA loopback device node
|
||||
local link_group = node:get_property ("node.link-group")
|
||||
local media_class = node:get_property ("media.class")
|
||||
if link_group ~= nil and link_group:find ("^loopback") and
|
||||
not media_class:find ("^Stream/") then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if there is a collection for this ALSA loopback
|
||||
local device_name = device:get_property ("device.name")
|
||||
local collection_name = getCollectionName (device_name, loopback_id)
|
||||
if not CollectionManager.has_collection (collection_name) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Collect the node into the device collection
|
||||
local node_name = node:get_property ("node.name")
|
||||
if not CollectionManager.collect_global (node, collection_name) then
|
||||
log:warning (device, "Failed to collect node '" .. node_name ..
|
||||
"' into '" .. collection_name .. "'")
|
||||
return
|
||||
end
|
||||
|
||||
log:info (device, "Collected node '" .. node_name .. "' into '" ..
|
||||
collection_name .. "'")
|
||||
end,
|
||||
}:register ()
|
||||
|
|
@ -129,7 +129,10 @@ local function getFilterSmartTarget (metadata, node, om)
|
|||
|
||||
-- Find target
|
||||
local target = nil
|
||||
for si_target in om:iterate { type = "SiLinkable" } do
|
||||
for si_target in om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
local n_target = si_target:get_associated_proxy ("node")
|
||||
if n_target == nil then
|
||||
goto skip_target
|
||||
|
|
@ -291,7 +294,10 @@ local function rescanFilters (om, metadata_om)
|
|||
|
||||
Log.info ("rescanning filters...")
|
||||
|
||||
for si in om:iterate { type = "SiLinkable" } do
|
||||
for si in om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
local filter = {}
|
||||
|
||||
local n = si:get_associated_proxy ("node")
|
||||
|
|
@ -337,7 +343,8 @@ local function rescanFilters (om, metadata_om)
|
|||
filter.stream_si = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "node.link-group", "=", filter.link_group },
|
||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" }
|
||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||
Constraint { "collection.name", "-" },
|
||||
}
|
||||
|
||||
-- Add the filter to the list sorted by before and after
|
||||
|
|
|
|||
102
src/scripts/linking/alsa-loopback/find-alsa-target.lua
Normal file
102
src/scripts/linking/alsa-loopback/find-alsa-target.lua
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2025 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Finds the hardware target for a linkable in 'alsa_loopback.*' collection
|
||||
|
||||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-linking")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "linking/alsa-loopback/find-alsa-target",
|
||||
before = "linking/prepare-link",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "#", "alsa_loopback.*" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source, om, si, si_props, si_flags, target =
|
||||
lutils:unwrap_select_target_event (event)
|
||||
|
||||
-- Bypass the hook if the target is already picked up
|
||||
if target then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the collection name
|
||||
local collection_name = si_props["collection.name"]
|
||||
assert (collection_name)
|
||||
|
||||
local target_direction = cutils.getTargetDirection (si_props)
|
||||
local target_picked = nil
|
||||
local target_can_passthrough = false
|
||||
local target_priority = 0
|
||||
|
||||
log:info (string.format ("handling item %d: %s (%s)", si.id,
|
||||
si_props ["node.name"], si_props ["node.id"]))
|
||||
|
||||
-- Find the hardware target (session items without node.link-group property)
|
||||
-- matching the collection name
|
||||
for target in om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.node.type", "=", "device" },
|
||||
Constraint { "item.node.direction", "=", target_direction },
|
||||
Constraint { "media.type", "=", si_props ["media.type"] },
|
||||
Constraint { "collection.name", "=", collection_name },
|
||||
Constraint { "node.link-group", "-" },
|
||||
} do
|
||||
local target_props = target.properties
|
||||
local priority = tonumber (target_props ["priority.session"]) or 0
|
||||
|
||||
log:debug (string.format ("Looking at: %s (%s)",
|
||||
target_props ["node.name"], target_props ["node.id"]))
|
||||
|
||||
if not lutils.canLink (si_props, target) then
|
||||
log:debug ("... cannot link, skip linkable")
|
||||
goto skip_linkable
|
||||
end
|
||||
|
||||
local passthrough_compatible, can_passthrough =
|
||||
lutils.checkPassthroughCompatibility (si, target)
|
||||
if not passthrough_compatible then
|
||||
log:debug ("... passthrough is not compatible, skip linkable")
|
||||
goto skip_linkable
|
||||
end
|
||||
|
||||
if target_picked == nil or priority > target_priority then
|
||||
log:debug ("... picked")
|
||||
target_picked = target
|
||||
target_can_passthrough = can_passthrough
|
||||
target_priority = priority
|
||||
end
|
||||
|
||||
::skip_linkable::
|
||||
end
|
||||
|
||||
local can_passthrough, passthrough_compatible
|
||||
if target then
|
||||
passthrough_compatible, can_passthrough =
|
||||
lutils.checkPassthroughCompatibility (si, target)
|
||||
if lutils.canLink (si_props, target) and passthrough_compatible then
|
||||
target_picked = true
|
||||
end
|
||||
end
|
||||
|
||||
if target_picked then
|
||||
log:info (si,
|
||||
string.format ("... hardware target picked: %s (%s), can_passthrough:%s",
|
||||
target_picked.properties ["node.name"],
|
||||
target_picked.properties ["node.id"],
|
||||
tostring (target_can_passthrough)))
|
||||
si_flags.can_passthrough = target_can_passthrough
|
||||
event:set_data ("target", target_picked)
|
||||
else
|
||||
event:stop_processing ()
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
97
src/scripts/linking/alsa-loopback/find-filter-target.lua
Normal file
97
src/scripts/linking/alsa-loopback/find-filter-target.lua
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2026 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Finds the filter target for a linkable in 'alsa_loopback.*' collection
|
||||
|
||||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-linking")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "linking/alsa-loopback/find-filter-target",
|
||||
before = "linking/alsa-loopback/find-alsa-target",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "#", "alsa_loopback.*" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source, om, si, si_props, si_flags, target =
|
||||
lutils:unwrap_select_target_event (event)
|
||||
|
||||
-- Bypass the hook if the target is already picked up
|
||||
if target then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the collection name
|
||||
local collection_name = si_props["collection.name"]
|
||||
assert (collection_name)
|
||||
|
||||
local target_direction = cutils.getTargetDirection (si_props)
|
||||
local target_picked = nil
|
||||
local target_can_passthrough = false
|
||||
|
||||
log:info (string.format ("handling item %d: %s (%s)", si.id,
|
||||
si_props ["node.name"], si_props ["node.id"]))
|
||||
|
||||
-- Find the first filter target (session items with node.link-group property
|
||||
-- that is not an alsa loopback item) matching the collection name
|
||||
for target in om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.node.type", "=", "device" },
|
||||
Constraint { "item.node.direction", "=", target_direction },
|
||||
Constraint { "media.type", "=", si_props ["media.type"] },
|
||||
Constraint { "collection.name", "=", collection_name },
|
||||
Constraint { "node.link-group", "+" },
|
||||
Constraint { "alsa.loopback.id", "-" },
|
||||
} do
|
||||
local target_props = target.properties
|
||||
|
||||
log:debug (string.format ("Looking at: %s (%s)",
|
||||
target_props ["node.name"], target_props ["node.id"]))
|
||||
|
||||
if not lutils.canLink (si_props, target) then
|
||||
log:debug ("... cannot link, skip linkable")
|
||||
goto skip_linkable
|
||||
end
|
||||
|
||||
local passthrough_compatible, can_passthrough =
|
||||
lutils.checkPassthroughCompatibility (si, target)
|
||||
if not passthrough_compatible then
|
||||
log:debug ("... passthrough is not compatible, skip linkable")
|
||||
goto skip_linkable
|
||||
end
|
||||
|
||||
log:debug ("... picked")
|
||||
target_picked = target
|
||||
target_can_passthrough = can_passthrough
|
||||
break
|
||||
|
||||
::skip_linkable::
|
||||
end
|
||||
|
||||
local can_passthrough, passthrough_compatible
|
||||
if target then
|
||||
passthrough_compatible, can_passthrough =
|
||||
lutils.checkPassthroughCompatibility (si, target)
|
||||
if lutils.canLink (si_props, target) and passthrough_compatible then
|
||||
target_picked = true
|
||||
end
|
||||
end
|
||||
|
||||
if target_picked then
|
||||
log:info (si,
|
||||
string.format ("... filter target picked: %s (%s), can_passthrough:%s",
|
||||
target_picked.properties ["node.name"],
|
||||
target_picked.properties ["node.id"],
|
||||
tostring (target_can_passthrough)))
|
||||
si_flags.can_passthrough = target_can_passthrough
|
||||
event:set_data ("target", target_picked)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
|
@ -18,6 +18,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -56,6 +57,7 @@ SimpleEventHook {
|
|||
Constraint { "item.node.type", "=", "device" },
|
||||
Constraint { "item.node.direction", "=", target_direction },
|
||||
Constraint { "media.type", "=", si_props ["media.type"] },
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
target_node = target:get_associated_proxy ("node")
|
||||
target_node_props = target_node.properties
|
||||
|
|
@ -21,6 +21,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -46,6 +47,7 @@ SimpleEventHook {
|
|||
Constraint { "item.node.type", "=", "device" },
|
||||
Constraint { "item.node.direction", "=", target_direction },
|
||||
Constraint { "media.type", "=", si_props ["media.type"] },
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
local target_props = target.properties
|
||||
local target_node_id = target_props ["node.id"]
|
||||
|
|
@ -19,6 +19,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -19,6 +19,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -75,12 +76,16 @@ SimpleEventHook {
|
|||
target = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { target_key, "=", target_value },
|
||||
Constraint { "collection.name", "-" },
|
||||
}
|
||||
if target and lutils.canLink (si_props, target) then
|
||||
target_picked = true
|
||||
end
|
||||
elseif target_value then
|
||||
for lnkbl in om:iterate { type = "SiLinkable" } do
|
||||
for lnkbl in om:iterate {
|
||||
type = "SiLinkable",
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
local target_props = lnkbl.properties
|
||||
if (target_props ["node.name"] == target_value or
|
||||
target_props ["object.path"] == target_value) and
|
||||
|
|
@ -38,6 +38,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -19,6 +19,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -45,6 +46,7 @@ SimpleEventHook {
|
|||
type = "SiLinkable",
|
||||
Constraint { "media.class", "=", "Audio/Sink" },
|
||||
Constraint { "node.link-group", "=", link_group },
|
||||
Constraint { "collection.name", "-" },
|
||||
}
|
||||
|
||||
if input_node == nil then
|
||||
|
|
@ -62,20 +64,22 @@ SimpleEventHook {
|
|||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "node.name", "=", target_name },
|
||||
Constraint { "collection.name", "-" },
|
||||
}
|
||||
if si_target == nil then
|
||||
si_target = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "node.nick", "=", target_name },
|
||||
}
|
||||
si_target = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "node.nick", "=", target_name },
|
||||
Constraint { "collection.name", "-" },
|
||||
}
|
||||
end
|
||||
if si_target then
|
||||
log:info (si,
|
||||
log:info (si,
|
||||
string.format ("... role based sink target picked: %s (%s)",
|
||||
tostring (si_target.properties ["node.name"]),
|
||||
tostring (si_target.properties ["node.id"])))
|
||||
event:set_data ("target", si_target)
|
||||
event:set_data ("target", si_target)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
|
@ -18,6 +18,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -41,6 +42,7 @@ SimpleEventHook {
|
|||
Constraint { "item.node.direction", "=", target_direction },
|
||||
Constraint { "device.intended-roles", "+" },
|
||||
Constraint { "media.type", "=", si_props["media.type"] },
|
||||
Constraint { "collection.name", "-" },
|
||||
} do
|
||||
|
||||
local roles_json = si_target.properties["device.intended-roles"]
|
||||
|
|
@ -15,6 +15,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -22,6 +22,7 @@ SimpleEventHook {
|
|||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
Constraint { "collection.name", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -68,6 +68,7 @@ function getSuspendPlaybackFromMetadata (om)
|
|||
local metadata = om:lookup {
|
||||
type = "metadata",
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
}
|
||||
if metadata then
|
||||
local value = metadata:find(0, "suspend.playback")
|
||||
|
|
@ -92,6 +93,7 @@ AsyncEventHook {
|
|||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "=", "suspend.playback" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
}
|
||||
},
|
||||
steps = {
|
||||
|
|
|
|||
|
|
@ -234,11 +234,13 @@ SimpleEventHook {
|
|||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "c", "default.audio.source",
|
||||
"default.audio.sink", "default.video.source" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
},
|
||||
-- on any "filters" metadata changed
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "filters" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
@ -312,6 +314,7 @@ function handleMoveSetting (enable)
|
|||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "c", "target.object", "target.node" },
|
||||
Constraint { "wireplumber.collection", "-" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function configProperties (node)
|
|||
local properties = node.properties
|
||||
local media_class = properties ["media.class"] or ""
|
||||
local factory_name = properties ["factory.name"] or ""
|
||||
local collection = CollectionManager.get_global_collection (node)
|
||||
|
||||
-- ensure a media.type is set
|
||||
if not properties ["media.type"] then
|
||||
|
|
@ -44,6 +45,7 @@ function configProperties (node)
|
|||
(factory_name == "api.alsa.pcm.sink" or factory_name == "api.bluez5.a2dp.sink") and
|
||||
Settings.get_boolean ("node.features.audio.mono")
|
||||
properties ["node.id"] = node ["bound-id"]
|
||||
properties ["collection.name"] = collection and collection:get_name () or nil
|
||||
|
||||
-- set the default media.role, if configured
|
||||
-- avoid Settings.get_string(), as it will parse the default "null" value
|
||||
|
|
@ -61,69 +63,76 @@ AsyncEventHook {
|
|||
name = "node/create-item",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "event.type", "c", "node-added", "node-collected", "node-dropped" },
|
||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "event.type", "c", "node-added", "node-collected", "node-dropped" },
|
||||
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "event.type", "c", "node-added", "node-collected", "node-dropped" },
|
||||
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
|
||||
Constraint { "wireplumber.is-virtual", "-", type = "pw" },
|
||||
},
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "register",
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local node = event:get_subject ()
|
||||
local id = node.id
|
||||
local item
|
||||
local item_type
|
||||
|
||||
local media_class = node.properties ['media.class']
|
||||
local node_name = node.properties ['node.name']
|
||||
local collection = CollectionManager.get_global_collection (node)
|
||||
local collection_name = collection and collection:get_name () or nil
|
||||
|
||||
-- Just return if the item already exists with the same collection name
|
||||
if items [id] and
|
||||
items [id]:get_property ("collection.name") == collection_name then
|
||||
transition:advance ()
|
||||
return
|
||||
end
|
||||
|
||||
-- Destroy the old item if any
|
||||
if items [id] then
|
||||
log:info (items [id], "destroying item for node " .. tostring (id))
|
||||
items [id]:remove ()
|
||||
items [id] = nil
|
||||
end
|
||||
|
||||
-- get the item type
|
||||
local item_type = nil
|
||||
if string.find (media_class, "Audio") then
|
||||
item_type = "si-audio-adapter"
|
||||
else
|
||||
item_type = "si-node"
|
||||
end
|
||||
|
||||
log:info (node, "creating item for node -> " .. item_type)
|
||||
|
||||
-- create item
|
||||
item = SessionItem (item_type)
|
||||
local item = SessionItem (item_type)
|
||||
items [id] = item
|
||||
log:info (item, "created item for node " .. tostring (id) .. ": " .. node_name)
|
||||
|
||||
-- configure item
|
||||
if not item:configure (configProperties (node)) then
|
||||
transition:return_error ("failed to configure item for node "
|
||||
.. tostring (id))
|
||||
transition:return_error ("failed to configure item for node " .. tostring (id))
|
||||
return
|
||||
end
|
||||
|
||||
-- activate item
|
||||
item:activate (Features.ALL, function (_, e)
|
||||
if e then
|
||||
transition:return_error ("failed to activate item: "
|
||||
.. tostring (e));
|
||||
else
|
||||
transition:advance ()
|
||||
transition:return_error ("failed to activate item for node " .. tostring (id) ..
|
||||
": " .. tostring (e));
|
||||
return
|
||||
end
|
||||
end)
|
||||
end,
|
||||
},
|
||||
register = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local node = event:get_subject ()
|
||||
local bound_id = node ["bound-id"]
|
||||
local item = items [node.id]
|
||||
|
||||
log:info (item, "activated item for node " .. tostring (bound_id))
|
||||
item:register ()
|
||||
transition:advance ()
|
||||
-- register item
|
||||
log:info (item, "activated item for node " .. tostring (id) .. ": " .. node_name)
|
||||
item:register ()
|
||||
transition:advance ()
|
||||
end)
|
||||
end,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ static struct {
|
|||
gboolean reset;
|
||||
} settings;
|
||||
|
||||
struct {
|
||||
const gchar *collection_name;
|
||||
} collections;
|
||||
|
||||
struct {
|
||||
guint64 id;
|
||||
const char *level;
|
||||
|
|
@ -1592,6 +1596,139 @@ out:
|
|||
g_main_loop_quit (self->loop);
|
||||
}
|
||||
|
||||
/* collections */
|
||||
|
||||
static gboolean
|
||||
collections_parse_positional (gint argc, gchar ** argv, GError **error)
|
||||
{
|
||||
cmdline.collections.collection_name = NULL;
|
||||
|
||||
if (argc >= 3)
|
||||
cmdline.collections.collection_name = argv[2];
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
collections_prepare (WpCtl * self, GError ** error)
|
||||
{
|
||||
wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
|
||||
wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
|
||||
WP_OBJECT_FEATURES_ALL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
print_global (WpCollection *collection, WpGlobalProxy *global)
|
||||
{
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
const gchar *global_name = NULL;
|
||||
gchar global_type = '-';
|
||||
guint32 bound_id;
|
||||
|
||||
props = wp_global_proxy_get_global_properties (global);
|
||||
bound_id = wp_proxy_get_bound_id (WP_PROXY (global));
|
||||
|
||||
if (WP_IS_NODE (global)) {
|
||||
global_name = wp_properties_get (props, "node.name");
|
||||
global_type = 'n';
|
||||
} else if (WP_IS_PORT (global)) {
|
||||
global_name = wp_properties_get (props, "port.name");
|
||||
global_type = 'p';
|
||||
} else if (WP_IS_DEVICE (global)) {
|
||||
global_name = wp_properties_get (props, "device.name");
|
||||
global_type = 'd';
|
||||
} else if (WP_IS_CLIENT (global)) {
|
||||
global_name = wp_properties_get (props, "client.name");
|
||||
global_type = 'c';
|
||||
} else if (WP_IS_METADATA (global)) {
|
||||
global_name = wp_properties_get (props, "metadata.name");
|
||||
global_type = 'm';
|
||||
} else if (WP_IS_FACTORY (global)) {
|
||||
global_name = wp_properties_get (props, "factory.name");
|
||||
global_type = 'f';
|
||||
}
|
||||
|
||||
g_print (" [%c] %4u. %s\n", global_type, bound_id,
|
||||
global_name ? global_name : "UNKNOWN");
|
||||
}
|
||||
|
||||
static void
|
||||
print_collection (WpCollectionManager *cm, WpCollection *collection)
|
||||
{
|
||||
const gchar *collection_name = wp_collection_get_name (collection);
|
||||
g_autoptr (WpMetadata) meta = NULL;
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
guint32 bound_id;
|
||||
|
||||
meta = wp_collection_get_metadata (collection);
|
||||
if (!meta)
|
||||
return;
|
||||
bound_id = wp_proxy_get_bound_id (WP_PROXY (meta));
|
||||
|
||||
g_print ("%4u. %s\n",
|
||||
bound_id,
|
||||
collection_name);
|
||||
|
||||
iter = wp_collection_manager_new_global_iterator (cm, collection_name);
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
WpGlobalProxy *global = g_value_get_object (&val);
|
||||
print_global (collection, global);
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
printf ("\n");
|
||||
}
|
||||
|
||||
static void
|
||||
print_collections (WpCollectionManager *cm)
|
||||
{
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
printf ("Collections:\n\n");
|
||||
|
||||
it = wp_collection_manager_new_collection_iterator (cm);
|
||||
while (wp_iterator_next (it, &item)) {
|
||||
WpCollection *collection = g_value_get_object (&item);
|
||||
print_collection (cm, collection);
|
||||
g_value_unset (&item);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
collections_run (WpCtl * self)
|
||||
{
|
||||
g_autoptr (WpCollectionManager) cm = NULL;
|
||||
const gchar *collection_name = cmdline.collections.collection_name;
|
||||
|
||||
cm = wp_collection_manager_find (self->core, NULL);
|
||||
if (!cm) {
|
||||
printf ("Could not find registered collection manager\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* Print all collections if name is not provided */
|
||||
if (collection_name) {
|
||||
WpCollection *collection;
|
||||
collection = wp_collection_manager_get_collection (cm, collection_name);
|
||||
if (collection)
|
||||
print_collection (cm, collection);
|
||||
else
|
||||
printf ("Collection '%s' does not exist\n", collection_name);
|
||||
} else {
|
||||
print_collections (cm);
|
||||
}
|
||||
|
||||
wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
|
||||
return;
|
||||
|
||||
error:
|
||||
self->exit_code = 3;
|
||||
g_main_loop_quit (self->loop);
|
||||
}
|
||||
|
||||
/* set-log-level */
|
||||
|
||||
static gboolean
|
||||
|
|
@ -1840,6 +1977,16 @@ static const struct subcommand {
|
|||
.prepare = settings_prepare,
|
||||
.run = settings_run,
|
||||
},
|
||||
{
|
||||
.name = "collections",
|
||||
.positional_args = "[COLLECTION]",
|
||||
.summary = "Shows information about collections",
|
||||
.description = NULL,
|
||||
.entries = { { NULL } },
|
||||
.parse_positional = collections_parse_positional,
|
||||
.prepare = collections_prepare,
|
||||
.run = collections_run,
|
||||
},
|
||||
{
|
||||
.name = "set-log-level",
|
||||
.positional_args = "[ID] LEVEL",
|
||||
|
|
@ -1867,6 +2014,22 @@ on_settings_activated (WpSettings *s, GAsyncResult *res, WpCtl *ctl)
|
|||
wp_core_register_object (ctl->core, g_object_ref (s));
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_manager_activated (WpCollectionManager *cm, GAsyncResult *res,
|
||||
WpCtl *ctl)
|
||||
{
|
||||
GError *error = NULL;
|
||||
|
||||
if (!wp_object_activate_finish (WP_OBJECT (cm), res, &error)) {
|
||||
fprintf (stderr, "%s\n", error->message);
|
||||
ctl->exit_code = 1;
|
||||
g_main_loop_quit (ctl->loop);
|
||||
return;
|
||||
}
|
||||
|
||||
wp_core_register_object (ctl->core, g_object_ref (cm));
|
||||
}
|
||||
|
||||
static void
|
||||
on_plugin_loaded (WpCore * core, GAsyncResult * res, WpCtl *ctl)
|
||||
{
|
||||
|
|
@ -1894,6 +2057,7 @@ main (gint argc, gchar **argv)
|
|||
g_autoptr (GError) error = NULL;
|
||||
g_autofree gchar *summary = NULL;
|
||||
g_autoptr (WpSettings) settings = NULL;
|
||||
g_autoptr (WpCollectionManager) collection_manager = NULL;
|
||||
|
||||
setlocale (LC_ALL, "");
|
||||
setlocale (LC_NUMERIC, "C");
|
||||
|
|
@ -1976,6 +2140,14 @@ main (gint argc, gchar **argv)
|
|||
(GAsyncReadyCallback)on_settings_activated,
|
||||
&ctl);
|
||||
|
||||
/* load and register the collection manager */
|
||||
collection_manager = wp_collection_manager_new (ctl.core, NULL);
|
||||
wp_object_activate (WP_OBJECT (collection_manager),
|
||||
WP_OBJECT_FEATURES_ALL,
|
||||
NULL,
|
||||
(GAsyncReadyCallback)on_collection_manager_activated,
|
||||
&ctl);
|
||||
|
||||
/* load required API modules */
|
||||
ctl.pending_plugins++;
|
||||
wp_core_load_component (ctl.core, "libwireplumber-module-default-nodes-api",
|
||||
|
|
|
|||
|
|
@ -240,12 +240,12 @@ load_components (ScriptRunnerFixture *f, gconstpointer argv)
|
|||
|
||||
load_component (f, "node/create-item.lua", "script/lua");
|
||||
|
||||
load_component (f, "linking/find-best-target.lua", "script/lua");
|
||||
load_component (f, "linking/find-default-target.lua", "script/lua");
|
||||
load_component (f, "linking/find-defined-target.lua", "script/lua");
|
||||
load_component (f, "linking/find-filter-target.lua", "script/lua");
|
||||
load_component (f, "linking/find-media-role-target.lua", "script/lua");
|
||||
load_component (f, "linking/get-filter-from-target.lua", "script/lua");
|
||||
load_component (f, "linking/default/find-best-target.lua", "script/lua");
|
||||
load_component (f, "linking/default/find-default-target.lua", "script/lua");
|
||||
load_component (f, "linking/default/find-defined-target.lua", "script/lua");
|
||||
load_component (f, "linking/default/find-filter-target.lua", "script/lua");
|
||||
load_component (f, "linking/default/find-media-role-target.lua", "script/lua");
|
||||
load_component (f, "linking/default/get-filter-from-target.lua", "script/lua");
|
||||
load_component (f, "linking/link-target.lua", "script/lua");
|
||||
load_component (f, "linking/prepare-link.lua", "script/lua");
|
||||
load_component (f, "linking/rescan.lua", "script/lua");
|
||||
|
|
|
|||
446
tests/wp/collection-manager.c
Normal file
446
tests/wp/collection-manager.c
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2025 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "../common/base-test-fixture.h"
|
||||
|
||||
#define COLLECTION_A_NAME "sm-collection-a"
|
||||
#define COLLECTION_B_NAME "sm-collection-b"
|
||||
#define COLLECTION_C_NAME "sm-collection-c"
|
||||
#define COLLECTION_MANAGER_METADATA_NAME "sm-collection-manager"
|
||||
|
||||
typedef struct {
|
||||
WpBaseTestFixture base;
|
||||
WpImplMetadata *collection_a_metadata;
|
||||
WpImplMetadata *collection_b_metadata;
|
||||
WpImplMetadata *collection_manager_metadata;
|
||||
WpCollectionManager *collection_manager;
|
||||
WpCollection *collection_a;
|
||||
WpCollection *collection_b;
|
||||
} TestCollectionManagerFixture;
|
||||
|
||||
static void
|
||||
on_metadata_activated (WpMetadata * m, GAsyncResult * res,
|
||||
gpointer d)
|
||||
{
|
||||
TestCollectionManagerFixture *self = d;
|
||||
g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL));
|
||||
g_main_loop_quit (self->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_manager_metadata_activated (WpMetadata * m, GAsyncResult * res,
|
||||
gpointer d)
|
||||
{
|
||||
TestCollectionManagerFixture *self = d;
|
||||
g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL));
|
||||
|
||||
/* Add collections A and B */
|
||||
wp_metadata_set (m, 0, COLLECTION_A_NAME, "Spa:String:JSON", "1");
|
||||
wp_metadata_set (m, 0, COLLECTION_B_NAME, "Spa:String:JSON", "1");
|
||||
|
||||
g_main_loop_quit (self->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_manager_ready (WpCollectionManager *s, GAsyncResult *res,
|
||||
gpointer d)
|
||||
{
|
||||
TestCollectionManagerFixture *self = d;
|
||||
|
||||
g_assert_true (wp_object_activate_finish (WP_OBJECT (s), res, NULL));
|
||||
|
||||
wp_core_register_object (self->base.core, g_object_ref (s));
|
||||
|
||||
g_main_loop_quit (self->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
collection_manager_setup (TestCollectionManagerFixture *self, gconstpointer d)
|
||||
{
|
||||
wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE);
|
||||
|
||||
/* Create collection A metadata and activate it */
|
||||
self->collection_a_metadata = wp_impl_metadata_new_full (
|
||||
self->base.core, COLLECTION_A_NAME,
|
||||
wp_properties_new ("wireplumber.collection", "true", NULL));
|
||||
wp_object_activate (WP_OBJECT (self->collection_a_metadata),
|
||||
WP_OBJECT_FEATURES_ALL, NULL,
|
||||
(GAsyncReadyCallback)on_metadata_activated, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Create collection B metadata and activate it */
|
||||
self->collection_b_metadata = wp_impl_metadata_new_full (
|
||||
self->base.core, COLLECTION_B_NAME,
|
||||
wp_properties_new ("wireplumber.collection", "true", NULL));
|
||||
wp_object_activate (WP_OBJECT (self->collection_b_metadata),
|
||||
WP_OBJECT_FEATURES_ALL, NULL,
|
||||
(GAsyncReadyCallback)on_metadata_activated, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Create collection manager metadata and activate it */
|
||||
self->collection_manager_metadata = wp_impl_metadata_new_full (
|
||||
self->base.core, COLLECTION_MANAGER_METADATA_NAME,
|
||||
wp_properties_new ("wireplumber.collection-manager", "true", NULL));
|
||||
wp_object_activate (WP_OBJECT (self->collection_manager_metadata),
|
||||
WP_OBJECT_FEATURES_ALL, NULL,
|
||||
(GAsyncReadyCallback)on_collection_manager_metadata_activated, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Create the collection manager and activate it */
|
||||
self->collection_manager = wp_collection_manager_new (self->base.core,
|
||||
COLLECTION_MANAGER_METADATA_NAME);
|
||||
wp_object_activate (WP_OBJECT (self->collection_manager),
|
||||
WP_OBJECT_FEATURES_ALL,
|
||||
NULL,
|
||||
(GAsyncReadyCallback)on_collection_manager_ready,
|
||||
self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Get collections A and B */
|
||||
self->collection_a = wp_collection_manager_get_collection (
|
||||
self->collection_manager, COLLECTION_A_NAME);
|
||||
g_assert_nonnull (self->collection_a);
|
||||
self->collection_b = wp_collection_manager_get_collection (
|
||||
self->collection_manager, COLLECTION_B_NAME);
|
||||
g_assert_nonnull (self->collection_b);
|
||||
}
|
||||
|
||||
static void
|
||||
collection_manager_teardown (TestCollectionManagerFixture *self,
|
||||
gconstpointer d)
|
||||
{
|
||||
g_clear_object (&self->collection_b);
|
||||
g_clear_object (&self->collection_a);
|
||||
g_clear_object (&self->collection_manager);
|
||||
g_clear_object (&self->collection_manager_metadata);
|
||||
g_clear_object (&self->collection_b_metadata);
|
||||
g_clear_object (&self->collection_a_metadata);
|
||||
wp_base_test_fixture_teardown (&self->base);
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_created (WpCollectionManager * cm, GAsyncResult * res, gpointer d)
|
||||
{
|
||||
TestCollectionManagerFixture *self = d;
|
||||
WpCollection *c = wp_collection_manager_create_collection_finish (cm, res,
|
||||
NULL);
|
||||
g_assert_nonnull (c);
|
||||
g_assert_true (WP_IS_COLLECTION (c));
|
||||
g_main_loop_quit (self->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
test_collection_manager_collection (TestCollectionManagerFixture *self,
|
||||
gconstpointer d)
|
||||
{
|
||||
g_autoptr (WpImplMetadata) collection_c_metadata;
|
||||
|
||||
/* Make sure the manager has 2 collections */
|
||||
g_assert_cmpuint (wp_collection_manager_get_collection_count (
|
||||
self->collection_manager), ==, 2);
|
||||
|
||||
/* Make sure collections A and B were added */
|
||||
g_assert_true (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_A_NAME));
|
||||
g_assert_true (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_B_NAME));
|
||||
|
||||
/* Make sure collection C was not added */
|
||||
g_assert_false (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_C_NAME));
|
||||
|
||||
/* Create collection C metadata and activate it */
|
||||
collection_c_metadata = wp_impl_metadata_new_full (
|
||||
self->base.core, COLLECTION_C_NAME,
|
||||
wp_properties_new ("wireplumber.collection", "true", NULL));
|
||||
wp_object_activate (WP_OBJECT (collection_c_metadata),
|
||||
WP_OBJECT_FEATURES_ALL, NULL,
|
||||
(GAsyncReadyCallback)on_metadata_activated, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Make sure collection C is still not added */
|
||||
g_assert_false (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_C_NAME));
|
||||
|
||||
/* Create collection C */
|
||||
wp_collection_manager_create_collection (self->collection_manager,
|
||||
COLLECTION_C_NAME, NULL, (GAsyncReadyCallback)on_collection_created, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Make sure the manager has 3 collections */
|
||||
g_assert_cmpuint (wp_collection_manager_get_collection_count (
|
||||
self->collection_manager), ==, 3);
|
||||
|
||||
/* Iterate collections */
|
||||
{
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
gboolean has_collection_a = FALSE;
|
||||
gboolean has_collection_b = FALSE;
|
||||
gboolean has_collection_c = FALSE;
|
||||
guint count = 0;
|
||||
|
||||
iter = wp_collection_manager_new_collection_iterator (
|
||||
self->collection_manager);
|
||||
g_assert_nonnull (iter);
|
||||
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
WpCollection *c = g_value_get_object (&val);
|
||||
g_assert_nonnull (c);
|
||||
if (g_str_equal (wp_collection_get_name (c), COLLECTION_A_NAME))
|
||||
has_collection_a = TRUE;
|
||||
if (g_str_equal (wp_collection_get_name (c), COLLECTION_B_NAME))
|
||||
has_collection_b = TRUE;
|
||||
if (g_str_equal (wp_collection_get_name (c), COLLECTION_C_NAME))
|
||||
has_collection_c = TRUE;
|
||||
count++;
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
g_assert_true (has_collection_a);
|
||||
g_assert_true (has_collection_b);
|
||||
g_assert_true (has_collection_c);
|
||||
g_assert_cmpuint (count, ==, 3);
|
||||
}
|
||||
|
||||
/* Make sure collection C was added */
|
||||
g_assert_true (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_C_NAME));
|
||||
|
||||
/* Destroy collection C */
|
||||
g_assert_true (wp_collection_manager_destroy_collection (
|
||||
self->collection_manager, COLLECTION_C_NAME));
|
||||
|
||||
/* Make sure the manager has 2 collections */
|
||||
g_assert_cmpuint (wp_collection_manager_get_collection_count (
|
||||
self->collection_manager), ==, 2);
|
||||
|
||||
/* Make sure collection C was removed */
|
||||
g_assert_false (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_C_NAME));
|
||||
|
||||
/* Remove collections A and B */
|
||||
g_assert_true (wp_collection_manager_destroy_collection (
|
||||
self->collection_manager, COLLECTION_A_NAME));
|
||||
g_assert_true (wp_collection_manager_destroy_collection (
|
||||
self->collection_manager, COLLECTION_B_NAME));
|
||||
|
||||
/* Make sure the manager does not have any collections */
|
||||
g_assert_cmpuint (wp_collection_manager_get_collection_count (
|
||||
self->collection_manager), ==, 0);
|
||||
|
||||
/* Make sure collections A and B were removed */
|
||||
g_assert_false (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_A_NAME));
|
||||
g_assert_false (wp_collection_manager_has_collection (self->collection_manager,
|
||||
COLLECTION_B_NAME));
|
||||
}
|
||||
|
||||
static void
|
||||
test_collection_manager_global (TestCollectionManagerFixture *self,
|
||||
gconstpointer d)
|
||||
{
|
||||
g_autoptr (WpGlobalProxy) global_a = NULL;
|
||||
g_autoptr (WpGlobalProxy) global_b = NULL;
|
||||
|
||||
/* Activate global A */
|
||||
global_a = WP_GLOBAL_PROXY (wp_impl_metadata_new_full (self->base.core,
|
||||
"global-a", NULL));
|
||||
wp_object_activate (WP_OBJECT (global_a), WP_OBJECT_FEATURES_ALL, NULL,
|
||||
(GAsyncReadyCallback)on_metadata_activated, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Activate global B */
|
||||
global_b = WP_GLOBAL_PROXY (wp_impl_metadata_new_full (self->base.core,
|
||||
"global-a", NULL));
|
||||
wp_object_activate (WP_OBJECT (global_b), WP_OBJECT_FEATURES_ALL, NULL,
|
||||
(GAsyncReadyCallback)on_metadata_activated, self);
|
||||
g_main_loop_run (self->base.loop);
|
||||
|
||||
/* Make sure collection A and B don't have any globals */
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_a), ==, 0);
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 0);
|
||||
|
||||
/* Make sure global A and B don't belong to any collection */
|
||||
g_assert_false (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_a, NULL));
|
||||
g_assert_false (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_b, NULL));
|
||||
|
||||
/* Collect global A into collection A and global B into collection B */
|
||||
g_assert_true (wp_collection_manager_collect_global (self->collection_manager,
|
||||
global_a, COLLECTION_A_NAME));
|
||||
g_assert_true (wp_collection_manager_collect_global (self->collection_manager,
|
||||
global_b, COLLECTION_B_NAME));
|
||||
|
||||
/* Make sure collection A and B have only one global */
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_a), ==, 1);
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 1);
|
||||
|
||||
/* Iterate collection A */
|
||||
{
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
guint count = 0;
|
||||
|
||||
iter = wp_collection_manager_new_global_iterator (self->collection_manager,
|
||||
COLLECTION_A_NAME);
|
||||
g_assert_nonnull (iter);
|
||||
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
WpGlobalProxy *g = g_value_get_object (&val);
|
||||
g_assert_nonnull (g);
|
||||
g_assert_true (g == global_a);
|
||||
count++;
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
g_assert_cmpuint (count, ==, 1);
|
||||
}
|
||||
|
||||
/* Iterate collection B */
|
||||
{
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
guint count = 0;
|
||||
|
||||
iter = wp_collection_manager_new_global_iterator (self->collection_manager,
|
||||
COLLECTION_B_NAME);
|
||||
g_assert_nonnull (iter);
|
||||
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
WpGlobalProxy *g = g_value_get_object (&val);
|
||||
g_assert_nonnull (g);
|
||||
g_assert_true (g == global_b);
|
||||
count++;
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
g_assert_cmpuint (count, ==, 1);
|
||||
}
|
||||
|
||||
/* Make sure global A and global B are in their respective collection */
|
||||
g_assert_true (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_a, COLLECTION_A_NAME));
|
||||
g_assert_true (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_b, COLLECTION_B_NAME));
|
||||
{
|
||||
g_autoptr (WpCollection) ca = NULL;
|
||||
g_autoptr (WpCollection) cb = NULL;
|
||||
ca = wp_collection_manager_get_global_collection (self->collection_manager,
|
||||
global_a);
|
||||
g_assert_true (ca == self->collection_a);
|
||||
cb = wp_collection_manager_get_global_collection (self->collection_manager,
|
||||
global_b);
|
||||
g_assert_true (cb == self->collection_b);
|
||||
}
|
||||
|
||||
g_assert_true (WP_IS_COLLECTION (self->collection_a));
|
||||
|
||||
/* Make sure we cannot add global A into collection B and vice versa */
|
||||
g_assert_false (wp_collection_manager_collect_global (
|
||||
self->collection_manager, global_a, COLLECTION_B_NAME));
|
||||
g_assert_false (wp_collection_manager_collect_global (
|
||||
self->collection_manager, global_b, COLLECTION_A_NAME));
|
||||
|
||||
/* Drop global A from collection A */
|
||||
g_assert_true (wp_collection_manager_drop_global (self->collection_manager,
|
||||
global_a));
|
||||
|
||||
/* Make sure collection A does not have any globals */
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_a), ==, 0);
|
||||
|
||||
/* Make sure global A does not belong to any collection */
|
||||
g_assert_false (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_a, NULL));
|
||||
|
||||
/* Make sure collection A was automatically removed */
|
||||
g_assert_false (wp_collection_manager_has_collection (
|
||||
self->collection_manager, COLLECTION_A_NAME));
|
||||
|
||||
/* Collect global A into collection B */
|
||||
g_assert_true (wp_collection_manager_collect_global (self->collection_manager,
|
||||
global_a, COLLECTION_B_NAME));
|
||||
|
||||
/* Make sure global A is in collection B */
|
||||
g_assert_true (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_a, COLLECTION_B_NAME));
|
||||
|
||||
/* Make sure collection B has 2 globals */
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 2);
|
||||
|
||||
/* Iterate collection B */
|
||||
{
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
gboolean has_global_a = FALSE;
|
||||
gboolean has_global_b = FALSE;
|
||||
guint count = 0;
|
||||
|
||||
iter = wp_collection_manager_new_global_iterator (self->collection_manager,
|
||||
COLLECTION_B_NAME);
|
||||
g_assert_nonnull (iter);
|
||||
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
WpGlobalProxy *g = g_value_get_object (&val);
|
||||
g_assert_nonnull (g);
|
||||
if (g == global_a)
|
||||
has_global_a = TRUE;
|
||||
if (g == global_b)
|
||||
has_global_b = TRUE;
|
||||
count++;
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
g_assert_true (has_global_a);
|
||||
g_assert_true (has_global_b);
|
||||
g_assert_cmpuint (count, ==, 2);
|
||||
}
|
||||
|
||||
/* Drop global B from collection B */
|
||||
g_assert_true (wp_collection_manager_drop_global (self->collection_manager,
|
||||
global_b));
|
||||
|
||||
/* Make sure collection B has only one global */
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 1);
|
||||
|
||||
/* Make sure global B does not belong to any collection */
|
||||
g_assert_false (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_b, NULL));
|
||||
|
||||
/* Drop global A from collection B */
|
||||
g_assert_true (wp_collection_manager_drop_global (self->collection_manager,
|
||||
global_a));
|
||||
|
||||
/* Make sure collection B does not have any globals */
|
||||
g_assert_cmpuint (wp_collection_get_global_count (self->collection_b), ==, 0);
|
||||
|
||||
/* Make sure global A does not belong to any collection */
|
||||
g_assert_false (wp_collection_manager_is_global_collected (
|
||||
self->collection_manager, global_a, NULL));
|
||||
|
||||
/* Make sure collection B was automatically removed */
|
||||
g_assert_false (wp_collection_manager_has_collection (
|
||||
self->collection_manager, COLLECTION_B_NAME));
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
wp_init (WP_INIT_ALL);
|
||||
|
||||
g_test_add ("/wp/collection-manager/collection",
|
||||
TestCollectionManagerFixture, NULL, collection_manager_setup,
|
||||
test_collection_manager_collection, collection_manager_teardown);
|
||||
g_test_add ("/wp/collection-manager/global",
|
||||
TestCollectionManagerFixture, NULL, collection_manager_setup,
|
||||
test_collection_manager_global, collection_manager_teardown);
|
||||
|
||||
return g_test_run ();
|
||||
}
|
||||
|
|
@ -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-manager',
|
||||
executable('test-collection-manager', 'collection-manager.c',
|
||||
dependencies: common_deps),
|
||||
env: common_env,
|
||||
)
|
||||
|
||||
test(
|
||||
'test-component-loader',
|
||||
executable('test-component-loader', 'component-loader.c',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue