diff --git a/modules/meson.build b/modules/meson.build index 9cb07773..a5da81e7 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -98,6 +98,7 @@ shared_library( [ 'module-endpoint-creation/generic-creation.c', 'module-endpoint-creation/limited-creation.c', + 'module-endpoint-creation/limited-creation-bluez5.c', 'module-endpoint-creation/parser-endpoint.c', 'module-endpoint-creation/parser-streams.c', 'module-endpoint-creation.c', diff --git a/modules/module-endpoint-creation.c b/modules/module-endpoint-creation.c index 75851593..7d99a95b 100644 --- a/modules/module-endpoint-creation.c +++ b/modules/module-endpoint-creation.c @@ -12,6 +12,7 @@ #include "module-endpoint-creation/generic-creation.h" #include "module-endpoint-creation/limited-creation.h" +#include "module-endpoint-creation/limited-creation-bluez5.h" struct _WpEndpointCreation { @@ -45,6 +46,12 @@ on_endpoint_created (WpLimitedCreation *li, WpSessionItem *ep, static WpLimitedCreation * create_device_limited_creation (WpEndpointCreation *self, WpProxy *device) { + const gchar *device_api = wp_proxy_get_property (device, PW_KEY_DEVICE_API); + + /* Bluez5 */ + if (g_strcmp0 (device_api, "bluez5") == 0) + return wp_limited_creation_bluez5_new (WP_DEVICE (device)); + /* Create future device limited creations here if needed */ return NULL; diff --git a/modules/module-endpoint-creation/limited-creation-bluez5.c b/modules/module-endpoint-creation/limited-creation-bluez5.c new file mode 100644 index 00000000..f7838c1d --- /dev/null +++ b/modules/module-endpoint-creation/limited-creation-bluez5.c @@ -0,0 +1,306 @@ +/* WirePlumber + * + * Copyright © 2020 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include + +#include "limited-creation-bluez5.h" + +struct _WpLimitedCreationBluez5 +{ + WpLimitedCreation parent; + + gboolean avail_profiles[2][2]; /* sink/source a2dp/sco */ + WpSessionItem *endpoints[2]; +}; + +G_DEFINE_TYPE (WpLimitedCreationBluez5, wp_limited_creation_bluez5, + WP_TYPE_LIMITED_CREATION) + +static void +endpoint_export_finish_cb (WpSessionItem * ep, GAsyncResult * res, + WpLimitedCreationBluez5 * self) +{ + g_autoptr (GError) error = NULL; + + if (!wp_session_item_export_finish (ep, res, &error)) { + wp_warning_object (self, "failed to export endpoint: %s", error->message); + return; + } + + wp_endpoint_creation_notify_endpoint_created (WP_LIMITED_CREATION (self), ep); +} + +static void +endpoint_activate_finish_cb (WpSessionItem * ep, GAsyncResult * res, + WpLimitedCreationBluez5 * self) +{ + g_autoptr (WpSession) session = NULL; + g_autoptr (GError) error = NULL; + + /* Finish */ + gboolean activate_ret = wp_session_item_activate_finish (ep, res, &error); + if (!activate_ret) { + wp_warning_object (self, "failed to activate endpoint: %s", error->message); + return; + } + + /* Only export if not already exported */ + if (!(wp_session_item_get_flags (ep) == WP_SI_FLAG_EXPORTED)) { + g_autoptr (WpSession) session = wp_limited_creation_lookup_session ( + WP_LIMITED_CREATION (self), WP_CONSTRAINT_TYPE_PW_PROPERTY, + "session.name", "=s", "audio", NULL); + if (!session) { + wp_warning_object (self, "could not find audio session for endpoint"); + return; + } + wp_session_item_export (ep, session, + (GAsyncReadyCallback) endpoint_export_finish_cb, self); + } +} + +static void +enable_endpoint (WpLimitedCreationBluez5 *self, WpNode *node, + WpDirection direction, guint32 priority) +{ + g_autoptr (WpDevice) device = wp_limited_creation_get_device ( + WP_LIMITED_CREATION (self)); + g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (device)); + + wp_info_object (self, "enabling endpoint %d", direction); + + /* Lookup node */ + node = wp_limited_creation_lookup_node (WP_LIMITED_CREATION (self), + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + PW_KEY_MEDIA_CLASS, "=s", + direction == WP_DIRECTION_INPUT ? "Audio/Sink" : "Audio/Source", + NULL); + + /* Create endpoint */ + if (!self->endpoints[direction]) + self->endpoints[direction] = wp_session_item_make (core, "si-bluez5-endpoint"); + g_return_if_fail (self->endpoints[direction]); + + /* Configure endpoint */ + { + g_auto (GVariantBuilder) b = + G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); + + g_variant_builder_add (&b, "{sv}", "device", + g_variant_new_uint64 ((guint64) device)); + g_variant_builder_add (&b, "{sv}", "name", + g_variant_new_printf ("Bluez5.%s.%s", + wp_proxy_get_property (WP_PROXY (device), PW_KEY_DEVICE_NAME), + direction == WP_DIRECTION_INPUT ? "Sink" : "Source")); + g_variant_builder_add (&b, "{sv}", "direction", + g_variant_new_uint32 (direction)); + g_variant_builder_add (&b, "{sv}", "a2dp-stream", + g_variant_new_boolean (self->avail_profiles[direction][0])); + g_variant_builder_add (&b, "{sv}", "sco-stream", + g_variant_new_boolean (self->avail_profiles[direction][1])); + g_variant_builder_add (&b, "{sv}", "node", + g_variant_new_uint64 ((guint64) node)); + g_variant_builder_add (&b, "{sv}", "priority", + g_variant_new_uint32 (priority)); + + g_return_if_fail (wp_session_item_configure (self->endpoints[direction], + g_variant_builder_end (&b))); + } + + /* Activate endpoint */ + wp_session_item_activate (self->endpoints[direction], + (GAsyncReadyCallback) endpoint_activate_finish_cb, self); +} + +static void +disable_endpoint (WpLimitedCreationBluez5 *self, WpDirection direction) +{ + wp_info_object (self, "disabling endpoint %d", direction); + + if (self->endpoints[direction]) { + wp_session_item_deactivate (self->endpoints[direction]); + wp_session_item_reset (self->endpoints[direction]); + } +} + +static WpNode * +lookup_node (WpLimitedCreationBluez5 *self, WpDirection direction) +{ + return wp_limited_creation_lookup_node (WP_LIMITED_CREATION (self), + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, + PW_KEY_MEDIA_CLASS, "=s", + direction == WP_DIRECTION_INPUT ? "Audio/Sink" : "Audio/Source", + NULL); +} + +static void +setup_endpoint (WpLimitedCreationBluez5 *self, WpNode *node, + WpDirection direction, guint32 priority) +{ + /* Enable endpoit if at least 1 profile is available, otherwise disable */ + if (self->avail_profiles[direction][0] || self->avail_profiles[direction][1]) + enable_endpoint (self, node, direction, priority); + else + disable_endpoint (self, direction); +} + +void +wp_limited_creation_bluez5_nodes_changed (WpLimitedCreation * ctx) +{ + WpLimitedCreationBluez5 *self = WP_LIMITED_CREATION_BLUEZ5 (ctx); + g_autoptr (WpNode) sink = NULL; + g_autoptr (WpNode) source = NULL; + + /* Lookup nodes */ + sink = lookup_node (self, WP_DIRECTION_INPUT); + source = lookup_node (self, WP_DIRECTION_OUTPUT); + + /* The nodes-changed event is also triggered when the nodes are removed, so + * the event is actually triggered twice when switching profiles. When both + * nodes are removed, we always make sure both endpoints are disabled and + * just return. The endpoints will be enabled in the next event */ + if (!sink && !source) { + disable_endpoint (self, WP_DIRECTION_INPUT); + disable_endpoint (self, WP_DIRECTION_OUTPUT); + return; + } + + /* Setup endpoints (at list one node must exist) */ + g_return_if_fail (sink || source); + setup_endpoint (self, sink, WP_DIRECTION_INPUT, 20); + setup_endpoint (self, source, WP_DIRECTION_OUTPUT, 20); +} + +static void +on_device_enum_profile_done (WpProxy *proxy, GAsyncResult *res, + WpLimitedCreationBluez5 *self) +{ + g_autoptr (WpIterator) profiles = NULL; + g_auto (GValue) item = G_VALUE_INIT; + g_autoptr (GError) error = NULL; + guint32 n_profiles = 0; + + /* Finish */ + profiles = wp_proxy_enum_params_finish (proxy, res, &error); + if (error) { + wp_warning_object (self, "failed to enum profiles in bluetooth device"); + return; + } + + /* Iterate all profiles */ + for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) { + WpSpaPod *pod = g_value_get_boxed (&item); + g_autoptr (WpSpaPodParser) pp = NULL; + gint index = 0; + const gchar *name = NULL; + const gchar *desc = NULL; + g_autoptr (WpSpaPod) classes = NULL; + + g_return_if_fail (pod); + g_return_if_fail (wp_spa_pod_is_object (pod)); + + /* Parse profile */ + { + g_autoptr (WpSpaPodParser) pp = wp_spa_pod_parser_new_object (pod, + "Profile", NULL); + g_return_if_fail (pp); + g_return_if_fail (wp_spa_pod_parser_get (pp, "index", "i", &index, NULL)); + if (index == 0) { + /* Skip profile 0 (Off) */ + wp_spa_pod_parser_end (pp); + continue; + } + g_return_if_fail (wp_spa_pod_parser_get (pp, "name", "s", &name, NULL)); + g_return_if_fail (wp_spa_pod_parser_get (pp, "description", "s", &desc, NULL)); + g_return_if_fail (wp_spa_pod_parser_get (pp, "classes", "P", &classes, NULL)); + wp_spa_pod_parser_end (pp); + } + + /* Parse profile classes */ + { + g_autoptr (WpIterator) it = wp_spa_pod_iterate (classes); + g_auto (GValue) v = G_VALUE_INIT; + g_return_if_fail (it); + g_return_if_fail (index > 0); + for (; wp_iterator_next (it, &v); g_value_unset (&v)) { + WpSpaPod *entry = g_value_get_boxed (&v); + g_autoptr (WpSpaPodParser) pp = NULL; + const gchar *media_class = NULL; + gint n_nodes = 0; + g_return_if_fail (entry); + pp = wp_spa_pod_parser_new_struct (entry); + g_return_if_fail (pp); + g_return_if_fail (wp_spa_pod_parser_get_string (pp, &media_class)); + g_return_if_fail (wp_spa_pod_parser_get_int (pp, &n_nodes)); + wp_spa_pod_parser_end (pp); + + /* Set available profiles matrix */ + if (g_strcmp0 ("Audio/Sink", media_class) == 0 && n_nodes > 0) + self->avail_profiles[WP_DIRECTION_INPUT][index - 1] = TRUE; + else if (g_strcmp0 ("Audio/Source", media_class) == 0 && n_nodes > 0) + self->avail_profiles[WP_DIRECTION_OUTPUT][index - 1] = TRUE;; + } + } + + n_profiles++; + } + + if (n_profiles == 0) + wp_warning_object (self, "bluetooth device does not support any profiles"); +} + +static void +wp_limited_creation_bluez5_constructed (GObject *object) +{ + WpLimitedCreationBluez5 *self = WP_LIMITED_CREATION_BLUEZ5 (object); + + g_autoptr (WpDevice) device = wp_limited_creation_get_device ( + WP_LIMITED_CREATION (self)); + g_return_if_fail (device); + wp_proxy_enum_params (WP_PROXY (device), "EnumProfile", NULL, NULL, + (GAsyncReadyCallback) on_device_enum_profile_done, self); + + G_OBJECT_CLASS (wp_limited_creation_bluez5_parent_class)->constructed (object); +} + +static void +wp_limited_creation_bluez5_finalize (GObject * object) +{ + WpLimitedCreationBluez5 *self = WP_LIMITED_CREATION_BLUEZ5 (object); + + g_clear_object (&self->endpoints[WP_DIRECTION_INPUT]); + g_clear_object (&self->endpoints[WP_DIRECTION_OUTPUT]); + + G_OBJECT_CLASS (wp_limited_creation_bluez5_parent_class)->finalize (object); +} + +static void +wp_limited_creation_bluez5_init (WpLimitedCreationBluez5 *self) +{ +} + +static void +wp_limited_creation_bluez5_class_init (WpLimitedCreationBluez5Class *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpLimitedCreationClass *parent_class = (WpLimitedCreationClass *) klass; + + object_class->constructed = wp_limited_creation_bluez5_constructed; + object_class->finalize = wp_limited_creation_bluez5_finalize; + + parent_class->nodes_changed = wp_limited_creation_bluez5_nodes_changed; +} + +WpLimitedCreation * +wp_limited_creation_bluez5_new (WpDevice *device) +{ + return g_object_new (wp_limited_creation_bluez5_get_type (), + "device", device, + NULL); +} diff --git a/modules/module-endpoint-creation/limited-creation-bluez5.h b/modules/module-endpoint-creation/limited-creation-bluez5.h new file mode 100644 index 00000000..1e41791b --- /dev/null +++ b/modules/module-endpoint-creation/limited-creation-bluez5.h @@ -0,0 +1,26 @@ +/* WirePlumber + * + * Copyright © 2020 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WIREPLUMBER_LIMITED_CREATION_BLUEZ5_H__ +#define __WIREPLUMBER_LIMITED_CREATION_BLUEZ5_H__ + +#include + +#include "limited-creation.h" + +G_BEGIN_DECLS + +#define WP_TYPE_LIMITED_CREATION_BLUEZ5 (wp_limited_creation_bluez5_get_type ()) +G_DECLARE_FINAL_TYPE (WpLimitedCreationBluez5, wp_limited_creation_bluez5, WP, + LIMITED_CREATION_BLUEZ5, WpLimitedCreation); + +WpLimitedCreation * wp_limited_creation_bluez5_new (WpDevice *device); + +G_END_DECLS + +#endif diff --git a/src/config/20-bluez5-audio-sink.endpoint b/src/config/20-bluez5-audio-sink.endpoint deleted file mode 100644 index 747af81f..00000000 --- a/src/config/20-bluez5-audio-sink.endpoint +++ /dev/null @@ -1,14 +0,0 @@ -[match-node] -properties = [ - { name = "media.class", value = "Audio/Sink" }, - { name = "device.api", value = "bluez5" }, -] - -[endpoint] -session = "audio" -type = "si-adapter" -streams = "audio-sink.streams" - -[endpoint.config] -enable-monitor = true -priority = 10 diff --git a/src/config/20-bluez5-audio-source.endpoint b/src/config/20-bluez5-audio-source.endpoint deleted file mode 100644 index cbce646a..00000000 --- a/src/config/20-bluez5-audio-source.endpoint +++ /dev/null @@ -1,12 +0,0 @@ -[match-node] -properties = [ - { name = "media.class", value = "Audio/Source" }, - { name = "device.api", value = "bluez5" }, -] - -[endpoint] -session = "audio" -type = "si-adapter" - -[endpoint.config] -priority = 10 diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index eb99a1f2..52301820 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -41,7 +41,7 @@ load-module C libwireplumber-module-si-bluez5-endpoint load-module C libwireplumber-module-monitor { "alsa": <{"factory": <"api.alsa.enum.udev">, "flags": <["use-adapter"]>}>, - #"bluez5": <{"factory": <"api.bluez5.enum.dbus">, "flags": <["local-nodes", "use-adapter"]>}>, + "bluez5": <{"factory": <"api.bluez5.enum.dbus">, "flags": <["local-nodes", "use-adapter"]>}>, "v4l2": <{"factory": <"api.v4l2.enum.udev">}> }