
PipeWire headers were recently cleaned up to reduce the number of included headers. This leads to a number of functions and types not being included when needed in wireplumber.
598 lines
17 KiB
C
598 lines
17 KiB
C
/* WirePlumber
|
|
*
|
|
* Copyright © 2019 Collabora Ltd.
|
|
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include <spa/monitor/device.h>
|
|
#include <spa/pod/builder.h>
|
|
#include <spa/pod/iter.h>
|
|
#include <spa/utils/result.h>
|
|
#include <pipewire/pipewire.h>
|
|
|
|
#include "monitor.h"
|
|
#include "error.h"
|
|
#include "wpenums.h"
|
|
|
|
typedef struct {
|
|
struct spa_handle *handle;
|
|
gpointer interface;
|
|
} WpSpaObject;
|
|
|
|
struct _WpMonitor
|
|
{
|
|
GObject parent;
|
|
|
|
/* Props */
|
|
GWeakRef core;
|
|
gchar *factory_name;
|
|
WpProperties *properties;
|
|
WpMonitorFlags flags;
|
|
|
|
struct object *device;
|
|
};
|
|
|
|
struct object
|
|
{
|
|
guint32 id;
|
|
guint32 type;
|
|
union {
|
|
WpSpaObject *spa_dev;
|
|
struct pw_node *node;
|
|
};
|
|
|
|
WpProxy *proxy;
|
|
WpProperties *properties;
|
|
|
|
GList *children; /* element-type: struct object* */
|
|
|
|
WpMonitor *self;
|
|
struct spa_hook listener;
|
|
};
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_CORE,
|
|
PROP_FACTORY_NAME,
|
|
PROP_PROPERTIES,
|
|
PROP_FLAGS,
|
|
};
|
|
|
|
enum {
|
|
SIG_SETUP_NODE_PROPS,
|
|
SIG_SETUP_DEVICE_PROPS,
|
|
N_SIGNALS
|
|
};
|
|
|
|
static guint32 signals[N_SIGNALS] = {0};
|
|
|
|
G_DEFINE_TYPE (WpMonitor, wp_monitor, G_TYPE_OBJECT)
|
|
|
|
static gpointer find_object (GList *list, guint32 id, GList **link);
|
|
static struct object * node_new (struct object *dev, uint32_t id,
|
|
const struct spa_device_object_info *info);
|
|
static struct object * device_new (WpMonitor *self, uint32_t id,
|
|
const gchar *factory_name, WpProperties *properties, GError **error);
|
|
static void object_free (struct object *obj);
|
|
|
|
/* device events */
|
|
|
|
static void
|
|
device_info (void *data, const struct spa_device_info *info)
|
|
{
|
|
struct object *obj = data;
|
|
|
|
/*
|
|
* This is emited syncrhonously at the time we add the listener and
|
|
* before object_info is emited. It gives us additional properties
|
|
* about the device, like the "api.alsa.card.*" ones that are not
|
|
* set by the monitor
|
|
*/
|
|
if (info->change_mask & SPA_DEVICE_CHANGE_MASK_PROPS && obj->properties)
|
|
wp_properties_update_from_dict (obj->properties, info->props);
|
|
}
|
|
|
|
static void
|
|
device_object_info (void *data, uint32_t id,
|
|
const struct spa_device_object_info *info)
|
|
{
|
|
struct object *obj = data;
|
|
struct object *child = NULL;
|
|
WpMonitor *self = obj->self;
|
|
GList *link = NULL;
|
|
g_autoptr (GError) err = NULL;
|
|
|
|
/* Find the child */
|
|
child = find_object (obj->children, id, &link);
|
|
|
|
/* new object, construct... */
|
|
if (info && !child) {
|
|
switch (info->type) {
|
|
case SPA_TYPE_INTERFACE_Device:
|
|
if (!(child = device_new (self, id, info->factory_name,
|
|
wp_properties_new_wrap_dict (info->props), &err))) {
|
|
g_debug ("WpMonitor:%p:%s %s", self, self->factory_name, err->message);
|
|
return;
|
|
}
|
|
break;
|
|
case SPA_TYPE_INTERFACE_Node:
|
|
if (!(child = node_new (obj, id, info)))
|
|
return;
|
|
break;
|
|
default:
|
|
g_debug ("WpMonitor:%p:%s got device_object_info for unknown object "
|
|
"type %u", self, self->factory_name, info->type);
|
|
return;
|
|
}
|
|
obj->children = g_list_append (obj->children, child);
|
|
}
|
|
/* object removed, delete... */
|
|
else if (!info && child) {
|
|
object_free (child);
|
|
obj->children = g_list_delete_link (obj->children, link);
|
|
}
|
|
}
|
|
|
|
static const struct spa_device_events device_events = {
|
|
SPA_VERSION_DEVICE_EVENTS,
|
|
.info = device_info,
|
|
.object_info = device_object_info
|
|
};
|
|
|
|
/* WpSpaObject */
|
|
|
|
static void
|
|
wp_spa_object_free (WpSpaObject *self)
|
|
{
|
|
pw_unload_spa_handle (self->handle);
|
|
}
|
|
|
|
static inline WpSpaObject *
|
|
wp_spa_object_ref (WpSpaObject *self)
|
|
{
|
|
return g_rc_box_acquire (self);
|
|
}
|
|
|
|
static inline void
|
|
wp_spa_object_unref (WpSpaObject *self)
|
|
{
|
|
g_rc_box_release_full (self, (GDestroyNotify) wp_spa_object_free);
|
|
}
|
|
|
|
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaObject, wp_spa_object_unref)
|
|
|
|
static WpSpaObject *
|
|
load_spa_object (WpCore *core, const gchar *factory, guint32 iface_type,
|
|
WpProperties *props, GError **error)
|
|
{
|
|
g_autoptr (WpSpaObject) self = g_rc_box_new0 (WpSpaObject);
|
|
gint res;
|
|
|
|
/* Load the monitor handle */
|
|
self->handle = pw_core_load_spa_handle (wp_core_get_pw_core (core),
|
|
factory, props ? wp_properties_peek_dict (props) : NULL);
|
|
if (!self->handle) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
|
"SPA handle '%s' could not be loaded; is it installed?",
|
|
factory);
|
|
return NULL;
|
|
}
|
|
|
|
/* Get the handle interface */
|
|
res = spa_handle_get_interface (self->handle, iface_type, &self->interface);
|
|
if (res < 0) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
|
"Could not get interface 0x%x from SPA handle", iface_type);
|
|
return NULL;
|
|
}
|
|
|
|
return g_steal_pointer (&self);
|
|
}
|
|
|
|
/* struct object */
|
|
|
|
static gpointer
|
|
find_object (GList *list, guint32 id, GList **link)
|
|
{
|
|
/*
|
|
* The first element of struct object is the guint32 containing the id,
|
|
* so we can directly cast the list data to guint32, no matter what the
|
|
* actual structure is
|
|
*/
|
|
for (; list; list = g_list_next (list)) {
|
|
if (id == *((guint32 *) list->data)) {
|
|
*link = list;
|
|
return list->data;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct object *
|
|
node_new (struct object *dev, uint32_t id,
|
|
const struct spa_device_object_info *info)
|
|
{
|
|
WpMonitor *self = dev->self;
|
|
g_autoptr (WpCore) core = NULL;
|
|
g_autoptr (WpProperties) props = NULL;
|
|
struct pw_proxy *pw_proxy = NULL;
|
|
struct pw_node *pw_node = NULL;
|
|
struct object *node = NULL;
|
|
const gchar *pw_factory_name = "spa-node-factory";
|
|
|
|
g_return_val_if_fail (info->type == SPA_TYPE_INTERFACE_Node, NULL);
|
|
|
|
g_debug ("WpMonitor:%p:%s new node %u", self, self->factory_name, id);
|
|
|
|
/* use the adapter instead of spa-node-factory if requested */
|
|
if (self->flags & WP_MONITOR_FLAG_USE_ADAPTER)
|
|
pw_factory_name = "adapter";
|
|
|
|
core = g_weak_ref_get (&self->core);
|
|
props = wp_properties_new_copy_dict (info->props);
|
|
|
|
/* pass down the id to the setup function */
|
|
wp_properties_setf (props, WP_MONITOR_KEY_OBJECT_ID, "%u", id);
|
|
|
|
/* the SPA factory name must be set as a property
|
|
for the spa-node-factory / adapter */
|
|
wp_properties_set (props, PW_KEY_FACTORY_NAME, info->factory_name);
|
|
|
|
/* the rest is up to the user */
|
|
g_signal_emit (self, signals[SIG_SETUP_NODE_PROPS], 0, dev->properties,
|
|
props);
|
|
|
|
/* and delete the id - it should not appear on the proxy */
|
|
wp_properties_set (props, WP_MONITOR_KEY_OBJECT_ID, NULL);
|
|
|
|
if (self->flags & WP_MONITOR_FLAG_LOCAL_NODES) {
|
|
struct pw_factory *factory;
|
|
|
|
/* create the pipewire node (and the underlying SPA node)
|
|
on the wireplumber process and export it */
|
|
|
|
factory = pw_core_find_factory (wp_core_get_pw_core (core), pw_factory_name);
|
|
if (!factory) {
|
|
g_warning ("WpMonitor:%p: no '%s' factory found; node '%s' will "
|
|
"not be created", self, pw_factory_name, info->factory_name);
|
|
return NULL;
|
|
}
|
|
|
|
pw_node = pw_factory_create_object (factory,
|
|
NULL,
|
|
PW_TYPE_INTERFACE_Node,
|
|
PW_VERSION_NODE_PROXY,
|
|
wp_properties_to_pw_properties (props),
|
|
0);
|
|
if (!pw_node) {
|
|
g_warning ("WpMonitor:%p: failed to construct pw_node; node '%s' will "
|
|
"not be created", self, info->factory_name);
|
|
return NULL;
|
|
}
|
|
|
|
pw_proxy = pw_remote_export (wp_core_get_pw_remote (core),
|
|
PW_TYPE_INTERFACE_Node,
|
|
wp_properties_to_pw_properties (props),
|
|
pw_node,
|
|
0);
|
|
if (!pw_proxy) {
|
|
g_warning ("WpMonitor:%p: failed to export node: %s", self,
|
|
g_strerror (errno));
|
|
pw_node_destroy (pw_node);
|
|
return NULL;
|
|
}
|
|
} else {
|
|
/* create the pipewire node (and the underlying SPA node)
|
|
on the remote pipewire process */
|
|
pw_proxy = pw_core_proxy_create_object (wp_core_get_pw_core_proxy (core),
|
|
pw_factory_name,
|
|
PW_TYPE_INTERFACE_Node,
|
|
PW_VERSION_NODE_PROXY,
|
|
wp_properties_peek_dict (props),
|
|
0);
|
|
}
|
|
|
|
node = g_slice_new0 (struct object);
|
|
node->self = self;
|
|
node->id = id;
|
|
node->type = SPA_TYPE_INTERFACE_Node;
|
|
node->node = pw_node;
|
|
node->proxy = wp_proxy_new_wrap (core, pw_proxy, PW_TYPE_INTERFACE_Node,
|
|
PW_VERSION_NODE_PROXY);
|
|
|
|
return node;
|
|
}
|
|
|
|
static void
|
|
set_profile(struct spa_device * dev, int index)
|
|
{
|
|
char buf[1024];
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
|
|
spa_device_set_param (dev,
|
|
SPA_PARAM_Profile, 0,
|
|
spa_pod_builder_add_object(&b,
|
|
SPA_TYPE_OBJECT_ParamProfile, 0,
|
|
SPA_PARAM_PROFILE_index, SPA_POD_Int(index)));
|
|
}
|
|
|
|
static struct object *
|
|
device_new (WpMonitor *self, uint32_t id, const gchar *factory_name,
|
|
WpProperties *properties, GError **error)
|
|
{
|
|
g_autoptr (GError) err = NULL;
|
|
g_autoptr (WpCore) core = NULL;
|
|
g_autoptr (WpProperties) props = NULL;
|
|
g_autoptr (WpSpaObject) spa_dev = NULL;
|
|
struct pw_proxy *proxy = NULL;
|
|
struct object *dev = NULL;
|
|
gint ret = 0;
|
|
|
|
g_debug ("WpMonitor:%p:%s new device %d", self, self->factory_name, (gint) id);
|
|
|
|
core = g_weak_ref_get (&self->core);
|
|
props = properties ? wp_properties_copy (properties) : wp_properties_new_empty ();
|
|
|
|
/* pass down the id to the setup function */
|
|
wp_properties_setf (props, WP_MONITOR_KEY_OBJECT_ID, "%d", (gint) id);
|
|
|
|
/* let the handler setup the properties accordingly */
|
|
g_signal_emit (self, signals[SIG_SETUP_DEVICE_PROPS], 0, props);
|
|
|
|
/* and delete the id - it should not appear on the proxy */
|
|
wp_properties_set (props, WP_MONITOR_KEY_OBJECT_ID, NULL);
|
|
|
|
if (!(spa_dev = load_spa_object (core, factory_name, SPA_TYPE_INTERFACE_Device,
|
|
props, &err))) {
|
|
g_propagate_error (error, g_steal_pointer (&err));
|
|
return NULL;
|
|
}
|
|
|
|
/* check for id != -1 to avoid exporting the "monitor" device itself;
|
|
exporting it is buggy, but we should revise this in the future; FIXME */
|
|
if (id != -1 && !(proxy = pw_remote_export (wp_core_get_pw_remote (core),
|
|
SPA_TYPE_INTERFACE_Device, wp_properties_to_pw_properties (props),
|
|
spa_dev->interface, 0))) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
|
"failed to export device: %s", g_strerror (errno));
|
|
return NULL;
|
|
}
|
|
|
|
/* Create the device */
|
|
dev = g_slice_new0 (struct object);
|
|
dev->self = self;
|
|
dev->id = id;
|
|
dev->type = SPA_TYPE_INTERFACE_Device;
|
|
dev->spa_dev = g_steal_pointer (&spa_dev);
|
|
dev->properties = g_steal_pointer (&props);
|
|
dev->proxy = wp_proxy_new_wrap (core, proxy, PW_TYPE_INTERFACE_Device,
|
|
PW_VERSION_DEVICE_PROXY);
|
|
|
|
/* Add device listener for events */
|
|
if ((ret = spa_device_add_listener ((struct spa_device *) dev->spa_dev->interface,
|
|
&dev->listener, &device_events, dev)) < 0) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
|
"failed to initialize device: %s", spa_strerror (ret));
|
|
return NULL;
|
|
}
|
|
|
|
/* HACK this is very specific to the current alsa pcm profiles */
|
|
if (self->flags & WP_MONITOR_FLAG_ACTIVATE_DEVICES)
|
|
set_profile ((struct spa_device *) dev->spa_dev->interface, 1);
|
|
|
|
return dev;
|
|
}
|
|
|
|
static void
|
|
object_free (struct object *obj)
|
|
{
|
|
gboolean is_node = (obj->type == SPA_TYPE_INTERFACE_Node);
|
|
|
|
g_debug ("WpMonitor:%p:%s free %s %u", obj->self,
|
|
obj->self->factory_name, is_node ? "node" : "device", obj->id);
|
|
|
|
if (!is_node)
|
|
spa_hook_remove (&obj->listener);
|
|
|
|
g_list_free_full (obj->children, (GDestroyNotify) object_free);
|
|
g_clear_object (&obj->proxy);
|
|
|
|
if (is_node)
|
|
g_clear_pointer (&obj->node, pw_node_destroy);
|
|
else
|
|
g_clear_pointer (&obj->spa_dev, wp_spa_object_unref);
|
|
|
|
g_clear_pointer (&obj->properties, wp_properties_unref);
|
|
|
|
g_slice_free (struct object, obj);
|
|
}
|
|
|
|
/* WpMonitor */
|
|
|
|
static void
|
|
wp_monitor_init (WpMonitor * self)
|
|
{
|
|
g_weak_ref_init (&self->core, NULL);
|
|
}
|
|
|
|
static void
|
|
wp_monitor_finalize (GObject * object)
|
|
{
|
|
WpMonitor * self = WP_MONITOR (object);
|
|
|
|
wp_monitor_stop (self);
|
|
|
|
g_clear_pointer (&self->properties, wp_properties_unref);
|
|
g_weak_ref_clear (&self->core);
|
|
g_free (self->factory_name);
|
|
|
|
G_OBJECT_CLASS (wp_monitor_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
wp_monitor_set_property (GObject * object, guint property_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpMonitor * self = WP_MONITOR (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_CORE:
|
|
g_weak_ref_set (&self->core, g_value_get_object (value));
|
|
break;
|
|
case PROP_FACTORY_NAME:
|
|
self->factory_name = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PROPERTIES:
|
|
self->properties = g_value_dup_boxed (value);
|
|
break;
|
|
case PROP_FLAGS:
|
|
self->flags = (WpMonitorFlags) g_value_get_flags (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_monitor_get_property (GObject * object, guint property_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpMonitor * self = WP_MONITOR (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_CORE:
|
|
g_value_take_object (value, g_weak_ref_get (&self->core));
|
|
break;
|
|
case PROP_FACTORY_NAME:
|
|
g_value_set_string (value, self->factory_name);
|
|
break;
|
|
case PROP_PROPERTIES:
|
|
g_value_set_boxed (value, self->properties);
|
|
break;
|
|
case PROP_FLAGS:
|
|
g_value_set_flags (value, (guint) self->flags);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_monitor_class_init (WpMonitorClass * klass)
|
|
{
|
|
GObjectClass *object_class = (GObjectClass *) klass;
|
|
|
|
object_class->finalize = wp_monitor_finalize;
|
|
object_class->set_property = wp_monitor_set_property;
|
|
object_class->get_property = wp_monitor_get_property;
|
|
|
|
/* Install the properties */
|
|
g_object_class_install_property (object_class, PROP_CORE,
|
|
g_param_spec_object ("core", "core", "The wireplumber core",
|
|
WP_TYPE_CORE,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (object_class, PROP_FACTORY_NAME,
|
|
g_param_spec_string ("factory-name", "factory-name",
|
|
"The factory name of the spa device", 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",
|
|
"Properties for the spa device", WP_TYPE_PROPERTIES,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (object_class, PROP_FLAGS,
|
|
g_param_spec_flags ("flags", "flags",
|
|
"Additional feature flags", WP_TYPE_MONITOR_FLAGS, 0,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
/**
|
|
* WpMonitor::setup-device-props:
|
|
* @self: the #WpMonitor
|
|
* @device_props: the properties of the device to be created
|
|
*
|
|
* This signal allows the handler to modify the properties of a device
|
|
* object before it is created.
|
|
*/
|
|
signals[SIG_SETUP_DEVICE_PROPS] = g_signal_new (
|
|
"setup-device-props", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, 0,
|
|
NULL, NULL, NULL, G_TYPE_NONE, 1, WP_TYPE_PROPERTIES);
|
|
|
|
/**
|
|
* WpMonitor::setup-node-props:
|
|
* @self: the #WpMonitor
|
|
* @device_props: the properties of the parent device
|
|
* @node_props: the properties of the node to be created
|
|
*
|
|
* This signal allows the handler to modify the properties of a node
|
|
* object before it is created.
|
|
*/
|
|
signals[SIG_SETUP_NODE_PROPS] = g_signal_new (
|
|
"setup-node-props", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, 0,
|
|
NULL, NULL, NULL, G_TYPE_NONE, 2, WP_TYPE_PROPERTIES, WP_TYPE_PROPERTIES);
|
|
}
|
|
|
|
/**
|
|
* wp_monitor_new:
|
|
* @core: the wireplumber core
|
|
* @factory_name: the factory name of the spa device
|
|
* @props: properties to pass to the spa device
|
|
* @flags: additional feature flags
|
|
*
|
|
* Returns: (transfer full): the newly created monitor
|
|
*/
|
|
WpMonitor *
|
|
wp_monitor_new (WpCore * core, const gchar * factory_name, WpProperties *props,
|
|
WpMonitorFlags flags)
|
|
{
|
|
g_return_val_if_fail (WP_IS_CORE (core), NULL);
|
|
g_return_val_if_fail (factory_name != NULL && *factory_name != '\0', NULL);
|
|
|
|
return g_object_new (WP_TYPE_MONITOR,
|
|
"core", core,
|
|
"factory-name", factory_name,
|
|
"properties", props,
|
|
"flags", flags,
|
|
NULL);
|
|
}
|
|
|
|
const gchar *
|
|
wp_monitor_get_factory_name (WpMonitor *self)
|
|
{
|
|
g_return_val_if_fail (WP_IS_MONITOR (self), NULL);
|
|
return self->factory_name;
|
|
}
|
|
|
|
gboolean
|
|
wp_monitor_start (WpMonitor *self, GError **error)
|
|
{
|
|
g_autoptr (WpCore) core = NULL;
|
|
g_autoptr (GError) err = NULL;
|
|
|
|
g_return_val_if_fail (WP_IS_MONITOR (self), FALSE);
|
|
|
|
core = g_weak_ref_get (&self->core);
|
|
|
|
g_debug ("WpMonitor:%p:%s starting monitor, flags 0x%x", self,
|
|
self->factory_name, self->flags);
|
|
|
|
self->device = device_new (self, -1, self->factory_name, self->properties,
|
|
&err);
|
|
if (!self->device) {
|
|
g_propagate_error (error, g_steal_pointer (&err));
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
wp_monitor_stop (WpMonitor *self)
|
|
{
|
|
g_return_if_fail (WP_IS_MONITOR (self));
|
|
|
|
g_debug ("WpMonitor:%p:%s stopping monitor", self, self->factory_name);
|
|
g_clear_pointer (&self->device, object_free);
|
|
}
|