wireplumber/modules/module-pipewire/simple-endpoint.c
George Kiagiadakis 50db6a5930 policy: add a hack that allows some clients to be linked always
This allows the bluealsa gstreamer helper to link to the speakers
constantly, with both A2DP and HFP streams. If this is not enabled,
only one of those will manage to connect, randomly.

This is definitely something to be superseeded by proper policy
management implemented with configuration files
2019-10-04 22:04:22 +03:00

603 lines
18 KiB
C

/* WirePlumber
*
* Copyright © 2019 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
/**
* The simple endpoint is a WpEndpoint implementation that represents
* all ports of a single direction of a single pipewire node.
* It can be used to create an Endpoint for a client node or for any
* other arbitrary node that does not need any kind of internal management.
*/
#include <spa/param/audio/format-utils.h>
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/pod/parser.h>
#include <spa/param/props.h>
struct _WpPipewireSimpleEndpoint
{
WpEndpoint parent;
/* The global-id this endpoint refers to */
guint global_id;
/* properties */
gchar *role;
guint64 creation_time;
gchar *target;
/* The task to signal the endpoint is initialized */
GTask *init_task;
gboolean init_abort;
/* The remote pipewire */
WpRemotePipewire *remote_pipewire;
/* Handler */
gulong proxy_node_done_handler_id;
/* Proxies */
WpProxyNode *proxy_node;
struct spa_hook node_proxy_listener;
GPtrArray *proxies_port;
/* controls cache */
gfloat volume;
gboolean mute;
};
enum {
PROP_0,
PROP_GLOBAL_ID,
PROP_ROLE,
PROP_CREATION_TIME,
PROP_TARGET,
};
enum {
CONTROL_VOLUME = 0,
CONTROL_MUTE,
};
static GAsyncInitableIface *wp_simple_endpoint_parent_interface = NULL;
static void wp_simple_endpoint_async_initable_init (gpointer iface,
gpointer iface_data);
G_DECLARE_FINAL_TYPE (WpPipewireSimpleEndpoint,
simple_endpoint, WP_PIPEWIRE, SIMPLE_ENDPOINT, WpEndpoint)
G_DEFINE_TYPE_WITH_CODE (WpPipewireSimpleEndpoint, simple_endpoint,
WP_TYPE_ENDPOINT,
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
wp_simple_endpoint_async_initable_init))
typedef GObject* (*WpObjectNewFinishFunc)(GObject *initable, GAsyncResult *res,
GError **error);
static GObject *
object_safe_new_finish(WpPipewireSimpleEndpoint * self, GObject *initable,
GAsyncResult *res, WpObjectNewFinishFunc new_finish_func)
{
GObject *object = NULL;
GError *error = NULL;
/* Return NULL if we are already aborting */
if (self->init_abort)
return NULL;
/* Get the object */
object = G_OBJECT (new_finish_func (initable, res, &error));
g_return_val_if_fail (object, NULL);
/* Check for error */
if (error) {
g_clear_object (&object);
g_warning ("WpPipewireSimpleEndpoint:%p Aborting construction", self);
self->init_abort = TRUE;
g_task_return_error (self->init_task, error);
g_clear_object (&self->init_task);
return NULL;
}
return object;
}
static void
node_proxy_param (void *object, int seq, uint32_t id,
uint32_t index, uint32_t next, const struct spa_pod *param)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (object);
switch (id) {
case SPA_PARAM_Props:
{
struct spa_pod_prop *prop;
struct spa_pod_object *obj = (struct spa_pod_object *) param;
float volume = self->volume;
bool mute = self->mute;
SPA_POD_OBJECT_FOREACH(obj, prop) {
switch (prop->key) {
case SPA_PROP_volume:
spa_pod_get_float(&prop->value, &volume);
break;
case SPA_PROP_mute:
spa_pod_get_bool(&prop->value, &mute);
break;
default:
break;
}
}
g_debug ("WpEndpoint:%p param event, vol:(%lf -> %f) mute:(%d -> %d)",
self, self->volume, volume, self->mute, mute);
if (self->volume != volume) {
self->volume = volume;
wp_endpoint_notify_control_value (WP_ENDPOINT (self), CONTROL_VOLUME);
}
if (self->mute != mute) {
self->mute = mute;
wp_endpoint_notify_control_value (WP_ENDPOINT (self), CONTROL_MUTE);
}
break;
}
default:
break;
}
}
static const struct pw_node_proxy_events node_node_proxy_events = {
PW_VERSION_NODE_PROXY_EVENTS,
.param = node_proxy_param,
};
static void
on_all_ports_done(WpProxy *proxy, gpointer data)
{
WpPipewireSimpleEndpoint *self = data;
/* Don't do anything if the endpoint has already been initialized */
if (!self->init_task)
return;
/* Finish the creation of the endpoint */
g_task_return_boolean (self->init_task, TRUE);
g_clear_object(&self->init_task);
}
static void
on_proxy_port_created(GObject *initable, GAsyncResult *res, gpointer data)
{
WpPipewireSimpleEndpoint *self = data;
WpProxyPort *proxy_port = NULL;
/* Get the proxy port */
proxy_port = WP_PROXY_PORT (object_safe_new_finish (self, initable, res,
(WpObjectNewFinishFunc)wp_proxy_port_new_finish));
if (!proxy_port)
return;
/* Add the proxy port to the array */
g_return_if_fail (self->proxies_port);
g_ptr_array_add(self->proxies_port, proxy_port);
/* Register the done callback */
if (!self->proxy_node_done_handler_id) {
self->proxy_node_done_handler_id = g_signal_connect_object(self->proxy_node,
"done", (GCallback)on_all_ports_done, self, 0);
wp_proxy_sync (WP_PROXY(self->proxy_node));
}
}
static void
on_port_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p,
gpointer d)
{
WpPipewireSimpleEndpoint *self = d;
struct pw_port_proxy *port_proxy = NULL;
/* Don't do anything if we are aborting */
if (self->init_abort)
return;
/* Only handle ports owned by this endpoint */
if (parent_id != self->global_id)
return;
/* Create the proxy port async */
port_proxy = wp_remote_pipewire_proxy_bind (self->remote_pipewire, id,
PW_TYPE_INTERFACE_Port);
g_return_if_fail(port_proxy);
wp_proxy_port_new(id, port_proxy, on_proxy_port_created, self);
}
static void
emit_endpoint_ports(WpPipewireSimpleEndpoint *self)
{
enum pw_direction direction = wp_endpoint_get_direction (WP_ENDPOINT (self));
struct pw_node_proxy* node_proxy = NULL;
struct spa_audio_info_raw format = { 0, };
struct spa_pod *param;
char buf[1024];
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
/* Get the pipewire node proxy */
node_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy_node));
g_return_if_fail (node_proxy);
/* The default format for audio clients */
format.format = SPA_AUDIO_FORMAT_F32P;
format.flags = 1;
format.rate = 48000;
format.channels = 2;
format.position[0] = SPA_AUDIO_CHANNEL_FL;
format.position[1] = SPA_AUDIO_CHANNEL_FR;
/* Build the param profile */
param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &format);
param = spa_pod_builder_add_object(&pod_builder,
SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
SPA_PARAM_PROFILE_direction, SPA_POD_Id(direction),
SPA_PARAM_PROFILE_format, SPA_POD_Pod(param));
/* Set the param profile to emit the ports */
pw_node_proxy_set_param(node_proxy, SPA_PARAM_Profile, 0, param);
}
static void
on_proxy_node_created(GObject *initable, GAsyncResult *res, gpointer data)
{
WpPipewireSimpleEndpoint *self = data;
GVariantDict d;
uint32_t ids[1] = { SPA_PARAM_Props };
uint32_t n_ids = 1;
struct pw_node_proxy *node_proxy = NULL;
const struct spa_dict *props;
/* Get the proxy node */
self->proxy_node = WP_PROXY_NODE (object_safe_new_finish (self, initable,
res, (WpObjectNewFinishFunc)wp_proxy_node_new_finish));
if (!self->proxy_node)
return;
props = wp_proxy_node_get_info (self->proxy_node)->props;
/* Set the role and target name */
self->role = g_strdup (spa_dict_lookup (props, "media.role"));
self->target = g_strdup (spa_dict_lookup (props, "target.name"));
/* HACK to tell the policy module that this endpoint needs to be linked always */
if (spa_dict_lookup (props, "wireplumber.keep-linked")) {
g_autofree gchar *c = g_strdup_printf ("Persistent/%s",
spa_dict_lookup(props, "media.class"));
g_object_set (self, "media-class", c, NULL);
}
/* Emit the ports */
emit_endpoint_ports(self);
/* Add a custom node proxy event listener */
node_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy_node));
g_return_if_fail (node_proxy);
pw_node_proxy_add_listener (node_proxy, &self->node_proxy_listener,
&node_node_proxy_events, self);
pw_node_proxy_subscribe_params (node_proxy, ids, n_ids);
g_variant_dict_init (&d, NULL);
g_variant_dict_insert (&d, "id", "u", 0);
g_variant_dict_insert (&d, "name", "s", "default");
wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d));
/* Audio streams have volume & mute controls */
if (g_strrstr (wp_endpoint_get_media_class (WP_ENDPOINT (self)), "Audio")) {
g_variant_dict_init (&d, NULL);
g_variant_dict_insert (&d, "id", "u", CONTROL_VOLUME);
g_variant_dict_insert (&d, "stream-id", "u", 0);
g_variant_dict_insert (&d, "name", "s", "volume");
g_variant_dict_insert (&d, "type", "s", "d");
g_variant_dict_insert (&d, "range", "(dd)", 0.0, 1.0);
g_variant_dict_insert (&d, "default-value", "d", 1.0);
wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
g_variant_dict_init (&d, NULL);
g_variant_dict_insert (&d, "id", "u", CONTROL_MUTE);
g_variant_dict_insert (&d, "stream-id", "u", 0);
g_variant_dict_insert (&d, "name", "s", "mute");
g_variant_dict_insert (&d, "type", "s", "b");
g_variant_dict_insert (&d, "default-value", "b", FALSE);
wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
}
}
static void
wp_simple_endpoint_init_async (GAsyncInitable *initable, int io_priority,
GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (initable);
g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self));
struct pw_node_proxy *node_proxy = NULL;
/* Create the async task */
self->init_task = g_task_new (initable, cancellable, callback, data);
/* Init the proxies_port array */
self->proxies_port = g_ptr_array_new_full(2, (GDestroyNotify)g_object_unref);
/* Register a port_added callback */
self->remote_pipewire = wp_core_get_global (core, WP_GLOBAL_REMOTE_PIPEWIRE);
g_return_if_fail(self->remote_pipewire);
g_signal_connect_object(self->remote_pipewire, "global-added::port",
(GCallback)on_port_added, self, 0);
/* Create the proxy node async */
node_proxy = wp_remote_pipewire_proxy_bind (self->remote_pipewire,
self->global_id, PW_TYPE_INTERFACE_Node);
g_return_if_fail(node_proxy);
wp_proxy_node_new(self->global_id, node_proxy, on_proxy_node_created, self);
/* Call the parent interface */
wp_simple_endpoint_parent_interface->init_async (initable, io_priority,
cancellable, callback, data);
}
static void
wp_simple_endpoint_async_initable_init (gpointer iface, gpointer iface_data)
{
GAsyncInitableIface *ai_iface = iface;
/* Set the parent interface */
wp_simple_endpoint_parent_interface = g_type_interface_peek_parent (iface);
/* Only set the init_async */
ai_iface->init_async = wp_simple_endpoint_init_async;
}
static void
simple_endpoint_init (WpPipewireSimpleEndpoint * self)
{
self->init_abort = FALSE;
self->creation_time = (guint64) g_get_monotonic_time ();
}
static void
simple_endpoint_finalize (GObject * object)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (object);
/* Destroy the proxies port */
if (self->proxies_port) {
g_ptr_array_free(self->proxies_port, TRUE);
self->proxies_port = NULL;
}
/* Destroy the proxy node */
g_clear_object(&self->proxy_node);
/* Destroy the done task */
g_clear_object(&self->init_task);
g_free (self->role);
G_OBJECT_CLASS (simple_endpoint_parent_class)->finalize (object);
}
static void
simple_endpoint_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (object);
switch (property_id) {
case PROP_GLOBAL_ID:
self->global_id = g_value_get_uint(value);
break;
case PROP_ROLE:
g_free (self->role);
self->role = g_value_dup_string (value);
break;
case PROP_TARGET:
g_free (self->target);
self->target = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
simple_endpoint_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (object);
switch (property_id) {
case PROP_GLOBAL_ID:
g_value_set_uint (value, self->global_id);
break;
case PROP_ROLE:
g_value_set_string (value, self->role);
break;
case PROP_CREATION_TIME:
g_value_set_uint64 (value, self->creation_time);
case PROP_TARGET:
g_value_set_string (value, self->target);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
proxies_port_foreach_func(gpointer data, gpointer user_data)
{
GVariantBuilder *b = user_data;
g_variant_builder_add (b, "t", data);
}
static gboolean
simple_endpoint_prepare_link (WpEndpoint * ep, guint32 stream_id,
WpEndpointLink * link, GVariant ** properties, GError ** error)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (ep);
GVariantBuilder b, *b_ports;
GVariant *v_ports;
/* Create a variant array with all the ports */
b_ports = g_variant_builder_new (G_VARIANT_TYPE ("at"));
g_ptr_array_foreach(self->proxies_port, proxies_port_foreach_func, b_ports);
v_ports = g_variant_builder_end (b_ports);
/* Set the properties */
g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add (&b, "{sv}", "node-id",
g_variant_new_uint32 (self->global_id));
g_variant_builder_add (&b, "{sv}", "ports", v_ports);
*properties = g_variant_builder_end (&b);
return TRUE;
}
static GVariant *
simple_endpoint_get_control_value (WpEndpoint * ep, guint32 control_id)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (ep);
switch (control_id) {
case CONTROL_VOLUME:
return g_variant_new_double (self->volume);
case CONTROL_MUTE:
return g_variant_new_boolean (self->mute);
default:
g_warning ("Unknown control id %u", control_id);
return NULL;
}
}
static gboolean
simple_endpoint_set_control_value (WpEndpoint * ep, guint32 control_id,
GVariant * value)
{
WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (ep);
char buf[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
float volume;
bool mute;
struct pw_node_proxy *node_proxy = NULL;
/* Get the node proxy */
node_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy_node));
switch (control_id) {
case CONTROL_VOLUME:
volume = g_variant_get_double (value);
g_debug("WpEndpoint:%p set volume control (%u) value, vol:%f", self,
control_id, volume);
pw_node_proxy_set_param (node_proxy,
SPA_PARAM_Props, 0,
spa_pod_builder_add_object (&b,
SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
SPA_PROP_volume, SPA_POD_Float(volume),
NULL));
break;
case CONTROL_MUTE:
mute = g_variant_get_boolean (value);
g_debug("WpEndpoint:%p set mute control (%u) value, mute:%d", self,
control_id, mute);
pw_node_proxy_set_param (node_proxy,
SPA_PARAM_Props, 0,
spa_pod_builder_add_object (&b,
SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
SPA_PROP_mute, SPA_POD_Bool(mute),
NULL));
break;
default:
g_warning ("Unknown control id %u", control_id);
return FALSE;
}
return TRUE;
}
static void
simple_endpoint_class_init (WpPipewireSimpleEndpointClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpEndpointClass *endpoint_class = (WpEndpointClass *) klass;
object_class->finalize = simple_endpoint_finalize;
object_class->set_property = simple_endpoint_set_property;
object_class->get_property = simple_endpoint_get_property;
endpoint_class->prepare_link = simple_endpoint_prepare_link;
endpoint_class->get_control_value = simple_endpoint_get_control_value;
endpoint_class->set_control_value = simple_endpoint_set_control_value;
g_object_class_install_property (object_class, PROP_GLOBAL_ID,
g_param_spec_uint ("global-id", "global-id",
"The global Id this endpoint refers to", 0, G_MAXUINT, 0,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_ROLE,
g_param_spec_string ("role", "role", "The role of the wrapped node", NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_CREATION_TIME,
g_param_spec_uint64 ("creation-time", "creation-time",
"The time that this endpoint was created, in monotonic time",
0, G_MAXUINT64, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_TARGET,
g_param_spec_string ("target", "target", "The target of the wrapped node", NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}
void
simple_endpoint_factory (WpFactory * factory, GType type,
GVariant * properties, GAsyncReadyCallback ready, gpointer user_data)
{
g_autoptr (WpCore) core = NULL;
const gchar *name, *media_class;
guint direction, global_id;
/* Make sure the type is correct */
g_return_if_fail (type == WP_TYPE_ENDPOINT);
/* Get the Core */
core = wp_factory_get_core (factory);
g_return_if_fail (core);
/* Get the properties */
if (!g_variant_lookup (properties, "name", "&s", &name))
return;
if (!g_variant_lookup (properties, "media-class", "&s", &media_class))
return;
if (!g_variant_lookup (properties, "direction", "u", &direction))
return;
if (!g_variant_lookup (properties, "global-id", "u", &global_id))
return;
g_async_initable_new_async (
simple_endpoint_get_type (), G_PRIORITY_DEFAULT, NULL, ready, user_data,
"core", core,
"name", name,
"media-class", media_class,
"direction", direction,
"global-id", global_id,
NULL);
}