From 770fe8c7033c5f5836ea1753180622913630f336 Mon Sep 17 00:00:00 2001 From: Julian Bouzas <julian.bouzas@collabora.com> Date: Wed, 28 Aug 2019 13:30:59 -0400 Subject: [PATCH] simple-policy: handle bluez endpoints --- modules/module-simple-policy.c | 345 ++++++++++++++++++++++++++------- 1 file changed, 271 insertions(+), 74 deletions(-) diff --git a/modules/module-simple-policy.c b/modules/module-simple-policy.c index 86f26e67..7e413387 100644 --- a/modules/module-simple-policy.c +++ b/modules/module-simple-policy.c @@ -7,6 +7,7 @@ */ #include <wp/wp.h> +#include <pipewire/pipewire.h> enum { DIRECTION_SINK = 0, @@ -125,41 +126,80 @@ select_endpoint (WpSimplePolicy *self, gint direction, WpEndpoint *ep, } static gboolean -select_new_endpoint (WpSimplePolicy *self) +try_select_new_endpoint (WpSimplePolicy *self, gint direction, + const gchar *media_class) { g_autoptr (WpCore) core = NULL; g_autoptr (GPtrArray) ptr_array = NULL; - const gchar *media_class = NULL; WpEndpoint *other; guint32 control_id; - gint direction, i; + gint i; + + /* Get the list of endpoints matching the media class */ + core = wp_policy_get_core (WP_POLICY (self)); + ptr_array = wp_endpoint_find (core, media_class); + + /* Find the endpoint in the list */ + for (i = 0; i < (ptr_array ? ptr_array->len : 0); i++) { + other = g_ptr_array_index (ptr_array, i); + if (g_str_has_prefix (media_class, "Alsa/")) { + /* If Alsa, select the "selected" endpoint */ + control_id = + wp_endpoint_find_control (other, WP_STREAM_ID_NONE, "selected"); + if (control_id == WP_CONTROL_ID_NONE) + continue; + select_endpoint (self, direction, other, control_id); + return TRUE; + } else { + /* If non-Alsa (Bluez and Stream), select the first endpoint */ + select_endpoint (self, direction, other, WP_CONTROL_ID_NONE); + return TRUE; + } + } + + return FALSE; +} + +static gboolean +select_new_endpoint (WpSimplePolicy *self) +{ + gint direction; + const gchar *bluez_headunit_media_class = NULL; + const gchar *bluez_a2dp_media_class = NULL; + const gchar *alsa_media_class = NULL; if (!self->selected[DIRECTION_SINK]) { direction = DIRECTION_SINK; - media_class = "Alsa/Sink"; + bluez_headunit_media_class = "Bluez/Sink/Headunit"; + bluez_a2dp_media_class = "Bluez/Sink/A2dp"; + alsa_media_class = "Alsa/Sink"; } else if (!self->selected[DIRECTION_SOURCE]) { direction = DIRECTION_SOURCE; - media_class = "Alsa/Source"; + bluez_headunit_media_class = "Bluez/Source/Headunit"; + bluez_a2dp_media_class = "Bluez/Source/A2dp"; + alsa_media_class = "Alsa/Source"; } else return G_SOURCE_REMOVE; - core = wp_policy_get_core (WP_POLICY (self)); - - /* Get all the endpoints with the same media class */ - ptr_array = wp_endpoint_find (core, media_class); + /* Bluez has higher priority than Alsa. Bluez A2DP profile has lower + * priority than Bluez non-gatewat profile (Headunit). Bluez Gateway profiles + * are not handled here because they always need to be linked with Alsa + * endpoints, so the priority list is as folows (from higher to lower): + * - Bluez Headunit + * - Bluez A2DP + * - Alsa + */ - /* select the first available that has the "selected" control */ - for (i = 0; i < (ptr_array ? ptr_array->len : 0); i++) { - other = g_ptr_array_index (ptr_array, i); + /* Try to select a Bluez Headunit endpoint */ + if (try_select_new_endpoint (self, direction, bluez_headunit_media_class)) + return G_SOURCE_REMOVE; - control_id = - wp_endpoint_find_control (other, WP_STREAM_ID_NONE, "selected"); - if (control_id == WP_CONTROL_ID_NONE) - continue; + /* Try to select a Bluez A2dp endpoint */ + if (try_select_new_endpoint (self, direction, bluez_a2dp_media_class)) + return G_SOURCE_REMOVE; - select_endpoint (self, direction, other, control_id); - break; - } + /* Try to select an Alsa endpoint */ + try_select_new_endpoint (self, direction, alsa_media_class); return G_SOURCE_REMOVE; } @@ -264,42 +304,32 @@ on_endpoint_link_created(GObject *initable, GAsyncResult *res, gpointer d) } } -static void -handle_client (WpPolicy *policy, WpEndpoint *ep) +static gboolean +link_endpoint (WpPolicy *policy, WpEndpoint *ep, GVariant *target_props) { - const char *media_class = wp_endpoint_get_media_class(ep); - GVariantDict d; g_autoptr (WpCore) core = NULL; g_autoptr (WpEndpoint) target = NULL; guint32 stream_id; + guint direction; gboolean is_capture = FALSE; - g_autofree gchar *role, *target_name = NULL; - - /* Detect if the client is doing capture or playback */ - is_capture = g_str_has_prefix (media_class, "Stream/Input"); - - /* Locate the target endpoint */ - g_variant_dict_init (&d, NULL); - g_variant_dict_insert (&d, "action", "s", "link"); - g_variant_dict_insert (&d, "media.class", "s", - is_capture ? "Alsa/Source" : "Alsa/Sink"); - - g_object_get (ep, "role", &role, NULL); - if (role) - g_variant_dict_insert (&d, "media.role", "s", role); - - g_object_get (ep, "target", &target_name, NULL); - if (target_name) - g_variant_dict_insert (&d, "media.name", "s", target_name); - /* TODO: more properties are needed here */ + /* Check if the endpoint is capture or not */ + direction = wp_endpoint_get_direction (WP_ENDPOINT (ep)); + switch (direction) { + case PW_DIRECTION_INPUT: + is_capture = TRUE; + break; + case PW_DIRECTION_OUTPUT: + is_capture = FALSE; + break; + default: + return FALSE; + } core = wp_policy_get_core (policy); - target = wp_policy_find_endpoint (core, g_variant_dict_end (&d), &stream_id); - if (!target) { - g_warning ("Could not find target endpoint"); - return; - } + target = wp_policy_find_endpoint (core, target_props, &stream_id); + if (!target) + return FALSE; /* if the client is already linked... */ if (wp_endpoint_is_linked (ep)) { @@ -315,7 +345,7 @@ handle_client (WpPolicy *policy, WpEndpoint *ep) /* ... do nothing if it's already linked to the correct target */ g_debug ("Client '%s' already linked correctly", wp_endpoint_get_name (ep)); - return; + return TRUE; } else { /* ... or else unlink it and continue */ g_debug ("Unlink client '%s' from its previous target", @@ -340,7 +370,7 @@ handle_client (WpPolicy *policy, WpEndpoint *ep) wp_endpoint_unlink (target); } - /* Link the client with the target */ + /* Link the endpoint with the target */ if (is_capture) { wp_endpoint_link_new (core, target, stream_id, ep, 0, on_endpoint_link_created, NULL); @@ -349,7 +379,103 @@ handle_client (WpPolicy *policy, WpEndpoint *ep) on_endpoint_link_created, NULL); } - return; + return TRUE; +} + +static void +handle_client (WpPolicy *policy, WpEndpoint *ep) +{ + const char *media_class = wp_endpoint_get_media_class(ep); + GVariantDict d; + gboolean is_capture = FALSE; + const gchar *role, *target_name = NULL; + + /* Detect if the client is doing capture or playback */ + is_capture = g_str_has_prefix (media_class, "Stream/Input"); + + /* All Stream client endpoints need to be linked with a Bluez non-gateway + * endpoint if any. If there is no Bluez non-gateway endpoints, the Stream + * client needs to be linked with a Bluez A2DP endpoint. Finally, if none + * of the previous endpoints are found, the Stream client needs to be linked + * with an Alsa endpoint. + */ + + /* Link endpoint with Bluez non-gateway target endpoint */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "action", "s", "link"); + g_variant_dict_insert (&d, "media.class", "s", + is_capture ? "Bluez/Source/Headunit" : "Bluez/Sink/Headunit"); + if (link_endpoint (policy, ep, g_variant_dict_end (&d))) + return; + + /* Link endpoint with Bluez A2DP target endpoint */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "action", "s", "link"); + g_variant_dict_insert (&d, "media.class", "s", + is_capture ? "Bluez/Source/A2dp" : "Bluez/Sink/A2dp"); + if (link_endpoint (policy, ep, g_variant_dict_end (&d))) + return; + + /* Link endpoint with Alsa target endpoint */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "action", "s", "link"); + g_variant_dict_insert (&d, "media.class", "s", + is_capture ? "Alsa/Source" : "Alsa/Sink"); + g_object_get (ep, "role", &role, NULL); + if (role) + g_variant_dict_insert (&d, "media.role", "s", role); + g_object_get (ep, "target", &target_name, NULL); + if (target_name) + g_variant_dict_insert (&d, "media.name", "s", target_name); + if (!link_endpoint (policy, ep, g_variant_dict_end (&d))) + g_info ("Could not find alsa target endpoint for client stream"); +} + +static void +handle_bluez_non_gateway (WpPolicy *policy, WpEndpoint *ep) +{ + GVariantDict d; + const char *media_class = wp_endpoint_get_media_class(ep); + gboolean is_sink = FALSE; + + /* All bluetooth non-gateway endpoints (A2DP/HSP_HS/HFP_HF) always + * need to be linked with the stream endpoints so that the computer + * does not play any sound + */ + + /* Detect if the client is a sink or not */ + is_sink = g_str_has_prefix (media_class, "Bluez/Sink"); + + /* Link endpoint with Stream target endpoint */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "action", "s", "link"); + g_variant_dict_insert (&d, "media.class", "s", + is_sink ? "Stream/Output/Audio" : "Stream/Input/Audio"); + if (!link_endpoint (policy, ep, g_variant_dict_end (&d))) + g_info ("Could not find stream target endpoint for non-gateway bluez"); +} + +static void +handle_bluez_gateway (WpPolicy *policy, WpEndpoint *ep) +{ + /* All bluetooth gateway endpoints (HSP_GW/HFP_GW) always need to + * be linked with the alsa endpoints so that the computer can act + * as a head unit + */ + GVariantDict d; + const char *media_class = wp_endpoint_get_media_class(ep); + gboolean is_sink = FALSE; + + /* Detect if the client is a sink or not */ + is_sink = g_str_has_prefix (media_class, "Bluez/Sink"); + + /* Link endpoint with Alsa target endpoint */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "action", "s", "link"); + g_variant_dict_insert (&d, "media.class", "s", + is_sink ? "Alsa/Source" : "Alsa/Sink"); + if (!link_endpoint (policy, ep, g_variant_dict_end (&d))) + g_info ("Could not find alsa target endpoint for gateway bluez"); } static gint @@ -393,35 +519,103 @@ compare_client_priority (gconstpointer a, gconstpointer b, gpointer user_data) return ret; } -static gboolean -simple_policy_rescan_in_idle (WpSimplePolicy *self) +static gint +compare_bluez_non_gateway_priority (gconstpointer a, gconstpointer b, + gpointer user_data) +{ + WpEndpoint *ae = *(const gpointer *) a; + WpEndpoint *be = *(const gpointer *) b; + const char *a_media_class, *b_media_class; + gint a_priority, b_priority; + + /* Bluez priorities (Gateway is a different case): + - Headset (1) + - A2dp (0) + */ + + /* Get endpoint A priority */ + a_media_class = wp_endpoint_get_media_class(ae); + a_priority = g_str_has_suffix (a_media_class, "Headset") ? 1 : 0; + + /* Get endpoint B priority */ + b_media_class = wp_endpoint_get_media_class(be); + b_priority = g_str_has_suffix (b_media_class, "Headset") ? 1 : 0; + + /* Return the difference of both priorities */ + return a_priority - b_priority; +} + +static gint +compare_bluez_gateway_priority (gconstpointer a, gconstpointer b, + gpointer user_data) +{ + /* Since Bluez Gateway profile does not have any priorities, just + * return positive to indicate endpoint A has higher priority than + * endpoint B */ + return 1; +} + +static void +rescan_sink_endpoints (WpSimplePolicy *self, const gchar *media_class, + void (*handler) (WpPolicy *policy, WpEndpoint *ep)) { g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self)); g_autoptr (GPtrArray) endpoints = NULL; WpEndpoint *ep; gint i; - g_debug ("rescanning for clients that need linking"); - - endpoints = wp_endpoint_find (core, "Stream/Input/Audio"); + endpoints = wp_endpoint_find (core, media_class); if (endpoints) { - /* link all capture clients */ + /* link all sink endpoints */ for (i = 0; i < endpoints->len; i++) { ep = g_ptr_array_index (endpoints, i); - handle_client (WP_POLICY (self), ep); + handler (WP_POLICY (self), ep); } } +} - endpoints = wp_endpoint_find (core, "Stream/Output/Audio"); +static void +rescan_source_endpoints (WpSimplePolicy *self, const gchar *media_class, + void (*handle) (WpPolicy *policy, WpEndpoint *ep), + GCompareDataFunc comp_func) +{ + g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self)); + g_autoptr (GPtrArray) endpoints = NULL; + WpEndpoint *ep; + + endpoints = wp_endpoint_find (core, media_class); if (endpoints && endpoints->len > 0) { - /* sort based on role priorities */ - g_ptr_array_sort_with_data (endpoints, compare_client_priority, - self->role_priorities); + /* sort based on priorities */ + g_ptr_array_sort_with_data (endpoints, comp_func, self->role_priorities); - /* link the highest priority client */ + /* link the highest priority */ ep = g_ptr_array_index (endpoints, 0); - handle_client (WP_POLICY (self), ep); + handle (WP_POLICY (self), ep); } +} + +static gboolean +simple_policy_rescan_in_idle (WpSimplePolicy *self) +{ + /* Alsa endpoints are never handled */ + + /* Handle clients */ + rescan_sink_endpoints (self, "Stream/Input/Audio", handle_client); + rescan_source_endpoints (self, "Stream/Output/Audio", handle_client, + compare_client_priority); + + /* Handle Bluez non-gateway */ + rescan_sink_endpoints (self, "Bluez/Sink/Headunit", handle_bluez_non_gateway); + rescan_source_endpoints (self, "Bluez/Source/Headunit", + handle_bluez_non_gateway, compare_bluez_non_gateway_priority); + rescan_sink_endpoints (self, "Bluez/Sink/A2dp", handle_bluez_non_gateway); + rescan_source_endpoints (self, "Bluez/Source/A2dp", handle_bluez_non_gateway, + compare_bluez_non_gateway_priority); + + /* Handle Bluez gateway */ + rescan_sink_endpoints (self, "Bluez/Sink/Gateway", handle_bluez_gateway); + rescan_source_endpoints (self, "Bluez/Source/Gateway", + handle_bluez_gateway, compare_bluez_gateway_priority); self->pending_rescan = 0; return G_SOURCE_REMOVE; @@ -439,18 +633,17 @@ static gboolean simple_policy_handle_endpoint (WpPolicy *policy, WpEndpoint *ep) { WpSimplePolicy *self = WP_SIMPLE_POLICY (policy); - const char *media_class = NULL; + const char *media_class = wp_endpoint_get_media_class(ep); - /* TODO: For now we only accept audio stream clients */ - media_class = wp_endpoint_get_media_class(ep); - if (!g_str_has_prefix (media_class, "Stream") || - !g_str_has_suffix (media_class, "Audio")) { - return FALSE; + /* Schedule rescan only if endpoint is audio stream or bluez */ + if ((g_str_has_prefix (media_class, "Stream") && + g_str_has_suffix (media_class, "Audio")) || + g_str_has_prefix (media_class, "Bluez")) { + simple_policy_rescan (self); + return TRUE; } - /* Schedule a rescan that will handle the endpoint */ - simple_policy_rescan (self); - return TRUE; + return FALSE; } static WpEndpoint * @@ -478,7 +671,7 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props, return NULL; /* Find the endpoint with the matching name, otherwise get the one with the - * "selected" flag */ + * "selected" flag (if it is an alsa endpoint) */ for (i = 0; i < ptr_array->len; i++) { ep = g_ptr_array_index (ptr_array, i); if (name) { @@ -486,7 +679,7 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props, g_object_ref (ep); goto select_stream; } - } else { + } else if (g_str_has_prefix (media_class, "Alsa/")) { g_autoptr (GVariant) value = NULL; guint id; @@ -506,6 +699,10 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props, ep = (ptr_array->len > 0) ? g_object_ref (g_ptr_array_index (ptr_array, 0)) : NULL; + /* Don't select any stream if it is not an alsa endpoint */ + if (!g_str_has_prefix (media_class, "Alsa/")) + return ep; + select_stream: g_variant_lookup (props, "media.role", "&s", &role); if (!g_strcmp0 (action, "mixer") && !g_strcmp0 (role, "Master")) -- GitLab