mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-12-16 08:56:45 -05:00
bluez5: basic codec-switching framework
This commit is contained in:
parent
0908588d0c
commit
e860f2bb4e
7 changed files with 537 additions and 14 deletions
|
|
@ -69,6 +69,10 @@ struct spa_bt_monitor {
|
|||
uint32_t count;
|
||||
uint32_t id;
|
||||
|
||||
/*
|
||||
* Lists of BlueZ objects, kept up-to-date by following DBus events
|
||||
* initiated by BlueZ. Object lifetime is also determined by that.
|
||||
*/
|
||||
struct spa_list adapter_list;
|
||||
struct spa_list device_list;
|
||||
struct spa_list remote_endpoint_list;
|
||||
|
|
@ -101,6 +105,34 @@ struct spa_bt_remote_endpoint {
|
|||
bool delay_reporting;
|
||||
};
|
||||
|
||||
/*
|
||||
* Codec switching tries various codec/remote endpoint combinations
|
||||
* in order, until an acceptable one is found. This triggers BlueZ
|
||||
* to initiate DBus calls that result to the creation of a transport
|
||||
* with the desired capabilities.
|
||||
* The codec switch struct tracks candidates still to be tried.
|
||||
*/
|
||||
struct spa_bt_a2dp_codec_switch {
|
||||
struct spa_bt_device *device;
|
||||
struct spa_list device_link;
|
||||
|
||||
DBusPendingCall *pending;
|
||||
|
||||
uint32_t profile;
|
||||
|
||||
/*
|
||||
* Called asynchronously, so endpoint paths instead of pointers (which may be
|
||||
* invalidated in the meantime).
|
||||
*/
|
||||
const struct a2dp_codec **codecs;
|
||||
char **paths;
|
||||
|
||||
const struct a2dp_codec **codec_iter; /**< outer iterator over codecs */
|
||||
char **path_iter; /**< inner iterator over endpoint paths */
|
||||
|
||||
size_t num_paths;
|
||||
};
|
||||
|
||||
/*
|
||||
* SCO socket connect may fail with ECONNABORTED if it is done too soon after
|
||||
* previous close. To avoid this in cases where nodes are toggled between
|
||||
|
|
@ -389,6 +421,9 @@ static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const
|
|||
d->path = strdup(path);
|
||||
spa_list_init(&d->remote_endpoint_list);
|
||||
spa_list_init(&d->transport_list);
|
||||
spa_list_init(&d->codec_switch_list);
|
||||
|
||||
spa_hook_list_init(&d->listener_list);
|
||||
|
||||
spa_list_prepend(&monitor->device_list, &d->link);
|
||||
|
||||
|
|
@ -397,9 +432,12 @@ static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const
|
|||
|
||||
static int device_stop_timer(struct spa_bt_device *device);
|
||||
|
||||
static void a2dp_codec_switch_free(struct spa_bt_a2dp_codec_switch *sw);
|
||||
|
||||
static void device_free(struct spa_bt_device *device)
|
||||
{
|
||||
struct spa_bt_remote_endpoint *ep;
|
||||
struct spa_bt_a2dp_codec_switch *sw;
|
||||
struct spa_bt_transport *t;
|
||||
struct spa_bt_monitor *monitor = device->monitor;
|
||||
|
||||
|
|
@ -419,6 +457,10 @@ static void device_free(struct spa_bt_device *device)
|
|||
t->device = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
spa_list_consume(sw, &device->codec_switch_list, device_link)
|
||||
a2dp_codec_switch_free(sw);
|
||||
|
||||
spa_list_remove(&device->link);
|
||||
free(device->path);
|
||||
free(device->alias);
|
||||
|
|
@ -1335,6 +1377,362 @@ static const struct spa_bt_transport_implementation transport_impl = {
|
|||
.release = transport_release,
|
||||
};
|
||||
|
||||
static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, int key_type_int, void* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size);
|
||||
|
||||
static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *userdata);
|
||||
|
||||
static int a2dp_codec_switch_cmp(const void *a, const void *b);
|
||||
|
||||
static struct spa_bt_a2dp_codec_switch *a2dp_codec_switch_cmp_sw; /* global for qsort */
|
||||
|
||||
static void a2dp_codec_switch_free(struct spa_bt_a2dp_codec_switch *sw)
|
||||
{
|
||||
char **p;
|
||||
|
||||
if (sw->pending != NULL) {
|
||||
dbus_pending_call_cancel(sw->pending);
|
||||
dbus_pending_call_unref(sw->pending);
|
||||
}
|
||||
|
||||
if (sw->device != NULL)
|
||||
spa_list_remove(&sw->device_link);
|
||||
|
||||
if (sw->paths != NULL)
|
||||
for (p = sw->paths; *p != NULL; ++p)
|
||||
free(*p);
|
||||
|
||||
free(sw->paths);
|
||||
free(sw->codecs);
|
||||
free(sw);
|
||||
}
|
||||
|
||||
static void a2dp_codec_switch_next(struct spa_bt_a2dp_codec_switch *sw)
|
||||
{
|
||||
spa_assert(*sw->codec_iter != NULL && *sw->path_iter != NULL);
|
||||
|
||||
++sw->path_iter;
|
||||
if (*sw->path_iter == NULL) {
|
||||
++sw->codec_iter;
|
||||
sw->path_iter = sw->paths;
|
||||
}
|
||||
}
|
||||
|
||||
static bool a2dp_codec_switch_process_current(struct spa_bt_a2dp_codec_switch *sw)
|
||||
{
|
||||
struct spa_bt_remote_endpoint *ep;
|
||||
const struct a2dp_codec *codec;
|
||||
uint8_t config[A2DP_MAX_CAPS_SIZE];
|
||||
char *local_endpoint_base;
|
||||
char *local_endpoint = NULL;
|
||||
int res, config_size;
|
||||
dbus_bool_t dbus_ret;
|
||||
const char *str;
|
||||
DBusMessage *m;
|
||||
DBusMessageIter iter, d;
|
||||
int i;
|
||||
|
||||
/* Try setting configuration for current codec on current endpoint in list */
|
||||
|
||||
codec = *sw->codec_iter;
|
||||
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: consider codec %s for remote endpoint %s",
|
||||
sw, (*sw->codec_iter)->name, *sw->path_iter);
|
||||
|
||||
ep = device_remote_endpoint_find(sw->device, *sw->path_iter);
|
||||
|
||||
if (ep == NULL || ep->capabilities == NULL || ep->uuid == NULL) {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: endpoint %s not valid, try next",
|
||||
*sw->path_iter);
|
||||
goto next;
|
||||
}
|
||||
|
||||
/* Setup and check compatible configuration */
|
||||
if (ep->codec != codec->codec_id) {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: different codec, try next", sw);
|
||||
goto next;
|
||||
}
|
||||
|
||||
if (!(sw->profile & spa_bt_profile_from_uuid(ep->uuid))) {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: wrong uuid (%s) for profile, try next",
|
||||
sw, ep->uuid);
|
||||
goto next;
|
||||
}
|
||||
|
||||
if (sw->profile & SPA_BT_PROFILE_A2DP_SINK) {
|
||||
local_endpoint_base = A2DP_SOURCE_ENDPOINT;
|
||||
} else if (sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) {
|
||||
local_endpoint_base = A2DP_SINK_ENDPOINT;
|
||||
} else {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: bad profile (%d), try next",
|
||||
sw, sw->profile);
|
||||
goto next;
|
||||
}
|
||||
|
||||
if (a2dp_codec_to_endpoint(codec, local_endpoint_base, &local_endpoint) < 0) {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: no endpoint for codec %s, try next",
|
||||
sw, codec->name);
|
||||
goto next;
|
||||
}
|
||||
|
||||
res = codec->select_config(codec, 0, ep->capabilities, ep->capabilities_len, NULL, config);
|
||||
if (res < 0) {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: incompatible capabilities (%d), try next",
|
||||
sw, res);
|
||||
goto next;
|
||||
}
|
||||
config_size = res;
|
||||
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: configuration %d", sw, config_size);
|
||||
for (i = 0; i < config_size; i++)
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: %d: %02x", sw, i, config[i]);
|
||||
|
||||
/* org.bluez.MediaEndpoint1.SetConfiguration on remote endpoint */
|
||||
m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration");
|
||||
if (m == NULL) {
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: dbus allocation failure, try next", sw);
|
||||
goto next;
|
||||
}
|
||||
|
||||
spa_log_info(sw->device->monitor->log, NAME": a2dp codec switch %p: trying codec %s for endpoint %s, local endpoint %s",
|
||||
sw, codec->name, ep->path, local_endpoint);
|
||||
|
||||
dbus_message_iter_init_append(m, &iter);
|
||||
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &local_endpoint);
|
||||
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &d);
|
||||
str = "Capabilities";
|
||||
append_basic_array_variant_dict_entry(&d, DBUS_TYPE_STRING, &str, "ay", "y", DBUS_TYPE_BYTE, config, config_size);
|
||||
dbus_message_iter_close_container(&iter, &d);
|
||||
|
||||
spa_assert(sw->pending == NULL);
|
||||
dbus_ret = dbus_connection_send_with_reply(sw->device->monitor->conn, m, &sw->pending, -1);
|
||||
|
||||
if (!dbus_ret || sw->pending == NULL) {
|
||||
spa_log_error(sw->device->monitor->log, NAME": a2dp codec switch %p: dbus call failure, try next", sw);
|
||||
dbus_message_unref(m);
|
||||
goto next;
|
||||
}
|
||||
|
||||
dbus_ret = dbus_pending_call_set_notify(sw->pending, a2dp_codec_switch_reply, sw, NULL);
|
||||
dbus_message_unref(m);
|
||||
|
||||
if (!dbus_ret) {
|
||||
spa_log_error(sw->device->monitor->log, NAME": a2dp codec switch %p: dbus set notify failure", sw);
|
||||
goto next;
|
||||
}
|
||||
|
||||
free(local_endpoint);
|
||||
return true;
|
||||
|
||||
next:
|
||||
free(local_endpoint);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void a2dp_codec_switch_process(struct spa_bt_a2dp_codec_switch *sw)
|
||||
{
|
||||
while (*sw->codec_iter != NULL && *sw->path_iter != NULL) {
|
||||
if (sw->path_iter == sw->paths && (*sw->codec_iter)->caps_preference_cmp) {
|
||||
/* Sort endpoints according to codec preference, when at a new codec. */
|
||||
a2dp_codec_switch_cmp_sw = sw;
|
||||
qsort(sw->paths, sw->num_paths, sizeof(char *), a2dp_codec_switch_cmp);
|
||||
}
|
||||
|
||||
if (a2dp_codec_switch_process_current(sw)) {
|
||||
/* Wait for dbus reply */
|
||||
return;
|
||||
}
|
||||
|
||||
a2dp_codec_switch_next(sw);
|
||||
};
|
||||
|
||||
/* Didn't find any suitable endpoint. Report failure. */
|
||||
spa_log_info(sw->device->monitor->log, NAME": a2dp codec switch %p: failed to get an endpoint", sw);
|
||||
sw->device->active_codec_switch = NULL;
|
||||
spa_bt_device_emit_codec_switched(sw->device, -ENODEV);
|
||||
a2dp_codec_switch_free(sw);
|
||||
}
|
||||
|
||||
static void a2dp_codec_switch_reply(DBusPendingCall *pending, void *user_data)
|
||||
{
|
||||
struct spa_bt_a2dp_codec_switch *sw = user_data;
|
||||
struct spa_bt_device *device = sw->device;
|
||||
DBusMessage *r;
|
||||
|
||||
r = dbus_pending_call_steal_reply(pending);
|
||||
|
||||
spa_assert(sw->pending == pending);
|
||||
dbus_pending_call_unref(pending);
|
||||
sw->pending = NULL;
|
||||
|
||||
if (sw->device->active_codec_switch != sw) {
|
||||
/* This codec switch has been canceled. Switch to the new one. */
|
||||
spa_log_debug(sw->device->monitor->log, NAME": a2dp codec switch %p: canceled, go to new switch", sw);
|
||||
a2dp_codec_switch_free(sw);
|
||||
|
||||
if (r != NULL)
|
||||
dbus_message_unref(r);
|
||||
|
||||
spa_assert(device->active_codec_switch != NULL);
|
||||
a2dp_codec_switch_process(device->active_codec_switch);
|
||||
return;
|
||||
}
|
||||
|
||||
if (r == NULL) {
|
||||
spa_log_error(sw->device->monitor->log,
|
||||
NAME": a2dp codec switch %p: empty reply from dbus, stopping",
|
||||
sw);
|
||||
a2dp_codec_switch_free(sw);
|
||||
device->active_codec_switch = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
|
||||
spa_log_debug(sw->device->monitor->log,
|
||||
NAME": a2dp codec switch %p: failed (%s), trying next",
|
||||
sw, dbus_message_get_error_name(r));
|
||||
dbus_message_unref(r);
|
||||
goto next;
|
||||
}
|
||||
|
||||
dbus_message_unref(r);
|
||||
|
||||
/* Success */
|
||||
spa_log_info(sw->device->monitor->log, NAME": a2dp codec switch %p: success", sw);
|
||||
device->active_codec_switch = NULL;
|
||||
spa_bt_device_emit_codec_switched(sw->device, 0);
|
||||
a2dp_codec_switch_free(sw);
|
||||
return;
|
||||
|
||||
next:
|
||||
a2dp_codec_switch_next(sw);
|
||||
a2dp_codec_switch_process(sw);
|
||||
return;
|
||||
}
|
||||
|
||||
static int a2dp_codec_switch_cmp(const void *a, const void *b)
|
||||
{
|
||||
struct spa_bt_a2dp_codec_switch *sw = a2dp_codec_switch_cmp_sw;
|
||||
const struct a2dp_codec *codec = *sw->codec_iter;
|
||||
const char *path1 = a, *path2 = b;
|
||||
struct spa_bt_remote_endpoint *ep1, *ep2;
|
||||
|
||||
ep1 = device_remote_endpoint_find(sw->device, path1);
|
||||
ep2 = device_remote_endpoint_find(sw->device, path2);
|
||||
|
||||
if (ep1 != NULL && (ep1->uuid == NULL || ep1->codec != codec->codec_id || ep1->capabilities == NULL))
|
||||
ep1 = NULL;
|
||||
if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id || ep2->capabilities == NULL))
|
||||
ep2 = NULL;
|
||||
|
||||
if (ep1 == NULL && ep2 == NULL)
|
||||
return 0;
|
||||
else if (ep1 == NULL)
|
||||
return 1;
|
||||
else if (ep2 == NULL)
|
||||
return -1;
|
||||
|
||||
return codec->caps_preference_cmp(codec, ep1->capabilities, ep1->capabilities_len,
|
||||
ep2->capabilities, ep2->capabilities_len);
|
||||
}
|
||||
|
||||
/* Ensure there's a transport for at least one of the listed codecs */
|
||||
int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec **codecs)
|
||||
{
|
||||
struct spa_bt_a2dp_codec_switch *sw;
|
||||
struct spa_bt_remote_endpoint *ep;
|
||||
struct spa_bt_transport *t;
|
||||
size_t i, num_codecs, num_eps;
|
||||
|
||||
if (!device->adapter->application_registered) {
|
||||
/* Codec switching not supported */
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
if (codecs[0] == NULL)
|
||||
return -ENODEV;
|
||||
|
||||
/* Check if we already have an enabled transport for the most preferred codec.
|
||||
* However, if there already was a codec switch running, these transports may
|
||||
* disapper soon. In that case, we have to do the full thing.
|
||||
*/
|
||||
if (device->active_codec_switch == NULL) {
|
||||
spa_list_for_each(t, &device->transport_list, device_link) {
|
||||
if (t->a2dp_codec != codecs[0] || !t->enabled)
|
||||
continue;
|
||||
|
||||
if ((device->connected_profiles & t->profile) != t->profile)
|
||||
continue;
|
||||
|
||||
spa_bt_device_emit_codec_switched(device, 0);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Setup and start iteration */
|
||||
|
||||
sw = calloc(1, sizeof(struct spa_bt_a2dp_codec_switch));
|
||||
if (sw == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
num_eps = 0;
|
||||
spa_list_for_each(ep, &device->remote_endpoint_list, device_link)
|
||||
++num_eps;
|
||||
|
||||
num_codecs = 0;
|
||||
while (codecs[num_codecs] != NULL)
|
||||
++num_codecs;
|
||||
|
||||
sw->codecs = calloc(num_codecs + 1, sizeof(const struct a2dp_codec *));
|
||||
sw->paths = calloc(num_eps + 1, sizeof(char *));
|
||||
sw->num_paths = num_eps;
|
||||
|
||||
if (sw->codecs == NULL || sw->paths == NULL) {
|
||||
a2dp_codec_switch_free(sw);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
memcpy(sw->codecs, codecs, num_codecs * sizeof(const struct a2dp_codec *));
|
||||
|
||||
i = 0;
|
||||
spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
|
||||
sw->paths[i] = strdup(ep->path);
|
||||
if (sw->paths[i] == NULL) {
|
||||
a2dp_codec_switch_free(sw);
|
||||
return -ENOMEM;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
sw->codecs[num_codecs] = NULL;
|
||||
sw->paths[num_eps] = NULL;
|
||||
|
||||
sw->codec_iter = sw->codecs;
|
||||
sw->path_iter = sw->paths;
|
||||
|
||||
sw->profile = device->connected_profiles;
|
||||
|
||||
sw->device = device;
|
||||
spa_list_append(&device->codec_switch_list, &sw->device_link);
|
||||
|
||||
if (device->active_codec_switch != NULL) {
|
||||
/*
|
||||
* There's a codec switch already running. BlueZ does not appear to allow
|
||||
* calling dbus_pending_call_cancel on an active request, so we have to
|
||||
* wait for the reply to arrive first, and only then start processing this
|
||||
* request.
|
||||
*/
|
||||
spa_log_debug(sw->device->monitor->log,
|
||||
NAME": a2dp codec switch: already in progress, canceling previous");
|
||||
spa_assert(device->active_codec_switch->pending != NULL);
|
||||
device->active_codec_switch = sw;
|
||||
} else {
|
||||
device->active_codec_switch = sw;
|
||||
a2dp_codec_switch_process(sw);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
|
||||
const char *path, DBusMessage *m, void *userdata)
|
||||
{
|
||||
|
|
@ -1402,6 +1800,8 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
|
|||
spa_log_info(monitor->log, "%p: %s validate conf channels:%d",
|
||||
monitor, path, transport->n_channels);
|
||||
|
||||
transport->enabled = true;
|
||||
|
||||
spa_bt_device_connect_profile(transport->device, transport->profile);
|
||||
|
||||
if ((r = dbus_message_new_method_return(m)) == NULL)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue