mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-09 02:48:05 +02:00
Merge branch 'metadata-collection' into 'master'
Draft: Add new collection proxy API to encapsulate objects together See merge request pipewire/wireplumber!830
This commit is contained in:
commit
bc23bc601d
35 changed files with 2870 additions and 92 deletions
991
lib/wp/collection.c
Normal file
991
lib/wp/collection.c
Normal file
|
|
@ -0,0 +1,991 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2026 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "collection.h"
|
||||
#include "core.h"
|
||||
#include "log.h"
|
||||
#include "error.h"
|
||||
#include "wpenums.h"
|
||||
|
||||
#include <pipewire/impl.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/extensions/metadata.h>
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-collection")
|
||||
|
||||
/*! \defgroup wpcollection WpCollection */
|
||||
/*!
|
||||
* \struct WpCollection
|
||||
*
|
||||
* The WpCollection class allows accessing the properties and methods of
|
||||
* PipeWire collection object (`struct pw_collection`).
|
||||
*
|
||||
* A WpCollection is constructed internally when a new collection object appears
|
||||
* on the PipeWire registry and it is made available through the WpObjectManager
|
||||
* API.
|
||||
*
|
||||
* \gsignals
|
||||
*
|
||||
* \par global_added
|
||||
* \parblock
|
||||
* \code
|
||||
* void
|
||||
* global_added_callback (WpCollection * self,
|
||||
* guint global_id,
|
||||
* gpointer user_data)
|
||||
* \endcode
|
||||
* Emitted when a gobal was added into the collection
|
||||
*
|
||||
* Parameters:
|
||||
* - `global_id` - the added global id
|
||||
*
|
||||
* Flags: G_SIGNAL_RUN_LAST
|
||||
* \endparblock
|
||||
*
|
||||
* \par global_removed
|
||||
* \parblock
|
||||
* \code
|
||||
* void
|
||||
* global_removed_callback (WpCollection * self,
|
||||
* guint global_id,
|
||||
* gpointer user_data)
|
||||
* \endcode
|
||||
* Emitted when a gobal was removed from the collection
|
||||
*
|
||||
* Parameters:
|
||||
* - `global_id` - the removed global id
|
||||
*
|
||||
* Flags: G_SIGNAL_RUN_LAST
|
||||
* \endparblock
|
||||
*/
|
||||
|
||||
enum {
|
||||
SIGNAL_GLOBAL_COLLECTED,
|
||||
SIGNAL_GLOBAL_DROPPED,
|
||||
N_SIGNALS,
|
||||
};
|
||||
|
||||
static guint32 signals[N_SIGNALS] = {0};
|
||||
|
||||
/* data structure */
|
||||
|
||||
struct item
|
||||
{
|
||||
uint32_t subject;
|
||||
gchar *key;
|
||||
gchar *type;
|
||||
gchar *value;
|
||||
};
|
||||
|
||||
static void
|
||||
set_item (struct item * item, uint32_t subject, const char * key,
|
||||
const char * type, const char * value)
|
||||
{
|
||||
item->subject = subject;
|
||||
item->key = key ? g_strdup (key) : NULL;
|
||||
item->type = type ? g_strdup (type) : NULL;
|
||||
item->value = value ? g_strdup (value) : NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
clear_item (struct item * item)
|
||||
{
|
||||
g_clear_pointer (&item->key, g_free);
|
||||
g_clear_pointer (&item->type, g_free);
|
||||
g_clear_pointer (&item->value, g_free);
|
||||
spa_zero (*item);
|
||||
}
|
||||
|
||||
static struct item *
|
||||
find_item (struct pw_array * metadata, uint32_t subject, const char * key)
|
||||
{
|
||||
struct item *item;
|
||||
|
||||
pw_array_for_each (item, metadata) {
|
||||
if (item->subject == subject && (key == NULL || !strcmp (item->key, key)))
|
||||
return item;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
clear_items (struct pw_array * metadata)
|
||||
{
|
||||
struct item *item;
|
||||
|
||||
pw_array_consume (item, metadata) {
|
||||
clear_item (item);
|
||||
pw_array_remove (metadata, item);
|
||||
}
|
||||
pw_array_reset (metadata);
|
||||
}
|
||||
|
||||
struct _WpCollection
|
||||
{
|
||||
WpGlobalProxy parent;
|
||||
|
||||
struct pw_metadata *iface;
|
||||
struct spa_hook listener;
|
||||
struct pw_array metadata;
|
||||
gboolean listener_added;
|
||||
};
|
||||
|
||||
|
||||
G_DEFINE_TYPE (WpCollection, wp_collection, WP_TYPE_GLOBAL_PROXY)
|
||||
|
||||
static void
|
||||
wp_collection_init (WpCollection * self)
|
||||
{
|
||||
pw_array_init (&self->metadata, 4096);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_finalize (GObject * object)
|
||||
{
|
||||
WpCollection * self = WP_COLLECTION (object);
|
||||
|
||||
pw_array_clear (&self->metadata);
|
||||
|
||||
G_OBJECT_CLASS (wp_collection_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static WpObjectFeatures
|
||||
wp_collection_get_supported_features (WpObject * object)
|
||||
{
|
||||
return WP_PROXY_FEATURE_BOUND | WP_COLLECTION_FEATURE_DATA;
|
||||
}
|
||||
|
||||
enum {
|
||||
STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
|
||||
STEP_CACHE
|
||||
};
|
||||
|
||||
static guint
|
||||
wp_collection_activate_get_next_step (WpObject * object,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
g_return_val_if_fail (
|
||||
missing & (WP_PROXY_FEATURE_BOUND | WP_COLLECTION_FEATURE_DATA),
|
||||
WP_TRANSITION_STEP_ERROR);
|
||||
|
||||
/* bind if not already bound */
|
||||
if (missing & WP_PROXY_FEATURE_BOUND)
|
||||
return STEP_BIND;
|
||||
else
|
||||
return STEP_CACHE;
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_activate_execute_step (WpObject * object,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
switch (step) {
|
||||
case STEP_CACHE:
|
||||
/* just wait for initial_sync_done() */
|
||||
break;
|
||||
default:
|
||||
WP_OBJECT_CLASS (wp_collection_parent_class)->
|
||||
activate_execute_step (object, transition, step, missing);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_event_property (void *object, uint32_t subject, const char *key,
|
||||
const char *type, const char *value)
|
||||
{
|
||||
WpCollection *self = WP_COLLECTION (object);
|
||||
struct item *item = NULL;
|
||||
|
||||
/* Clear subject if key is NULL */
|
||||
if (key == NULL) {
|
||||
while (true) {
|
||||
guint32 global_id = SPA_ID_INVALID;
|
||||
|
||||
item = find_item (&self->metadata, subject, NULL);
|
||||
if (item == NULL)
|
||||
break;
|
||||
|
||||
pw_array_remove (&self->metadata, item);
|
||||
|
||||
if (spa_atou32 (item->key, &global_id, 0))
|
||||
g_signal_emit (self, signals[SIGNAL_GLOBAL_DROPPED], 0, global_id);
|
||||
|
||||
clear_item (item);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
item = find_item (&self->metadata, subject, key);
|
||||
if (item == NULL) {
|
||||
if (value == NULL)
|
||||
return 0;
|
||||
item = pw_array_add (&self->metadata, sizeof (*item));
|
||||
if (item == NULL)
|
||||
return -errno;
|
||||
} else {
|
||||
clear_item (item);
|
||||
}
|
||||
|
||||
if (value != NULL) {
|
||||
guint32 global_id = SPA_ID_INVALID;
|
||||
set_item (item, subject, key, type, value);
|
||||
if (spa_atou32 (key, &global_id, 0))
|
||||
g_signal_emit (self, signals[SIGNAL_GLOBAL_COLLECTED], 0, global_id);
|
||||
} else {
|
||||
guint32 global_id = SPA_ID_INVALID;
|
||||
pw_array_remove (&self->metadata, item);
|
||||
if (spa_atou32 (key, &global_id, 0))
|
||||
g_signal_emit (self, signals[SIGNAL_GLOBAL_DROPPED], 0, global_id);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct pw_metadata_events metadata_events = {
|
||||
PW_VERSION_METADATA_EVENTS,
|
||||
.property = metadata_event_property,
|
||||
};
|
||||
|
||||
static void
|
||||
wp_collection_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
|
||||
{
|
||||
WpCollection *self = WP_COLLECTION (proxy);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
|
||||
self->iface = (struct pw_metadata *) pw_proxy;
|
||||
pw_metadata_add_listener (self->iface, &self->listener, &metadata_events,
|
||||
self);
|
||||
self->listener_added = TRUE;
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), WP_COLLECTION_FEATURE_DATA, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_pw_proxy_destroyed (WpProxy * proxy)
|
||||
{
|
||||
WpCollection *self = WP_COLLECTION (proxy);
|
||||
|
||||
if (self->listener_added) {
|
||||
spa_hook_remove (&self->listener);
|
||||
self->listener_added = FALSE;
|
||||
}
|
||||
clear_items (&self->metadata);
|
||||
wp_object_update_features (WP_OBJECT (self), 0, WP_COLLECTION_FEATURE_DATA);
|
||||
|
||||
WP_PROXY_CLASS (wp_collection_parent_class)->pw_proxy_destroyed (proxy);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_collection_class_init (WpCollectionClass * klass)
|
||||
{
|
||||
GObjectClass *object_class = (GObjectClass *) klass;
|
||||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
||||
WpProxyClass *proxy_class = (WpProxyClass *) klass;
|
||||
|
||||
object_class->finalize = wp_collection_finalize;
|
||||
|
||||
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;
|
||||
|
||||
proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Metadata;
|
||||
proxy_class->pw_iface_version = PW_VERSION_METADATA;
|
||||
proxy_class->pw_proxy_created = wp_collection_pw_proxy_created;
|
||||
proxy_class->pw_proxy_destroyed = wp_collection_pw_proxy_destroyed;
|
||||
|
||||
signals[SIGNAL_GLOBAL_COLLECTED] = g_signal_new ("global-collected",
|
||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||
G_TYPE_NONE, 1, G_TYPE_UINT);
|
||||
|
||||
signals[SIGNAL_GLOBAL_DROPPED] = g_signal_new ("global-dropped",
|
||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||
G_TYPE_NONE, 1, G_TYPE_UINT);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the name of the collection
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the collection object
|
||||
* \returns (nullable): the name of the collection
|
||||
*/
|
||||
const gchar *
|
||||
wp_collection_get_name (WpCollection * self)
|
||||
{
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION (self), NULL);
|
||||
|
||||
props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self));
|
||||
return props ? wp_properties_get (props, "collection.name") : NULL;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the total number of globals the collection has collected
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the collection object
|
||||
* \returns the total number of globals the collection has collected
|
||||
*/
|
||||
gsize
|
||||
wp_collection_get_size (WpCollection * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_COLLECTION (self), 0);
|
||||
|
||||
return pw_array_get_len (&self->metadata, struct item);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks if a global ID is collected in the collection
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the collection object
|
||||
* \param global_id the global ID to check
|
||||
* \returns TRUE if the global ID is collected in the colleciton, FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
gboolean
|
||||
wp_collection_contains_global (WpCollection * self, guint32 global_id)
|
||||
{
|
||||
struct item *item;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION (self), FALSE);
|
||||
|
||||
pw_array_for_each (item, &self->metadata) {
|
||||
guint32 id = SPA_ID_INVALID;
|
||||
if (spa_atou32 (item->key, &id, 0) && id == global_id)
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Collects a global ID into the collection.
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the collection object
|
||||
* \param global_id the global ID to collect
|
||||
*/
|
||||
void
|
||||
wp_collection_collect_global (WpCollection * self, guint32 global_id)
|
||||
{
|
||||
g_autofree gchar *global_id_str = NULL;
|
||||
|
||||
g_return_if_fail (WP_IS_COLLECTION (self));
|
||||
g_return_if_fail (global_id != SPA_ID_INVALID);
|
||||
|
||||
global_id_str = g_strdup_printf ("%u", global_id);
|
||||
pw_metadata_set_property (self->iface, 0, global_id_str, NULL, "collected");
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Drops a global ID from the collection.
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the collection object
|
||||
* \param global_id the global ID to drop
|
||||
*/
|
||||
void
|
||||
wp_collection_drop_global (WpCollection * self, guint32 global_id)
|
||||
{
|
||||
g_autofree gchar *global_id_str = NULL;
|
||||
|
||||
g_return_if_fail (WP_IS_COLLECTION (self));
|
||||
g_return_if_fail (global_id != SPA_ID_INVALID);
|
||||
|
||||
global_id_str = g_strdup_printf ("%u", global_id);
|
||||
pw_metadata_set_property (self->iface, 0, global_id_str, NULL, NULL);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Clears all collected global Ids
|
||||
* \ingroup wpcollection
|
||||
* \param self the collection object
|
||||
*/
|
||||
void
|
||||
wp_collection_clear (WpCollection * self)
|
||||
{
|
||||
g_return_if_fail (WP_IS_COLLECTION (self));
|
||||
|
||||
pw_metadata_clear (self->iface);
|
||||
}
|
||||
|
||||
struct collection_iterator_data
|
||||
{
|
||||
WpCollection *collection;
|
||||
const struct item *item;
|
||||
};
|
||||
|
||||
static void
|
||||
collection_iterator_reset (WpIterator *it)
|
||||
{
|
||||
struct collection_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
WpCollection *self = it_data->collection;
|
||||
|
||||
it_data->item = pw_array_first (&self->metadata);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
collection_iterator_next (WpIterator *it, GValue *item)
|
||||
{
|
||||
struct collection_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
WpCollection *self = it_data->collection;
|
||||
|
||||
while (pw_array_check (&self->metadata, it_data->item)) {
|
||||
guint global_id = SPA_ID_INVALID;
|
||||
if (spa_atou32 (it_data->item->key, &global_id, 0)) {
|
||||
g_value_init (item, G_TYPE_UINT);
|
||||
g_value_set_uint (item, global_id);
|
||||
it_data->item++;
|
||||
return TRUE;
|
||||
}
|
||||
it_data->item++;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
collection_iterator_finalize (WpIterator *it)
|
||||
{
|
||||
struct collection_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
g_object_unref (it_data->collection);
|
||||
}
|
||||
|
||||
static const WpIteratorMethods collection_iterator_methods = {
|
||||
.version = WP_ITERATOR_METHODS_VERSION,
|
||||
.reset = collection_iterator_reset,
|
||||
.next = collection_iterator_next,
|
||||
.finalize = collection_iterator_finalize,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Iterates over all the collected global IDs.
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self a collection object
|
||||
* \returns (transfer full): an iterator that iterates over the collected global
|
||||
* IDs. The type of the iterator item is an unsigned integer.
|
||||
*/
|
||||
WpIterator *
|
||||
wp_collection_new_iterator (WpCollection * self)
|
||||
{
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
struct collection_iterator_data *it_data;
|
||||
|
||||
g_return_val_if_fail (WP_IS_COLLECTION (self), NULL);
|
||||
|
||||
it = wp_iterator_new (&collection_iterator_methods,
|
||||
sizeof (struct collection_iterator_data));
|
||||
it_data = wp_iterator_get_user_data (it);
|
||||
it_data->collection = g_object_ref (self);
|
||||
it_data->item = pw_array_first (&self->metadata);
|
||||
return g_steal_pointer (&it);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \struct WpImplCollection
|
||||
* The implementation side of the collection object.
|
||||
*
|
||||
* Activate this object with at least WP_PROXY_FEATURE_BOUND to export it to
|
||||
* PipeWire.
|
||||
*/
|
||||
struct _WpImplCollection
|
||||
{
|
||||
WpProxy parent;
|
||||
|
||||
gchar *name;
|
||||
WpProperties *properties;
|
||||
|
||||
struct pw_metadata *iface;
|
||||
struct pw_impl_metadata *impl;
|
||||
struct spa_hook listener;
|
||||
struct pw_array metadata;
|
||||
};
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_NAME,
|
||||
PROP_PROPERTIES,
|
||||
};
|
||||
|
||||
enum {
|
||||
IMPL_SIGNAL_GLOBAL_COLLECTED,
|
||||
IMPL_SIGNAL_GLOBAL_DROPPED,
|
||||
IMPL_N_SIGNALS,
|
||||
};
|
||||
|
||||
static guint32 impl_signals[IMPL_N_SIGNALS] = {0};
|
||||
|
||||
G_DEFINE_TYPE (WpImplCollection, wp_impl_collection, WP_TYPE_PROXY)
|
||||
|
||||
static void
|
||||
wp_impl_collection_init (WpImplCollection * self)
|
||||
{
|
||||
pw_array_init (&self->metadata, 4096);
|
||||
}
|
||||
|
||||
static int
|
||||
impl_metadata_event_property (void *object, uint32_t subject, const char *key,
|
||||
const char *type, const char *value)
|
||||
{
|
||||
WpImplCollection *self = WP_IMPL_COLLECTION (object);
|
||||
struct item *item = NULL;
|
||||
|
||||
/* Clear subject if key is NULL */
|
||||
if (key == NULL) {
|
||||
while (true) {
|
||||
guint32 global_id = SPA_ID_INVALID;
|
||||
|
||||
item = find_item (&self->metadata, subject, NULL);
|
||||
if (item == NULL)
|
||||
break;
|
||||
|
||||
pw_array_remove (&self->metadata, item);
|
||||
|
||||
if (spa_atou32 (item->key, &global_id, 0))
|
||||
g_signal_emit (self, impl_signals[IMPL_SIGNAL_GLOBAL_DROPPED], 0,
|
||||
global_id);
|
||||
|
||||
clear_item (item);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
item = find_item (&self->metadata, subject, key);
|
||||
if (item == NULL) {
|
||||
if (value == NULL)
|
||||
return 0;
|
||||
item = pw_array_add (&self->metadata, sizeof (*item));
|
||||
if (item == NULL)
|
||||
return -errno;
|
||||
} else {
|
||||
clear_item (item);
|
||||
}
|
||||
|
||||
if (value != NULL) {
|
||||
guint32 global_id = SPA_ID_INVALID;
|
||||
set_item (item, subject, key, type, value);
|
||||
if (spa_atou32 (key, &global_id, 0))
|
||||
g_signal_emit (self, impl_signals[IMPL_SIGNAL_GLOBAL_COLLECTED], 0,
|
||||
global_id);
|
||||
} else {
|
||||
guint32 global_id = SPA_ID_INVALID;
|
||||
pw_array_remove (&self->metadata, item);
|
||||
if (spa_atou32 (key, &global_id, 0))
|
||||
g_signal_emit (self, impl_signals[IMPL_SIGNAL_GLOBAL_DROPPED], 0,
|
||||
global_id);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct pw_impl_metadata_events impl_metadata_events = {
|
||||
PW_VERSION_IMPL_METADATA_EVENTS,
|
||||
.property = impl_metadata_event_property,
|
||||
};
|
||||
|
||||
static void
|
||||
wp_impl_collection_constructed (GObject *object)
|
||||
{
|
||||
WpImplCollection *self = WP_IMPL_COLLECTION (object);
|
||||
g_autoptr (WpCore) core = NULL;
|
||||
struct pw_context *pw_context;
|
||||
struct pw_properties *props;
|
||||
|
||||
core = wp_object_get_core (WP_OBJECT (self));
|
||||
g_return_if_fail (core);
|
||||
pw_context = wp_core_get_pw_context (core);
|
||||
g_return_if_fail (pw_context);
|
||||
|
||||
/* Make sure the collection name and flag is set */
|
||||
if (!self->properties)
|
||||
self->properties = wp_properties_new_empty ();
|
||||
wp_properties_set (self->properties, "collection.name", self->name);
|
||||
wp_properties_set (self->properties, "wireplumber.collection", "true");
|
||||
|
||||
props = wp_properties_to_pw_properties (self->properties);
|
||||
self->impl = pw_context_create_metadata (pw_context, self->name, props , 0);
|
||||
g_return_if_fail (self->impl);
|
||||
self->iface = pw_impl_metadata_get_implementation (self->impl);
|
||||
g_return_if_fail (self->iface);
|
||||
|
||||
pw_impl_metadata_add_listener (self->impl, &self->listener,
|
||||
&impl_metadata_events, self);
|
||||
|
||||
G_OBJECT_CLASS (wp_impl_collection_parent_class)->constructed (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_impl_collection_finalize (GObject * object)
|
||||
{
|
||||
WpImplCollection *self = WP_IMPL_COLLECTION (object);
|
||||
|
||||
pw_array_clear (&self->metadata);
|
||||
spa_hook_remove (&self->listener);
|
||||
g_clear_pointer (&self->impl, pw_impl_metadata_destroy);
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
g_clear_pointer (&self->name, g_free);
|
||||
|
||||
G_OBJECT_CLASS (wp_impl_collection_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_impl_collection_set_property (GObject * object, guint property_id,
|
||||
const GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
WpImplCollection *self = WP_IMPL_COLLECTION (object);
|
||||
|
||||
switch (property_id) {
|
||||
case PROP_NAME:
|
||||
g_clear_pointer (&self->name, g_free);
|
||||
self->name = g_value_dup_string (value);
|
||||
break;
|
||||
case PROP_PROPERTIES:
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
self->properties = g_value_dup_boxed (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
wp_impl_collection_get_property (GObject * object, guint property_id,
|
||||
GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
WpImplCollection *self = WP_IMPL_COLLECTION (object);
|
||||
|
||||
switch (property_id) {
|
||||
case PROP_NAME:
|
||||
g_value_set_string (value, self->name);
|
||||
break;
|
||||
case PROP_PROPERTIES:
|
||||
g_value_set_boxed (value, self->properties);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
enum {
|
||||
STEP_EXPORT = WP_TRANSITION_STEP_CUSTOM_START,
|
||||
};
|
||||
|
||||
static WpObjectFeatures
|
||||
wp_impl_collection_get_supported_features (WpObject * object)
|
||||
{
|
||||
return WP_PROXY_FEATURE_BOUND;
|
||||
}
|
||||
|
||||
static guint
|
||||
wp_impl_collection_activate_get_next_step (WpObject * object,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
g_return_val_if_fail (missing & (WP_PROXY_FEATURE_BOUND),
|
||||
WP_TRANSITION_STEP_ERROR);
|
||||
|
||||
return STEP_EXPORT;
|
||||
}
|
||||
|
||||
static void
|
||||
wp_impl_collection_activate_execute_step (WpObject * object,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
WpImplCollection *self = WP_IMPL_COLLECTION (object);
|
||||
|
||||
switch (step) {
|
||||
case STEP_EXPORT: {
|
||||
g_autoptr (WpCore) core = wp_object_get_core (object);
|
||||
struct pw_core *pw_core = wp_core_get_pw_core (core);
|
||||
const struct pw_properties *props = NULL;
|
||||
|
||||
/* no pw_core -> we are not connected */
|
||||
if (!pw_core) {
|
||||
wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
|
||||
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
||||
"The WirePlumber core is not connected; "
|
||||
"object cannot be exported to PipeWire"));
|
||||
return;
|
||||
}
|
||||
|
||||
props = pw_impl_metadata_get_properties (self->impl);
|
||||
wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
|
||||
PW_TYPE_INTERFACE_Metadata, &props->dict, self->iface, 0));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
WP_OBJECT_CLASS (wp_impl_collection_parent_class)->
|
||||
activate_execute_step (object, transition, step, missing);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
wp_impl_collection_class_init (WpImplCollectionClass * klass)
|
||||
{
|
||||
GObjectClass *object_class = (GObjectClass *) klass;
|
||||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
||||
WpProxyClass *proxy_class = (WpProxyClass *) klass;
|
||||
|
||||
object_class->constructed = wp_impl_collection_constructed;
|
||||
object_class->finalize = wp_impl_collection_finalize;
|
||||
object_class->set_property = wp_impl_collection_set_property;
|
||||
object_class->get_property = wp_impl_collection_get_property;
|
||||
|
||||
wpobject_class->get_supported_features =
|
||||
wp_impl_collection_get_supported_features;
|
||||
wpobject_class->activate_get_next_step =
|
||||
wp_impl_collection_activate_get_next_step;
|
||||
wpobject_class->activate_execute_step =
|
||||
wp_impl_collection_activate_execute_step;
|
||||
|
||||
proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Metadata;
|
||||
proxy_class->pw_iface_version = PW_VERSION_METADATA;
|
||||
|
||||
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));
|
||||
|
||||
g_object_class_install_property (object_class, PROP_PROPERTIES,
|
||||
g_param_spec_boxed ("properties", "properties",
|
||||
"The collection properties", WP_TYPE_PROPERTIES,
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
impl_signals[IMPL_SIGNAL_GLOBAL_COLLECTED] = g_signal_new ("global-collected",
|
||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||
G_TYPE_NONE, 1, G_TYPE_UINT);
|
||||
|
||||
impl_signals[IMPL_SIGNAL_GLOBAL_DROPPED] = g_signal_new ("global-dropped",
|
||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||
G_TYPE_NONE, 1, G_TYPE_UINT);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Creates a new impl collection
|
||||
* \ingroup wpcollection
|
||||
* \param core the core
|
||||
* \param name (nullable): the collection name
|
||||
* \param properties (nullable) (transfer full): the collection properties
|
||||
* \returns (transfer full): a new WpImplCollection
|
||||
*/
|
||||
WpImplCollection *
|
||||
wp_impl_collection_new (WpCore * core, const gchar *name,
|
||||
WpProperties *properties)
|
||||
{
|
||||
g_autoptr (WpProperties) props = properties;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CORE (core), NULL);
|
||||
|
||||
return g_object_new (WP_TYPE_IMPL_COLLECTION,
|
||||
"core", core,
|
||||
"name", name,
|
||||
"properties", props,
|
||||
NULL);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the properties of the impl collection
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
\returns (transfer full) (nullable): the properties of the impl collection
|
||||
*/
|
||||
WpProperties *
|
||||
wp_impl_collection_get_properties (WpImplCollection *self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), NULL);
|
||||
|
||||
return self->properties ? wp_properties_ref (self->properties) : NULL;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the name of the impl collection
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
* \returns (nullable): the name of the impl collection
|
||||
*/
|
||||
const gchar *
|
||||
wp_impl_collection_get_name (WpImplCollection *self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), NULL);
|
||||
|
||||
return self->name;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the total number of globals the impl collection has collected
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
* \returns the total number of globals the impl collection has collected
|
||||
*/
|
||||
gsize
|
||||
wp_impl_collection_get_size (WpImplCollection * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), 0);
|
||||
|
||||
return pw_array_get_len (&self->metadata, struct item);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks if a global ID is collected in the impl collection
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
* \param global_id the global ID to check
|
||||
* \returns TRUE if the global ID is collected in the colleciton, FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
gboolean
|
||||
wp_impl_collection_contains_global (WpImplCollection * self, guint32 global_id)
|
||||
{
|
||||
struct item *item;
|
||||
|
||||
g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), FALSE);
|
||||
|
||||
pw_array_for_each (item, &self->metadata) {
|
||||
guint32 id = SPA_ID_INVALID;
|
||||
if (spa_atou32 (item->key, &id, 0) && id == global_id)
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Collects a global ID into the impl collection.
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
* \param global_id the global ID to collect
|
||||
*/
|
||||
void
|
||||
wp_impl_collection_collect_global (WpImplCollection * self, guint32 global_id)
|
||||
{
|
||||
g_autofree gchar *global_id_str = NULL;
|
||||
|
||||
g_return_if_fail (WP_IS_IMPL_COLLECTION (self));
|
||||
g_return_if_fail (global_id != SPA_ID_INVALID);
|
||||
|
||||
global_id_str = g_strdup_printf ("%u", global_id);
|
||||
pw_metadata_set_property (self->iface, 0, global_id_str, NULL, "collected");
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Drops a global ID from the impl collection.
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
* \param global_id the global ID to drop
|
||||
*/
|
||||
void
|
||||
wp_impl_collection_drop_global (WpImplCollection * self, guint32 global_id)
|
||||
{
|
||||
g_autofree gchar *global_id_str = NULL;
|
||||
|
||||
g_return_if_fail (WP_IS_IMPL_COLLECTION (self));
|
||||
g_return_if_fail (global_id != SPA_ID_INVALID);
|
||||
|
||||
global_id_str = g_strdup_printf ("%u", global_id);
|
||||
pw_metadata_set_property (self->iface, 0, global_id_str, NULL, NULL);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Clears all collected global Ids
|
||||
* \ingroup wpcollection
|
||||
* \param self the impl collection object
|
||||
*/
|
||||
void
|
||||
wp_impl_collection_clear (WpImplCollection * self)
|
||||
{
|
||||
g_return_if_fail (WP_IS_IMPL_COLLECTION (self));
|
||||
|
||||
pw_metadata_clear (self->iface);
|
||||
}
|
||||
|
||||
struct impl_collection_iterator_data
|
||||
{
|
||||
WpImplCollection *impl_collection;
|
||||
const struct item *item;
|
||||
};
|
||||
|
||||
static void
|
||||
impl_collection_iterator_reset (WpIterator *it)
|
||||
{
|
||||
struct impl_collection_iterator_data *it_data =
|
||||
wp_iterator_get_user_data (it);
|
||||
WpImplCollection *self = it_data->impl_collection;
|
||||
|
||||
it_data->item = pw_array_first (&self->metadata);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
impl_collection_iterator_next (WpIterator *it, GValue *item)
|
||||
{
|
||||
struct impl_collection_iterator_data *it_data =
|
||||
wp_iterator_get_user_data (it);
|
||||
WpImplCollection *self = it_data->impl_collection;
|
||||
|
||||
while (pw_array_check (&self->metadata, it_data->item)) {
|
||||
guint global_id = SPA_ID_INVALID;
|
||||
if (spa_atou32 (it_data->item->key, &global_id, 0)) {
|
||||
g_value_init (item, G_TYPE_UINT);
|
||||
g_value_set_uint (item, global_id);
|
||||
it_data->item++;
|
||||
return TRUE;
|
||||
}
|
||||
it_data->item++;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
impl_collection_iterator_finalize (WpIterator *it)
|
||||
{
|
||||
struct impl_collection_iterator_data *it_data =
|
||||
wp_iterator_get_user_data (it);
|
||||
g_object_unref (it_data->impl_collection);
|
||||
}
|
||||
|
||||
static const WpIteratorMethods impl_collection_iterator_methods = {
|
||||
.version = WP_ITERATOR_METHODS_VERSION,
|
||||
.reset = impl_collection_iterator_reset,
|
||||
.next = impl_collection_iterator_next,
|
||||
.finalize = impl_collection_iterator_finalize,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Iterates over all the collected global IDs.
|
||||
*
|
||||
* \ingroup wpcollection
|
||||
* \param self an impl collection object
|
||||
* \returns (transfer full): an iterator that iterates over the collected global
|
||||
* IDs. The type of the iterator item is an unsigned integer.
|
||||
*/
|
||||
WpIterator *
|
||||
wp_impl_collection_new_iterator (WpImplCollection * self)
|
||||
{
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
struct impl_collection_iterator_data *it_data;
|
||||
|
||||
g_return_val_if_fail (WP_IS_IMPL_COLLECTION (self), NULL);
|
||||
|
||||
it = wp_iterator_new (&impl_collection_iterator_methods,
|
||||
sizeof (struct impl_collection_iterator_data));
|
||||
it_data = wp_iterator_get_user_data (it);
|
||||
it_data->impl_collection = g_object_ref (self);
|
||||
it_data->item = pw_array_first (&self->metadata);
|
||||
return g_steal_pointer (&it);
|
||||
}
|
||||
102
lib/wp/collection.h
Normal file
102
lib/wp/collection.h
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2026 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef __WIREPLUMBER_COLLECTION_H__
|
||||
#define __WIREPLUMBER_COLLECTION_H__
|
||||
|
||||
#include "global-proxy.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief An extension of WpProxyFeatures for WpCollection objects
|
||||
* \ingroup wpcollection
|
||||
*/
|
||||
typedef enum { /*< flags >*/
|
||||
/*! caches collection data locally */
|
||||
WP_COLLECTION_FEATURE_DATA = (WP_PROXY_FEATURE_CUSTOM_START << 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,
|
||||
WpGlobalProxy)
|
||||
|
||||
WP_API
|
||||
const gchar *wp_collection_get_name (WpCollection * self);
|
||||
|
||||
WP_API
|
||||
gsize wp_collection_get_size (WpCollection * self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_collection_contains_global (WpCollection * self, guint32 global_id);
|
||||
|
||||
WP_API
|
||||
void wp_collection_collect_global (WpCollection * self, guint32 global_id);
|
||||
|
||||
WP_API
|
||||
void wp_collection_drop_global (WpCollection * self, guint32 global_id);
|
||||
|
||||
WP_API
|
||||
void wp_collection_clear (WpCollection * self);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_collection_new_iterator (WpCollection * self);
|
||||
|
||||
|
||||
/* WpImplCollection */
|
||||
|
||||
/*!
|
||||
* \brief The WpImplCollection GType
|
||||
* \ingroup wpcollection
|
||||
*/
|
||||
#define WP_TYPE_IMPL_COLLECTION (wp_impl_collection_get_type ())
|
||||
|
||||
WP_API
|
||||
G_DECLARE_FINAL_TYPE (WpImplCollection, wp_impl_collection, WP, IMPL_COLLECTION,
|
||||
WpProxy)
|
||||
|
||||
WP_API
|
||||
WpImplCollection * wp_impl_collection_new (WpCore * core, const gchar *name,
|
||||
WpProperties *properties);
|
||||
|
||||
WP_API
|
||||
WpProperties * wp_impl_collection_get_properties (WpImplCollection *self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_impl_collection_get_name (WpImplCollection *self);
|
||||
|
||||
WP_API
|
||||
gsize wp_impl_collection_get_size (WpImplCollection * self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_impl_collection_contains_global (WpImplCollection * self,
|
||||
guint32 global_id);
|
||||
|
||||
WP_API
|
||||
void wp_impl_collection_collect_global (WpImplCollection * self,
|
||||
guint32 global_id);
|
||||
|
||||
WP_API
|
||||
void wp_impl_collection_drop_global (WpImplCollection * self,
|
||||
guint32 global_id);
|
||||
|
||||
WP_API
|
||||
void wp_impl_collection_clear (WpImplCollection * self);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_impl_collection_new_iterator (WpImplCollection * self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
#include "global-proxy.h"
|
||||
#include "collection.h"
|
||||
#include "private/registry.h"
|
||||
#include "core.h"
|
||||
#include "error.h"
|
||||
|
|
@ -43,6 +44,8 @@ struct _WpGlobalProxyPrivate
|
|||
WpGlobal *global;
|
||||
gchar factory_name[96];
|
||||
WpProperties *properties;
|
||||
GWeakRef collection;
|
||||
guint32 collected_id;
|
||||
};
|
||||
|
||||
enum {
|
||||
|
|
@ -51,6 +54,7 @@ enum {
|
|||
PROP_FACTORY_NAME,
|
||||
PROP_GLOBAL_PROPERTIES,
|
||||
PROP_PERMISSIONS,
|
||||
PROP_COLLECTION_NAME,
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE_WITH_PRIVATE (WpGlobalProxy, wp_global_proxy, WP_TYPE_PROXY)
|
||||
|
|
@ -58,6 +62,11 @@ G_DEFINE_TYPE_WITH_PRIVATE (WpGlobalProxy, wp_global_proxy, WP_TYPE_PROXY)
|
|||
static void
|
||||
wp_global_proxy_init (WpGlobalProxy * self)
|
||||
{
|
||||
WpGlobalProxyPrivate *priv =
|
||||
wp_global_proxy_get_instance_private (self);
|
||||
|
||||
g_weak_ref_init (&priv->collection, NULL);
|
||||
priv->collected_id = SPA_ID_INVALID;
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -67,6 +76,8 @@ wp_global_proxy_dispose (GObject * object)
|
|||
WpGlobalProxyPrivate *priv =
|
||||
wp_global_proxy_get_instance_private (self);
|
||||
|
||||
wp_global_proxy_attach_collection (self, NULL);
|
||||
|
||||
if (priv->global)
|
||||
wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
||||
|
||||
|
|
@ -80,6 +91,7 @@ wp_global_proxy_finalize (GObject * object)
|
|||
WpGlobalProxyPrivate *priv =
|
||||
wp_global_proxy_get_instance_private (self);
|
||||
|
||||
g_weak_ref_clear (&priv->collection);
|
||||
g_clear_pointer (&priv->properties, wp_properties_unref);
|
||||
g_clear_pointer (&priv->global, wp_global_unref);
|
||||
|
||||
|
|
@ -125,6 +137,9 @@ wp_global_proxy_get_property (GObject * object, guint property_id,
|
|||
case PROP_GLOBAL_PROPERTIES:
|
||||
g_value_take_boxed (value, wp_global_proxy_get_global_properties (self));
|
||||
break;
|
||||
case PROP_COLLECTION_NAME:
|
||||
g_value_set_string (value, wp_global_proxy_get_collection_name (self));
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
break;
|
||||
|
|
@ -250,6 +265,8 @@ wp_global_proxy_destroyed (WpProxy * proxy)
|
|||
WpGlobalProxyPrivate *priv =
|
||||
wp_global_proxy_get_instance_private (self);
|
||||
|
||||
wp_global_proxy_attach_collection (self, NULL);
|
||||
|
||||
if (priv->global && priv->global->proxy &&
|
||||
(priv->global->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY)) {
|
||||
/* We can end up here as a result of _request_destroy() followed by
|
||||
|
|
@ -308,6 +325,11 @@ wp_global_proxy_class_init (WpGlobalProxyClass * klass)
|
|||
g_param_spec_uint ("permissions", "permissions",
|
||||
"The pipewire global permissions", 0, G_MAXUINT, 0,
|
||||
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (object_class, PROP_COLLECTION_NAME,
|
||||
g_param_spec_string ("collection-name", "collection-name",
|
||||
"The collection name this global belongs to", NULL,
|
||||
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
@ -420,3 +442,97 @@ wp_global_proxy_bind (WpGlobalProxy * self)
|
|||
wp_proxy_set_pw_proxy (WP_PROXY (self), p);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_dropped (WpCollection *obj, guint32 global_id,
|
||||
WpGlobalProxy *self)
|
||||
{
|
||||
WpGlobalProxyPrivate *priv;
|
||||
|
||||
priv = wp_global_proxy_get_instance_private (self);
|
||||
|
||||
/* If this global was dropped from the collection, detach proxy from it */
|
||||
if (global_id == priv->collected_id) {
|
||||
priv->collected_id = SPA_ID_INVALID;
|
||||
g_weak_ref_set (&priv->collection, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Attaches a collection in the global proxy
|
||||
*
|
||||
* \ingroup wpglobalproxy
|
||||
* \param self the pipewire global
|
||||
* \param collection (transfer none) (nullable): the collection to attach, or
|
||||
* NULL to detach the current collection.
|
||||
* \returns TRUE if the collection was attached and the global was collected,
|
||||
* FALSE otherwise.
|
||||
*/
|
||||
gboolean
|
||||
wp_global_proxy_attach_collection (WpGlobalProxy *self,
|
||||
WpCollection *collection)
|
||||
{
|
||||
WpGlobalProxyPrivate *priv;
|
||||
g_autoptr (WpCollection) curr_collection = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), FALSE);
|
||||
|
||||
priv = wp_global_proxy_get_instance_private (self);
|
||||
|
||||
/* Dont do anything if this collection is already attached */
|
||||
curr_collection = g_weak_ref_get (&priv->collection);
|
||||
if (curr_collection == collection)
|
||||
return TRUE;
|
||||
|
||||
/* Make sure the global proxy is bound before attaching a new collection */
|
||||
if (collection &&
|
||||
!(wp_object_get_active_features (WP_OBJECT (self)) &
|
||||
WP_PROXY_FEATURE_BOUND)) {
|
||||
wp_warning_object (self,
|
||||
"global proxy %p is not bound, cannot attach collection", self);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Drop this global from the old collection if any */
|
||||
if (curr_collection) {
|
||||
g_signal_handlers_disconnect_by_data (curr_collection, self);
|
||||
wp_collection_drop_global (curr_collection, priv->collected_id);
|
||||
priv->collected_id = SPA_ID_INVALID;
|
||||
g_weak_ref_set (&priv->collection, NULL);
|
||||
}
|
||||
|
||||
/* Collect this global into the new collection if any */
|
||||
if (collection) {
|
||||
priv->collected_id = wp_proxy_get_bound_id (WP_PROXY (self));
|
||||
g_weak_ref_set (&priv->collection, collection);
|
||||
wp_collection_collect_global (collection, priv->collected_id);
|
||||
g_signal_connect_object (collection, "global-dropped",
|
||||
G_CALLBACK (on_global_dropped), self, 0);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the collection naem this global proxy is attached to
|
||||
*
|
||||
* \ingroup wpglobalproxy
|
||||
* \param self the pipewire global
|
||||
* \returns (nullable): the collection name this global proxy is attached to
|
||||
*/
|
||||
const gchar *
|
||||
wp_global_proxy_get_collection_name (WpGlobalProxy *self)
|
||||
{
|
||||
WpGlobalProxyPrivate *priv;
|
||||
g_autoptr (WpCollection) c = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), NULL);
|
||||
|
||||
priv = wp_global_proxy_get_instance_private (self);
|
||||
|
||||
c = g_weak_ref_get (&priv->collection);
|
||||
if (!c)
|
||||
return NULL;
|
||||
|
||||
return wp_collection_get_name (c);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct _WpCollection WpCollection;
|
||||
|
||||
/*!
|
||||
* \brief The WpGlobalProxy GType
|
||||
* \ingroup wpglobalproxy
|
||||
|
|
@ -44,6 +46,13 @@ WpProperties * wp_global_proxy_get_global_properties (
|
|||
WP_API
|
||||
gboolean wp_global_proxy_bind (WpGlobalProxy * self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_global_proxy_attach_collection (WpGlobalProxy *self,
|
||||
WpCollection *collection);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_global_proxy_get_collection_name (WpGlobalProxy *self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ wp_lib_sources = files(
|
|||
'component-loader.c',
|
||||
'conf.c',
|
||||
'core.c',
|
||||
'collection.c',
|
||||
'device.c',
|
||||
'error.c',
|
||||
'event.c',
|
||||
|
|
@ -52,6 +53,7 @@ wp_lib_headers = files(
|
|||
'component-loader.h',
|
||||
'conf.h',
|
||||
'core.h',
|
||||
'collection.h',
|
||||
'defs.h',
|
||||
'device.h',
|
||||
'error.h',
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@
|
|||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <pipewire/impl.h>
|
||||
|
||||
#include "registry.h"
|
||||
#include "object-manager.h"
|
||||
#include "log.h"
|
||||
#include "collection.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-registry")
|
||||
|
||||
|
|
@ -73,16 +76,26 @@ object_manager_destroyed (gpointer data, GObject * om)
|
|||
/* find the subclass of WpPipewireGloabl that can handle
|
||||
the given pipewire interface type of the given version */
|
||||
static inline GType
|
||||
find_proxy_instance_type (const char * type, guint32 version)
|
||||
find_proxy_instance_type (const char * type, guint32 version,
|
||||
const struct spa_dict *props)
|
||||
{
|
||||
g_autofree GType *children;
|
||||
g_autofree GType *children = NULL;
|
||||
guint n_children;
|
||||
|
||||
/* Check if this is a collection */
|
||||
if (g_str_equal (type, PW_TYPE_INTERFACE_Metadata) &&
|
||||
version == PW_VERSION_METADATA &&
|
||||
props && spa_atob (spa_dict_lookup (props, "wireplumber.collection"))) {
|
||||
return WP_TYPE_COLLECTION;
|
||||
}
|
||||
|
||||
/* Otherwise find the matching proxy non-collection type */
|
||||
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
|
||||
|
||||
for (guint i = 0; i < n_children; i++) {
|
||||
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
|
||||
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
|
||||
if (children[i] != WP_TYPE_COLLECTION &&
|
||||
g_strcmp0 (klass->pw_iface_type, type) == 0 &&
|
||||
klass->pw_iface_version == version) {
|
||||
g_type_class_unref (klass);
|
||||
return children[i];
|
||||
|
|
@ -100,7 +113,7 @@ registry_global (void *data, uint32_t id, uint32_t permissions,
|
|||
const char *type, uint32_t version, const struct spa_dict *props)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
GType gtype = find_proxy_instance_type (type, version);
|
||||
GType gtype = find_proxy_instance_type (type, version, props);
|
||||
|
||||
wp_debug_object (wp_registry_get_core (self),
|
||||
"global:%u perm:0x%x type:%s/%u -> %s",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ wp_init (WpInitFlags flags)
|
|||
g_type_ensure (WP_TYPE_NODE);
|
||||
g_type_ensure (WP_TYPE_PORT);
|
||||
g_type_ensure (WP_TYPE_FACTORY);
|
||||
g_type_ensure (WP_TYPE_COLLECTION);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "component-loader.h"
|
||||
#include "conf.h"
|
||||
#include "core.h"
|
||||
#include "collection.h"
|
||||
#include "device.h"
|
||||
#include "error.h"
|
||||
#include "event-dispatcher.h"
|
||||
|
|
|
|||
|
|
@ -568,6 +568,15 @@ static const luaL_Reg proxy_methods[] = {
|
|||
|
||||
/* WpGlobalProxy */
|
||||
|
||||
static int
|
||||
global_proxy_get_global_properties (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy * p = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
WpProperties * props = wp_global_proxy_get_global_properties (p);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
global_proxy_request_destroy (lua_State *L)
|
||||
{
|
||||
|
|
@ -576,8 +585,29 @@ global_proxy_request_destroy (lua_State *L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
global_proxy_attach_collection (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy * p = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
WpCollection * c = luaL_opt (L, wplua_toobject, 2, NULL);
|
||||
wp_global_proxy_attach_collection (p, c);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
global_proxy_get_collection_name (lua_State *L)
|
||||
{
|
||||
WpGlobalProxy * p = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
|
||||
const gchar *collection_name = wp_global_proxy_get_collection_name (p);
|
||||
lua_pushstring (L, collection_name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const luaL_Reg global_proxy_methods[] = {
|
||||
{ "get_global_properties", global_proxy_get_global_properties },
|
||||
{ "request_destroy", global_proxy_request_destroy },
|
||||
{ "attach_collection", global_proxy_attach_collection },
|
||||
{ "get_collection_name", global_proxy_get_collection_name },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
|
|
@ -2179,6 +2209,166 @@ 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_size (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
lua_pushinteger (L, wp_collection_get_size (c));
|
||||
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);
|
||||
guint32 global_id = wp_proxy_get_bound_id (WP_PROXY (g));
|
||||
lua_pushboolean (L, wp_collection_contains_global (c, global_id));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_collect_global (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
WpGlobalProxy *g = wplua_checkobject (L, 2, WP_TYPE_GLOBAL_PROXY);
|
||||
guint32 global_id = wp_proxy_get_bound_id (WP_PROXY (g));
|
||||
wp_collection_collect_global (c, global_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_drop_global (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
WpGlobalProxy *g = wplua_checkobject (L, 2, WP_TYPE_GLOBAL_PROXY);
|
||||
guint32 global_id = wp_proxy_get_bound_id (WP_PROXY (g));
|
||||
wp_collection_drop_global (c, global_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
collection_iterate (lua_State *L)
|
||||
{
|
||||
WpCollection *c = wplua_checkobject (L, 1, WP_TYPE_COLLECTION);
|
||||
WpIterator *it = wp_collection_new_iterator (c);
|
||||
return push_wpiterator (L, it);
|
||||
}
|
||||
|
||||
static const luaL_Reg collection_funcs[] = {
|
||||
{ "get_name", collection_get_name },
|
||||
{ "get_size", collection_get_size },
|
||||
{ "contains_global", collection_contains_global },
|
||||
{ "collect_global", collection_collect_global },
|
||||
{ "drop_global", collection_drop_global },
|
||||
{ "iterate", collection_iterate },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
|
||||
/* WpImplCollection */
|
||||
|
||||
static int
|
||||
impl_collection_new (lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring (L, 1);
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
|
||||
if (lua_istable (L, 2))
|
||||
props = wplua_table_to_properties (L, 2);
|
||||
else if (!lua_isnone (L, 2) && !lua_isnil (L, 2))
|
||||
props = wp_properties_ref (wplua_checkboxed (L, 2, WP_TYPE_PROPERTIES));
|
||||
else
|
||||
props = wp_properties_new_empty ();
|
||||
|
||||
wplua_pushobject (L, wp_impl_collection_new (get_wp_export_core (L),
|
||||
name, wp_properties_ref (props)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_get_properties (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
WpProperties *props = wp_impl_collection_get_properties (c);
|
||||
wplua_pushboxed (L, WP_TYPE_PROPERTIES, props);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_get_name (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
lua_pushstring (L, wp_impl_collection_get_name (c));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_get_size (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
lua_pushinteger (L, wp_impl_collection_get_size (c));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_contains_global (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
WpGlobalProxy *g = wplua_checkobject (L, 2, WP_TYPE_GLOBAL_PROXY);
|
||||
guint32 global_id = wp_proxy_get_bound_id (WP_PROXY (g));
|
||||
lua_pushboolean (L, wp_impl_collection_contains_global (c, global_id));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_collect_global (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
guint32 global_id = luaL_checkinteger (L, 2);
|
||||
wp_impl_collection_collect_global (c, global_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_drop_global (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
guint32 global_id = luaL_checkinteger (L, 2);
|
||||
wp_impl_collection_drop_global (c, global_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
impl_collection_iterate (lua_State *L)
|
||||
{
|
||||
WpImplCollection *c = wplua_checkobject (L, 1, WP_TYPE_IMPL_COLLECTION);
|
||||
WpIterator *it = wp_impl_collection_new_iterator (c);
|
||||
return push_wpiterator (L, it);
|
||||
}
|
||||
|
||||
static const luaL_Reg impl_collection_funcs[] = {
|
||||
{ "get_properties", impl_collection_get_properties },
|
||||
{ "get_name", impl_collection_get_name },
|
||||
{ "get_size", impl_collection_get_size },
|
||||
{ "contains_global", impl_collection_contains_global },
|
||||
{ "collect_global", impl_collection_collect_global },
|
||||
{ "drop_global", impl_collection_drop_global },
|
||||
{ "iterate", impl_collection_iterate },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
|
||||
/* WpSettings */
|
||||
|
||||
static int
|
||||
|
|
@ -3284,6 +3474,10 @@ wp_lua_scripting_api_init (lua_State *L)
|
|||
properties_new, properties_funcs);
|
||||
wplua_register_type_methods (L, WP_TYPE_PERMISSION_MANAGER,
|
||||
permission_manager_new, permission_manager_funcs);
|
||||
wplua_register_type_methods (L, WP_TYPE_COLLECTION,
|
||||
NULL, collection_funcs);
|
||||
wplua_register_type_methods (L, WP_TYPE_IMPL_COLLECTION,
|
||||
impl_collection_new, impl_collection_funcs);
|
||||
|
||||
if (!wplua_load_uri (L, URI_API, &error) ||
|
||||
!wplua_pcall (L, 0, 0, &error)) {
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ SANDBOX_EXPORT = {
|
|||
State = WpState_new,
|
||||
LocalModule = WpImplModule_new,
|
||||
ImplMetadata = WpImplMetadata_new,
|
||||
ImplCollection = WpImplCollection_new,
|
||||
Settings = WpSettings,
|
||||
Conf = WpConf,
|
||||
JsonUtils = JsonUtils,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ typedef enum {
|
|||
OBJECT_TYPE_CLIENT,
|
||||
OBJECT_TYPE_DEVICE,
|
||||
OBJECT_TYPE_METADATA,
|
||||
OBJECT_TYPE_COLLECTION,
|
||||
N_OBJECT_TYPES,
|
||||
OBJECT_TYPE_INVALID = N_OBJECT_TYPES
|
||||
} ObjectType;
|
||||
|
|
@ -65,6 +66,7 @@ rescan_context_get_type (void)
|
|||
struct _WpStandardEventSource
|
||||
{
|
||||
WpPlugin parent;
|
||||
WpObjectManager *globals_om;
|
||||
WpObjectManager *oms[N_OBJECT_TYPES];
|
||||
WpEventHook *rescan_done_hook;
|
||||
gboolean rescan_scheduled[N_RESCAN_CONTEXTS];
|
||||
|
|
@ -93,6 +95,7 @@ object_type_to_gtype (ObjectType type)
|
|||
case OBJECT_TYPE_CLIENT: return WP_TYPE_CLIENT;
|
||||
case OBJECT_TYPE_DEVICE: return WP_TYPE_DEVICE;
|
||||
case OBJECT_TYPE_METADATA: return WP_TYPE_METADATA;
|
||||
case OBJECT_TYPE_COLLECTION: return WP_TYPE_COLLECTION;
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
|
@ -115,6 +118,8 @@ type_str_to_object_type (const gchar * type_str)
|
|||
return OBJECT_TYPE_DEVICE;
|
||||
else if (!g_strcmp0 (type_str, "metadata"))
|
||||
return OBJECT_TYPE_METADATA;
|
||||
else if (!g_strcmp0 (type_str, "collection"))
|
||||
return OBJECT_TYPE_COLLECTION;
|
||||
else
|
||||
return OBJECT_TYPE_INVALID;
|
||||
}
|
||||
|
|
@ -148,6 +153,8 @@ get_object_type (gpointer obj, WpProperties **properties)
|
|||
return "device";
|
||||
else if (WP_IS_METADATA (obj))
|
||||
return "metadata";
|
||||
else if (WP_IS_COLLECTION (obj))
|
||||
return "collection";
|
||||
|
||||
wp_debug_object (obj, "Unknown global proxy type");
|
||||
return G_OBJECT_TYPE_NAME (obj);
|
||||
|
|
@ -324,6 +331,50 @@ on_metadata_changed (WpMetadata *obj, guint32 subject,
|
|||
wp_standard_event_source_push_event (self, "changed", obj, properties);
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_collected (WpCollection *obj, guint32 global_id,
|
||||
WpStandardEventSource *self)
|
||||
{
|
||||
g_autoptr (WpGlobalProxy) global = NULL;
|
||||
const gchar *collection_name = NULL;
|
||||
g_autoptr (WpProperties) properties = NULL;
|
||||
|
||||
global = wp_object_manager_lookup (self->globals_om, WP_TYPE_GLOBAL_PROXY,
|
||||
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", global_id, NULL);
|
||||
if (!global) {
|
||||
wp_info_object (self, "could not find collected global %u", global_id);
|
||||
return;
|
||||
}
|
||||
|
||||
collection_name = wp_collection_get_name (obj);
|
||||
|
||||
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 (WpCollection *obj, guint32 global_id,
|
||||
WpStandardEventSource *self)
|
||||
{
|
||||
g_autoptr (WpGlobalProxy) global = NULL;
|
||||
const gchar *collection_name = NULL;
|
||||
g_autoptr (WpProperties) properties = NULL;
|
||||
|
||||
global = wp_object_manager_lookup (self->globals_om, WP_TYPE_GLOBAL_PROXY,
|
||||
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", global_id, NULL);
|
||||
if (!global) {
|
||||
wp_info_object (self, "could not find dropped global %u", global_id);
|
||||
return;
|
||||
}
|
||||
|
||||
collection_name = wp_collection_get_name (obj);
|
||||
|
||||
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_params_changed (WpPipewireObject *obj, const gchar *id,
|
||||
WpStandardEventSource *self)
|
||||
|
|
@ -368,6 +419,12 @@ on_object_added (WpObjectManager *om, WpObject *obj, WpStandardEventSource *self
|
|||
g_signal_connect_object (obj, "changed",
|
||||
G_CALLBACK (on_metadata_changed), self, 0);
|
||||
}
|
||||
else if (WP_IS_COLLECTION (obj)) {
|
||||
g_signal_connect_object (obj, "global-collected",
|
||||
G_CALLBACK (on_global_collected), self, 0);
|
||||
g_signal_connect_object (obj, "global-dropped",
|
||||
G_CALLBACK (on_global_dropped), self, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -383,6 +440,29 @@ on_om_installed (WpObjectManager * om, WpStandardEventSource * self)
|
|||
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_om_installed (WpObjectManager * om, WpStandardEventSource * self)
|
||||
{
|
||||
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
||||
|
||||
/* install object specific object managers */
|
||||
self->n_oms_installed = 0;
|
||||
for (gint i = 0; i < N_OBJECT_TYPES; i++) {
|
||||
GType gtype = object_type_to_gtype (i);
|
||||
self->oms[i] = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->oms[i], gtype, NULL);
|
||||
wp_object_manager_request_object_features (self->oms[i],
|
||||
gtype, WP_OBJECT_FEATURES_ALL);
|
||||
g_signal_connect_object (self->oms[i], "object-added",
|
||||
G_CALLBACK (on_object_added), self, 0);
|
||||
g_signal_connect_object (self->oms[i], "object-removed",
|
||||
G_CALLBACK (on_object_removed), self, 0);
|
||||
g_signal_connect_object (self->oms[i], "installed",
|
||||
G_CALLBACK (on_om_installed), self, 0);
|
||||
wp_core_install_object_manager (core, self->oms[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_rescan_done (WpEvent * event, WpStandardEventSource * self)
|
||||
{
|
||||
|
|
@ -408,22 +488,14 @@ wp_standard_event_source_enable (WpPlugin * plugin, WpTransition * transition)
|
|||
wp_event_dispatcher_get_instance (core);
|
||||
g_return_if_fail (dispatcher);
|
||||
|
||||
/* install object managers */
|
||||
self->n_oms_installed = 0;
|
||||
for (gint i = 0; i < N_OBJECT_TYPES; i++) {
|
||||
GType gtype = object_type_to_gtype (i);
|
||||
self->oms[i] = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->oms[i], gtype, NULL);
|
||||
wp_object_manager_request_object_features (self->oms[i],
|
||||
gtype, WP_OBJECT_FEATURES_ALL);
|
||||
g_signal_connect_object (self->oms[i], "object-added",
|
||||
G_CALLBACK (on_object_added), self, 0);
|
||||
g_signal_connect_object (self->oms[i], "object-removed",
|
||||
G_CALLBACK (on_object_removed), self, 0);
|
||||
g_signal_connect_object (self->oms[i], "installed",
|
||||
G_CALLBACK (on_om_installed), self, 0);
|
||||
wp_core_install_object_manager (core, self->oms[i]);
|
||||
}
|
||||
/* Install global object manager */
|
||||
self->globals_om = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->globals_om, WP_TYPE_GLOBAL_PROXY, NULL);
|
||||
wp_object_manager_request_object_features (self->globals_om,
|
||||
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
|
||||
g_signal_connect_object (self->globals_om, "installed",
|
||||
G_CALLBACK (on_global_om_installed), self, 0);
|
||||
wp_core_install_object_manager (core, self->globals_om);
|
||||
|
||||
/* install hook to restore the rescan_scheduled state just before rescanning */
|
||||
self->rescan_done_hook = wp_simple_event_hook_new (
|
||||
|
|
@ -446,6 +518,7 @@ wp_standard_event_source_disable (WpPlugin * plugin)
|
|||
|
||||
for (gint i = 0; i < N_OBJECT_TYPES; i++)
|
||||
g_clear_object (&self->oms[i]);
|
||||
g_clear_object (&self->globals_om);
|
||||
|
||||
if (dispatcher)
|
||||
wp_event_dispatcher_unregister_hook (dispatcher, self->rescan_done_hook);
|
||||
|
|
|
|||
|
|
@ -557,10 +557,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,
|
||||
|
|
@ -682,38 +687,46 @@ wireplumber.components = [
|
|||
requires = [ 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
|
||||
|
|
@ -735,13 +748,15 @@ wireplumber.components = [
|
|||
hooks.linking.target.prepare-link,
|
||||
hooks.linking.target.link ]
|
||||
wants = [ hooks.linking.rescan-on-linkable,
|
||||
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,
|
||||
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 ]
|
||||
}
|
||||
|
||||
|
|
@ -757,15 +772,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)
|
||||
|
|
|
|||
518
src/scripts/device/create-alsa-loopback.lua
Normal file
518
src/scripts/device/create-alsa-loopback.lua
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
-- 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 = {}
|
||||
local impl_collections = {}
|
||||
|
||||
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 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 ()
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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 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
|
||||
|
||||
-- Create the collection for this loopback
|
||||
local collection_name = getCollectionName (device_name, loopback_id)
|
||||
|
||||
-- Create the collection
|
||||
if impl_collections [device.id] == nil then
|
||||
impl_collections [device.id] = {}
|
||||
end
|
||||
local collection = impl_collections [device.id][collection_name]
|
||||
if collection == nil then
|
||||
impl_collections [device.id][collection_name] = ImplCollection (collection_name, {
|
||||
["device.id"] = device["bound-id"],
|
||||
["alsa.loopback.id"] = loopback_id,
|
||||
})
|
||||
impl_collections [device.id][collection_name]:activate (Features.ALL, function (c, 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 .. " " .. tostring (c["bound-id"]))
|
||||
|
||||
-- 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))
|
||||
|
||||
transition:advance ()
|
||||
end)
|
||||
else
|
||||
log:warning (device,
|
||||
"Collection '" .. collection_name .. "' already exists")
|
||||
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
|
||||
|
||||
-- Deactivate and destroy loopback collection
|
||||
local collection_name = getCollectionName (device_name, loopback_id)
|
||||
if impl_collections [device.id] ~= nil and
|
||||
impl_collections [device.id][collection_name] then
|
||||
impl_collections [device.id][collection_name]:deactivate(Features.ALL)
|
||||
impl_collections [device.id][collection_name] = nil
|
||||
end
|
||||
|
||||
log:info (device, "Destroyed loopback module and collection 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 ()
|
||||
local device_name = device:get_property ("device.name")
|
||||
|
||||
-- Remove all loopbacks associated with this device
|
||||
alsa_loopbacks [device.id] = nil
|
||||
|
||||
-- Deactivate and destroy all collections associated with this device
|
||||
if impl_collections [device.id] ~= nil then
|
||||
for _, collection in ipairs(impl_collections [device.id]) do
|
||||
collection:deactivate(Features.ALL)
|
||||
end
|
||||
impl_collections [device.id] = nil
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
|
||||
function evaluateCollectionForNode (collection, collect_node_rules, devices_om, node)
|
||||
local collection_name = collection:get_name ()
|
||||
local collection_props = collection:get_global_properties ()
|
||||
local collection_device_id = collection_props:get_int ("device.id")
|
||||
local collection_loopback_id = collection_props:get_int ("alsa.loopback.id")
|
||||
local node_name = node:get_property ("node.name")
|
||||
|
||||
-- Collect the node if there is a rule for it
|
||||
JsonUtils.match_rules (collect_node_rules, node.properties, function (action, value)
|
||||
if action == "select-route-device" and value:is_int () and value:parse () == collection_loopback_id then
|
||||
node:attach_collection (collection)
|
||||
log:info (collection, "Collected node '" .. node_name .. "' into '" ..
|
||||
collection_name .. "'")
|
||||
end
|
||||
end)
|
||||
if node:get_collection_name () ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Otherwise check if it is a matching ALSA or loopback node
|
||||
|
||||
-- Never 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
|
||||
|
||||
-- Get the node device Id
|
||||
local device_id = node.properties:get_int ("device.id")
|
||||
if device_id == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the node 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
|
||||
|
||||
-- Skip nodes with unmatched device_id or loopback_id
|
||||
if loopback_id ~= collection_loopback_id or device_id ~= collection_device_id then
|
||||
return
|
||||
end
|
||||
|
||||
-- Collect the node into the device collection
|
||||
node:attach_collection (collection)
|
||||
log:info (collection, "Collected node '" .. node_name .. "' into '" ..
|
||||
collection_name .. "'")
|
||||
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "device/collection-added",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "collection-added" },
|
||||
Constraint { "collection.name", "#", "alsa_loopback.*" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local collection = event:get_subject ()
|
||||
local collection_name = collection:get_name ()
|
||||
local collection_props = collection:get_global_properties ()
|
||||
local device_id = collection_props:get_int ("device.id")
|
||||
|
||||
-- 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
|
||||
|
||||
-- Check if there are matching rules for this device
|
||||
local collect_node_rules = Json.Array {}
|
||||
JsonUtils.match_rules (config.rules, device.properties, function (action, value)
|
||||
if action == "collect-nodes" then
|
||||
collect_node_rules = value
|
||||
end
|
||||
return true
|
||||
end)
|
||||
|
||||
-- Re-evaluate all nodes
|
||||
local nodes_om = source:call ("get-object-manager", "node")
|
||||
for node in nodes_om:iterate () do
|
||||
evaluateCollectionForNode (collection, collect_node_rules, devices_om, node)
|
||||
end
|
||||
|
||||
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)
|
||||
|
|
@ -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_name = node:get_collection_name ()
|
||||
|
||||
-- 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_name
|
||||
|
||||
-- set the default media.role, if configured
|
||||
-- avoid Settings.get_string(), as it will parse the default "null" value
|
||||
|
|
@ -61,69 +63,75 @@ 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_name = node:get_collection_name ()
|
||||
|
||||
-- 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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ static struct {
|
|||
gboolean reset;
|
||||
} settings;
|
||||
|
||||
struct {
|
||||
const gchar *collection_name;
|
||||
} collections;
|
||||
|
||||
struct {
|
||||
guint64 id;
|
||||
const char *level;
|
||||
|
|
@ -1765,6 +1769,133 @@ 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 (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';
|
||||
} else if (WP_IS_COLLECTION (global)) {
|
||||
global_name = wp_properties_get (props, "collection.name");
|
||||
global_type = 'o';
|
||||
}
|
||||
|
||||
g_print (" [%c] %4u. %s\n", global_type, bound_id,
|
||||
global_name ? global_name : "UNKNOWN");
|
||||
}
|
||||
|
||||
static void
|
||||
print_collection (WpCtl * self, WpCollection *collection)
|
||||
{
|
||||
const gchar *collection_name = NULL;
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
guint32 bound_id;
|
||||
|
||||
collection_name = wp_collection_get_name (collection);
|
||||
bound_id = wp_proxy_get_bound_id (WP_PROXY (collection));
|
||||
|
||||
g_print ("%4u. %s\n", bound_id, collection_name);
|
||||
|
||||
iter = wp_collection_new_iterator (collection);
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
guint32 global_id = g_value_get_uint (&val);
|
||||
g_autoptr (WpGlobalProxy) global = NULL;
|
||||
global = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
|
||||
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", global_id, NULL);
|
||||
if (global)
|
||||
print_global (global);
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
printf ("\n");
|
||||
}
|
||||
|
||||
static void
|
||||
print_collections (WpCtl * self)
|
||||
{
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
printf ("Collections:\n\n");
|
||||
|
||||
it = wp_object_manager_new_filtered_iterator (self->om, WP_TYPE_COLLECTION,
|
||||
NULL);
|
||||
while (wp_iterator_next (it, &item)) {
|
||||
WpCollection *collection = g_value_get_object (&item);
|
||||
print_collection (self, collection);
|
||||
g_value_unset (&item);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
collections_run (WpCtl * self)
|
||||
{
|
||||
const gchar *collection_name = cmdline.collections.collection_name;
|
||||
|
||||
/* Print all collections if name is not provided */
|
||||
if (collection_name) {
|
||||
g_autoptr (WpCollection) collection = wp_object_manager_lookup (self->om,
|
||||
WP_TYPE_COLLECTION, WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
|
||||
"wireplumber.collection", "=s", "true",
|
||||
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "collection.name", "=s",
|
||||
collection_name, NULL);
|
||||
if (collection)
|
||||
print_collection (self, collection);
|
||||
else
|
||||
printf ("Collection '%s' does not exist\n", collection_name);
|
||||
} else {
|
||||
print_collections (self);
|
||||
}
|
||||
|
||||
wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
|
||||
}
|
||||
|
||||
/* set-log-level */
|
||||
|
||||
static gboolean
|
||||
|
|
@ -2023,6 +2154,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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
281
tests/wp/collection.c
Normal file
281
tests/wp/collection.c
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2026 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <pipewire/extensions/session-manager/keys.h>
|
||||
|
||||
#include "../common/base-test-fixture.h"
|
||||
|
||||
typedef struct {
|
||||
WpBaseTestFixture base;
|
||||
|
||||
WpObjectManager *om;
|
||||
WpCollection *collection;
|
||||
|
||||
guint32 last_collected_id;
|
||||
guint32 last_dropped_id;
|
||||
} TestFixture;
|
||||
|
||||
static void
|
||||
test_collection_setup (TestFixture *self, gconstpointer user_data)
|
||||
{
|
||||
wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE);
|
||||
self->om = wp_object_manager_new ();
|
||||
}
|
||||
|
||||
static void
|
||||
test_collection_teardown (TestFixture *self, gconstpointer user_data)
|
||||
{
|
||||
g_clear_object (&self->om);
|
||||
wp_base_test_fixture_teardown (&self->base);
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_added (WpObjectManager *om, WpCollection *collection,
|
||||
TestFixture *fixture)
|
||||
{
|
||||
g_assert_true (WP_IS_COLLECTION (collection));
|
||||
|
||||
g_assert_null (fixture->collection);
|
||||
fixture->collection = WP_COLLECTION (collection);
|
||||
|
||||
g_main_loop_quit (fixture->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
on_collection_removed (WpObjectManager *om, WpCollection *collection,
|
||||
TestFixture *fixture)
|
||||
{
|
||||
g_assert_true (WP_IS_COLLECTION (collection));
|
||||
|
||||
g_assert_nonnull (fixture->collection);
|
||||
fixture->collection = NULL;
|
||||
|
||||
g_main_loop_quit (fixture->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_collected (WpCollection *om, guint32 global_id, TestFixture *fixture)
|
||||
{
|
||||
fixture->last_collected_id = global_id;
|
||||
g_main_loop_quit (fixture->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
on_global_dropped (WpCollection *om, guint32 global_id, TestFixture *fixture)
|
||||
{
|
||||
fixture->last_dropped_id = global_id;
|
||||
g_main_loop_quit (fixture->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
test_impl_collection_activated (WpObject * impl_collection, GAsyncResult * res,
|
||||
TestFixture *fixture)
|
||||
{
|
||||
g_autoptr (GError) error = NULL;
|
||||
|
||||
g_assert_true (wp_object_activate_finish (impl_collection, res, &error));
|
||||
g_assert_no_error (error);
|
||||
|
||||
g_assert_true (WP_IS_IMPL_COLLECTION (impl_collection));
|
||||
|
||||
g_main_loop_quit (fixture->base.loop);
|
||||
}
|
||||
|
||||
static void
|
||||
test_collection_basic (TestFixture *fixture, gconstpointer data)
|
||||
{
|
||||
g_autoptr (WpImplCollection) impl_collection = NULL;
|
||||
|
||||
/* Install object manager on the client side */
|
||||
g_signal_connect (fixture->om, "object-added",
|
||||
(GCallback) on_collection_added, fixture);
|
||||
g_signal_connect (fixture->om, "object-removed",
|
||||
(GCallback) on_collection_removed, fixture);
|
||||
wp_object_manager_add_interest (fixture->om, WP_TYPE_COLLECTION, NULL);
|
||||
wp_object_manager_request_object_features (fixture->om, WP_TYPE_COLLECTION,
|
||||
WP_OBJECT_FEATURES_ALL);
|
||||
wp_core_install_object_manager (fixture->base.client_core, fixture->om);
|
||||
|
||||
/* Create the collection */
|
||||
impl_collection = wp_impl_collection_new (fixture->base.core, "my-collection",
|
||||
NULL);
|
||||
g_assert_nonnull (impl_collection);
|
||||
|
||||
/* Export the collection */
|
||||
wp_object_activate (WP_OBJECT (impl_collection), WP_OBJECT_FEATURES_ALL,
|
||||
NULL, (GAsyncReadyCallback) test_impl_collection_activated, fixture);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
|
||||
/* Run again so the collection is added in the client object manager */
|
||||
g_assert_null (fixture->collection);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (wp_object_manager_get_n_objects (fixture->om), ==, 1);
|
||||
g_assert_nonnull (fixture->collection);
|
||||
|
||||
/* Check name */
|
||||
{
|
||||
const gchar *name = wp_collection_get_name (fixture->collection);
|
||||
g_assert_cmpstr (name, ==, "my-collection");
|
||||
}
|
||||
|
||||
/* Check properties */
|
||||
{
|
||||
g_autoptr (WpProperties) props = NULL;
|
||||
props = wp_global_proxy_get_global_properties (
|
||||
WP_GLOBAL_PROXY (fixture->collection));
|
||||
const gchar *str = wp_properties_get (props, "wireplumber.collection");
|
||||
g_assert_cmpstr (str, ==, "true");
|
||||
}
|
||||
|
||||
/* Handle collection signals */
|
||||
g_signal_connect (fixture->collection, "global-collected",
|
||||
(GCallback) on_global_collected, fixture);
|
||||
g_signal_connect (fixture->collection, "global-dropped",
|
||||
(GCallback) on_global_dropped, fixture);
|
||||
|
||||
/* Make sure collection does not have any globals */
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 0);
|
||||
|
||||
/* Collect the first global */
|
||||
fixture->last_collected_id = 0;
|
||||
wp_collection_collect_global (fixture->collection, 42);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (fixture->last_collected_id, ==, 42);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1);
|
||||
g_assert_true (wp_collection_contains_global (fixture->collection, 42));
|
||||
g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1);
|
||||
g_assert_true (wp_impl_collection_contains_global (impl_collection, 42));
|
||||
|
||||
/* Collect the second global */
|
||||
fixture->last_collected_id = 0;
|
||||
wp_collection_collect_global (fixture->collection, 99);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (fixture->last_collected_id, ==, 99);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 2);
|
||||
g_assert_true (wp_collection_contains_global (fixture->collection, 99));
|
||||
g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 2);
|
||||
g_assert_true (wp_impl_collection_contains_global (impl_collection, 99));
|
||||
|
||||
/* Iterate the globals from the client side */
|
||||
{
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
gboolean has_first = FALSE;
|
||||
gboolean has_second = FALSE;
|
||||
guint count = 0;
|
||||
|
||||
iter = wp_collection_new_iterator (fixture->collection);
|
||||
g_assert_nonnull (iter);
|
||||
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
guint32 global_id = g_value_get_uint (&val);
|
||||
if (global_id == 42)
|
||||
has_first = TRUE;
|
||||
if (global_id == 99)
|
||||
has_second = TRUE;
|
||||
count++;
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
g_assert_true (has_first);
|
||||
g_assert_true (has_second);
|
||||
g_assert_cmpuint (count, ==, 2);
|
||||
}
|
||||
|
||||
/* Iterate the globals from the impl side */
|
||||
{
|
||||
g_autoptr (WpIterator) iter = NULL;
|
||||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
gboolean has_first = FALSE;
|
||||
gboolean has_second = FALSE;
|
||||
guint count = 0;
|
||||
|
||||
iter = wp_impl_collection_new_iterator (impl_collection);
|
||||
g_assert_nonnull (iter);
|
||||
|
||||
while (wp_iterator_next (iter, &val)) {
|
||||
guint32 global_id = g_value_get_uint (&val);
|
||||
if (global_id == 42)
|
||||
has_first = TRUE;
|
||||
if (global_id == 99)
|
||||
has_second = TRUE;
|
||||
count++;
|
||||
g_value_unset (&val);
|
||||
}
|
||||
|
||||
g_assert_true (has_first);
|
||||
g_assert_true (has_second);
|
||||
g_assert_cmpuint (count, ==, 2);
|
||||
}
|
||||
|
||||
/* Drop the first global */
|
||||
fixture->last_dropped_id = 0;
|
||||
wp_collection_drop_global (fixture->collection, 42);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (fixture->last_dropped_id, ==, 42);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1);
|
||||
g_assert_false (wp_collection_contains_global (fixture->collection, 42));
|
||||
g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1);
|
||||
g_assert_false (wp_impl_collection_contains_global (impl_collection, 42));
|
||||
|
||||
/* Collect an existing global and make sure nothing happens */
|
||||
fixture->last_collected_id = 0;
|
||||
wp_collection_collect_global (fixture->collection, 99);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1);
|
||||
g_assert_true (wp_collection_contains_global (fixture->collection, 99));
|
||||
g_assert_true (wp_impl_collection_contains_global (impl_collection, 99));
|
||||
|
||||
/* Drop an unexisting global and make sure nothing happens */
|
||||
fixture->last_dropped_id = 0;
|
||||
wp_collection_drop_global (fixture->collection, 42);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1);
|
||||
g_assert_false (wp_collection_contains_global (fixture->collection, 42));
|
||||
g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1);
|
||||
g_assert_false (wp_impl_collection_contains_global (impl_collection, 42));
|
||||
|
||||
/* Drop the second global */
|
||||
fixture->last_dropped_id = 0;
|
||||
wp_collection_drop_global (fixture->collection, 99);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (fixture->last_dropped_id, ==, 99);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 0);
|
||||
g_assert_false (wp_collection_contains_global (fixture->collection, 99));
|
||||
g_assert_false (wp_impl_collection_contains_global (impl_collection, 99));
|
||||
|
||||
/* Collect a global from the impl side */
|
||||
fixture->last_collected_id = 0;
|
||||
wp_impl_collection_collect_global (impl_collection, 36);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (fixture->last_collected_id, ==, 36);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 1);
|
||||
g_assert_true (wp_collection_contains_global (fixture->collection, 36));
|
||||
g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 1);
|
||||
g_assert_true (wp_impl_collection_contains_global (impl_collection, 36));
|
||||
|
||||
/* Drop the global from the impl size */
|
||||
fixture->last_dropped_id = 0;
|
||||
wp_impl_collection_drop_global (impl_collection, 36);
|
||||
g_main_loop_run (fixture->base.loop);
|
||||
g_assert_cmpuint (fixture->last_dropped_id, ==, 36);
|
||||
g_assert_cmpuint (wp_collection_get_size (fixture->collection), ==, 0);
|
||||
g_assert_false (wp_collection_contains_global (fixture->collection, 36));
|
||||
g_assert_cmpuint (wp_impl_collection_get_size (impl_collection), ==, 0);
|
||||
g_assert_false (wp_impl_collection_contains_global (impl_collection, 36));
|
||||
}
|
||||
|
||||
gint
|
||||
main (gint argc, gchar *argv[])
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
wp_init (WP_INIT_ALL);
|
||||
|
||||
g_test_add ("/wp/collection/basic", TestFixture, NULL,
|
||||
test_collection_setup, test_collection_basic, test_collection_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',
|
||||
executable('test-collection', 'collection.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