Files
wireplumber/modules/module-pw-bluez.c
George Kiagiadakis a63f2bb99b lib/wp: merge both WpRemote & WpRemotePipewire in WpCore
In practice we always create a remote and connect to pipewire.
Any other scenario is invalid, therefore, it is not justified
to be confused with so many classes for such small functionality.
This simplifies a lot the modules code.

Also, this commit exposes the pw_core and pw_remote objects
out of WpCore. This is in practice useful when dealing with low-level
pw and spa factories, which are used in the monitors. Let's not
add API wrappers for everything... Bindings will never use this
functionality anyway, since it depends on low level pipewire C API.
2019-09-07 17:55:46 +03:00

606 lines
15 KiB
C

/* WirePlumber
*
* Copyright © 2019 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
/**
* module-pw-bluez provides bluetooth device detection through pipewire
* and automatically creates pipewire audio nodes to play and capture audio
*/
#include <spa/utils/keys.h>
#include <spa/utils/names.h>
#include <spa/monitor/monitor.h>
#include <pipewire/pipewire.h>
#include <wp/wp.h>
enum wp_bluez_profile {
WP_BLUEZ_A2DP = 0,
WP_BLUEZ_HEADUNIT = 1, /* HSP/HFP Head Unit (Headsets) */
WP_BLUEZ_GATEWAY = 2 /* HSP/HFP Gateway (Phones) */
};
struct monitor {
struct spa_handle *handle;
struct spa_monitor *monitor;
struct spa_list device_list;
};
struct impl {
WpModule *module;
GHashTable *registered_endpoints;
/* The bluez monitor */
struct monitor monitor;
};
struct device {
struct impl *impl;
struct spa_list link;
uint32_t id;
struct pw_properties *props;
struct spa_handle *handle;
struct pw_proxy *proxy;
struct spa_device *device;
struct spa_hook device_listener;
struct spa_list node_list;
};
struct node {
struct impl *impl;
struct device *device;
struct spa_list link;
uint32_t id;
struct pw_properties *props;
struct pw_node *adapter;
struct pw_proxy *proxy;
};
static void
on_endpoint_created(GObject *initable, GAsyncResult *res, gpointer d)
{
struct impl *data = d;
WpEndpoint *endpoint = NULL;
guint global_id = 0;
GError *error = NULL;
/* Get the endpoint */
endpoint = wp_endpoint_new_finish(initable, res, NULL);
g_return_if_fail (endpoint);
/* Check for error */
if (error) {
g_clear_object (&endpoint);
g_warning ("Failed to create client endpoint: %s", error->message);
return;
}
/* Get the endpoint global id */
g_object_get (endpoint, "global-id", &global_id, NULL);
g_debug ("Created bluetooth endpoint for global id %d", global_id);
/* Register the endpoint and add it to the table */
wp_endpoint_register (endpoint);
g_hash_table_insert (data->registered_endpoints, GUINT_TO_POINTER(global_id),
endpoint);
}
static gboolean
parse_bluez_properties (WpProperties *props, const gchar **name,
const gchar **media_class, enum pw_direction *direction)
{
const char *local_name = NULL;
const char *local_media_class = NULL;
enum pw_direction local_direction;
enum wp_bluez_profile profile;
/* Get the name */
local_name = wp_properties_get (props, PW_KEY_NODE_NAME);
if (!local_name)
return FALSE;
/* Get the media class */
local_media_class = wp_properties_get (props, PW_KEY_MEDIA_CLASS);
if (!local_media_class)
return FALSE;
/* Get the direction */
if (g_str_has_prefix (local_media_class, "Audio/Sink"))
local_direction = PW_DIRECTION_INPUT;
else if (g_str_has_prefix (local_media_class, "Audio/Source"))
local_direction = PW_DIRECTION_OUTPUT;
else
return FALSE;
/* Get the bluez profile */
if (g_str_has_prefix (local_name, "bluez5.a2dp"))
profile = WP_BLUEZ_A2DP;
else if (g_str_has_prefix (local_name, "bluez5.hsp-hs"))
profile = WP_BLUEZ_HEADUNIT;
else if (g_str_has_prefix (local_name, "bluez5.hfp-hf"))
profile = WP_BLUEZ_HEADUNIT;
else if (g_str_has_prefix (local_name, "bluez5.hsp-ag"))
profile = WP_BLUEZ_GATEWAY;
else if (g_str_has_prefix (local_name, "bluez5.hfp-ag"))
profile = WP_BLUEZ_GATEWAY;
else
return FALSE;
/* Set the name */
if (name)
*name = local_name;
/* Set the media class */
if (media_class) {
switch (local_direction) {
case PW_DIRECTION_INPUT:
switch (profile) {
case WP_BLUEZ_A2DP:
*media_class = "Bluez/Sink/A2dp";
break;
case WP_BLUEZ_HEADUNIT:
*media_class = "Bluez/Sink/Headunit";
break;
case WP_BLUEZ_GATEWAY:
*media_class = "Bluez/Sink/Gateway";
break;
default:
break;
}
break;
case PW_DIRECTION_OUTPUT:
switch (profile) {
case WP_BLUEZ_A2DP:
*media_class = "Bluez/Source/A2dp";
break;
case WP_BLUEZ_HEADUNIT:
*media_class = "Bluez/Source/Headunit";
break;
case WP_BLUEZ_GATEWAY:
*media_class = "Bluez/Source/Gateway";
break;
}
break;
default:
break;
}
}
/* Set the direction */
if (direction)
*direction = local_direction;
return TRUE;
}
/* TODO: we need to find a better way to do this */
static gboolean
is_bluez_node (WpProperties *props)
{
const gchar *name = NULL;
/* Get the name */
name = wp_properties_get (props, PW_KEY_NODE_NAME);
if (!name)
return FALSE;
/* Check if it is a bluez device */
if (!g_str_has_prefix (name, "bluez5."))
return FALSE;
return TRUE;
}
static void
on_node_added (WpCore *core, WpProxy *proxy, struct impl *data)
{
const gchar *name, *media_class;
enum pw_direction direction;
GVariantBuilder b;
g_autoptr (WpProperties) props = NULL;
g_autoptr (GVariant) endpoint_props = NULL;
guint32 id = wp_proxy_get_global_id (proxy);
props = wp_proxy_get_global_properties (proxy);
g_return_if_fail(props);
/* Only handle bluez nodes */
if (!is_bluez_node (props))
return;
/* Parse the bluez properties */
if (!parse_bluez_properties (props, &name, &media_class, &direction)) {
g_critical ("failed to parse bluez properties");
return;
}
/* Set the properties */
g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add (&b, "{sv}",
"name", g_variant_new_take_string (
g_strdup_printf ("Bluez %u (%s)", id, name)));
g_variant_builder_add (&b, "{sv}",
"media-class", g_variant_new_string (media_class));
g_variant_builder_add (&b, "{sv}",
"direction", g_variant_new_uint32 (direction));
g_variant_builder_add (&b, "{sv}",
"proxy-node", g_variant_new_uint64 ((guint64) proxy));
endpoint_props = g_variant_builder_end (&b);
/* Create the endpoint async */
wp_factory_make (core, "pipewire-simple-endpoint", WP_TYPE_ENDPOINT,
endpoint_props, on_endpoint_created, data);
}
static void
on_node_removed (WpCore *core, WpProxy *proxy, struct impl *data)
{
WpEndpoint *endpoint = NULL;
guint32 id = wp_proxy_get_global_id (proxy);
/* Get the endpoint */
endpoint = g_hash_table_lookup (data->registered_endpoints,
GUINT_TO_POINTER(id));
if (!endpoint)
return;
/* Unregister the endpoint and remove it from the table */
wp_endpoint_unregister (endpoint);
g_hash_table_remove (data->registered_endpoints, GUINT_TO_POINTER(id));
}
static struct node *
create_node(struct impl *impl, struct device *dev, uint32_t id,
const struct spa_device_object_info *info)
{
g_autoptr (WpCore) core = wp_module_get_core (impl->module);
struct node *node;
const char *name, *profile;
struct pw_properties *props = NULL;
struct pw_factory *factory = NULL;
struct pw_node *adapter = NULL;
/* Check if the type is a node */
if (info->type != SPA_TYPE_INTERFACE_Node)
return NULL;
/* Get the bluez name */
name = pw_properties_get(dev->props, SPA_KEY_DEVICE_DESCRIPTION);
if (name == NULL)
name = pw_properties_get(dev->props, SPA_KEY_DEVICE_NAME);
if (name == NULL)
name = pw_properties_get(dev->props, SPA_KEY_DEVICE_NICK);
if (name == NULL)
name = pw_properties_get(dev->props, SPA_KEY_DEVICE_ALIAS);
if (name == NULL)
name = "bluetooth-device";
/* Get the bluez profile */
profile = spa_dict_lookup(info->props, SPA_KEY_API_BLUEZ5_PROFILE);
if (!profile)
profile = "null";
/* Find the factory */
factory = pw_core_find_factory (wp_core_get_pw_core (core), "adapter");
g_return_val_if_fail (factory, NULL);
/* Create the properties */
props = pw_properties_new_dict(info->props);
pw_properties_setf(props, PW_KEY_NODE_NAME, "bluez5.%s.%s", profile, name);
pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, name);
pw_properties_set(props, PW_KEY_FACTORY_NAME, info->factory_name);
/* Create the adapter */
adapter = pw_factory_create_object(factory, NULL, PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE_PROXY, props, 0);
if (!adapter) {
pw_properties_free(props);
return NULL;
}
/* Create the node */
node = g_slice_new0(struct node);
node->impl = impl;
node->device = dev;
node->id = id;
node->props = props;
node->adapter = adapter;
node->proxy = pw_remote_export (wp_core_get_pw_remote (core),
PW_TYPE_INTERFACE_Node, props, adapter, 0);
if (!node->proxy) {
pw_properties_free(props);
g_slice_free (struct node, node);
return NULL;
}
/* Add the node to the list */
spa_list_append(&dev->node_list, &node->link);
return node;
}
static void
update_node(struct impl *impl, struct device *dev, struct node *node,
const struct spa_device_object_info *info)
{
/* Just update the properties */
pw_properties_update(node->props, info->props);
}
static void destroy_node(struct impl *impl, struct device *dev, struct node *node)
{
/* Remove the node from the list */
spa_list_remove(&node->link);
/* Destroy the proxy node */
pw_proxy_destroy(node->proxy);
/* Destroy the node */
g_slice_free (struct node, node);
}
static struct node *
find_node(struct device *dev, uint32_t id)
{
struct node *node;
/* Find the node in the list */
spa_list_for_each(node, &dev->node_list, link) {
if (node->id == id)
return node;
}
return NULL;
}
static void
device_object_info(void *data, uint32_t id,
const struct spa_device_object_info *info)
{
struct device *dev = data;
struct impl *impl = dev->impl;
struct node *node = NULL;
/* Find the node */
node = find_node(dev, id);
if (info) {
/* Just update the node if it already exits, otherwise create it */
if (node)
update_node(impl, dev, node, info);
else
create_node(impl, dev, id, info);
} else {
/* Just remove the node if it already exists */
if (node)
destroy_node(impl, dev, node);
}
}
static const struct spa_device_events device_events = {
SPA_VERSION_DEVICE_EVENTS,
.object_info = device_object_info
};
static struct device*
create_device(struct impl *impl, uint32_t id,
const struct spa_monitor_object_info *info)
{
g_autoptr (WpCore) core = wp_module_get_core (impl->module);
struct device *dev;
struct spa_handle *handle;
int res;
void *iface;
/* Check if the type is a device */
if (info->type != SPA_TYPE_INTERFACE_Device)
return NULL;
/* Load the device handle */
handle = pw_core_load_spa_handle (wp_core_get_pw_core (core),
info->factory_name, info->props);
if (!handle)
return NULL;
/* Get the handle interface */
res = spa_handle_get_interface(handle, info->type, &iface);
if (res < 0) {
pw_unload_spa_handle(handle);
return NULL;
}
/* Create the device */
dev = g_slice_new0(struct device);
dev->impl = impl;
dev->id = id;
dev->handle = handle;
dev->device = iface;
dev->props = pw_properties_new_dict(info->props);
dev->proxy = pw_remote_export (wp_core_get_pw_remote (core),
info->type, dev->props, dev->device, 0);
if (!dev->proxy) {
pw_unload_spa_handle(handle);
return NULL;
}
spa_list_init(&dev->node_list);
/* Add device listener for events */
spa_device_add_listener(dev->device, &dev->device_listener, &device_events,
dev);
/* Add the device to the list */
spa_list_append(&impl->monitor.device_list, &dev->link);
return dev;
}
static void
update_device(struct impl *impl, struct device *dev,
const struct spa_monitor_object_info *info)
{
/* Update the properties of the device */
pw_properties_update(dev->props, info->props);
}
static void
destroy_device(struct impl *impl, struct device *dev)
{
struct node *node;
/* Remove the device from the list */
spa_list_remove(&dev->link);
/* Remove the device listener */
spa_hook_remove(&dev->device_listener);
/* Destry all the nodes that the device has */
spa_list_consume(node, &dev->node_list, link)
destroy_node(impl, dev, node);
/* Destroy the device proxy */
pw_proxy_destroy(dev->proxy);
/* Unload the device handle */
pw_unload_spa_handle(dev->handle);
/* Destroy the object */
g_slice_free (struct device, dev);
}
static struct device *
find_device(struct impl *impl, uint32_t id)
{
struct device *dev;
/* Find the device in the list */
spa_list_for_each(dev, &impl->monitor.device_list, link) {
if (dev->id == id)
return dev;
}
return NULL;
}
static int
monitor_object_info(gpointer data, uint32_t id,
const struct spa_monitor_object_info *info)
{
struct impl *impl = data;
struct device *dev = NULL;
/* Find the device */
dev = find_device(impl, id);
if (info) {
/* Just update the device if it already exits, otherwise create it */
if (dev)
update_device(impl, dev, info);
else
if (!create_device(impl, id, info))
return -ENOMEM;
} else {
/* Just remove the device if it already exists, otherwise return error */
if (dev)
destroy_device(impl, dev);
else
return -ENODEV;
}
return 0;
}
static const struct spa_monitor_callbacks monitor_callbacks =
{
SPA_VERSION_MONITOR_CALLBACKS,
.object_info = monitor_object_info,
};
static void
start_monitor (WpCore *core, WpRemoteState state, gpointer data)
{
struct impl *impl = data;
struct spa_handle *handle;
int res;
void *iface;
/* Load the monitor handle */
handle = pw_core_load_spa_handle (wp_core_get_pw_core (core),
SPA_NAME_API_BLUEZ5_MONITOR, NULL);
if (!handle) {
g_message ("SPA bluez5 plugin could not be loaded; is it installed?");
return;
}
/* Get the handle interface */
res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Monitor, &iface);
if (res < 0) {
g_critical ("module-pw-alsa-udev cannot get monitor interface");
pw_unload_spa_handle(handle);
return;
}
/* Init the monitor data */
impl->monitor.handle = handle;
impl->monitor.monitor = iface;
spa_list_init(&impl->monitor.device_list);
/* Set the monitor callbacks */
spa_monitor_set_callbacks(impl->monitor.monitor, &monitor_callbacks, impl);
}
static void
module_destroy (gpointer data)
{
struct impl *impl = data;
/* Set to NULL as we don't own the reference */
impl->module = NULL;
/* Destroy the registered endpoints table */
g_hash_table_unref(impl->registered_endpoints);
impl->registered_endpoints = NULL;
/* Clean up */
g_slice_free (struct impl, impl);
}
void
wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
{
struct impl *impl;
/* Create the module data */
impl = g_slice_new0(struct impl);
impl->module = module;
impl->registered_endpoints = g_hash_table_new_full (g_direct_hash,
g_direct_equal, NULL, (GDestroyNotify)g_object_unref);
/* Set destroy callback for impl */
wp_module_set_destroy_callback (module, module_destroy, impl);
/* Add the spa lib */
pw_core_add_spa_lib (wp_core_get_pw_core (core),
"api.bluez5.*", "bluez5/libspa-bluez5");
/* Start the monitor when the connected callback is triggered */
g_signal_connect(core, "remote-state-changed::connected",
(GCallback) start_monitor, impl);
/* Register the global addded/removed callbacks */
g_signal_connect(core, "remote-global-added::node",
(GCallback) on_node_added, impl);
g_signal_connect(core, "remote-global-removed::node",
(GCallback) on_node_removed, impl);
}