bluez5: basic codec-switching framework

This commit is contained in:
Pauli Virtanen 2021-01-25 19:57:45 +02:00 committed by Wim Taymans
parent 0908588d0c
commit e860f2bb4e
7 changed files with 537 additions and 14 deletions

View file

@ -346,6 +346,14 @@ struct a2dp_codec {
const void *caps, size_t caps_size,
struct spa_audio_info *info);
/** qsort comparison sorting caps in order of preference for the codec.
* Used in codec switching to select best remote endpoints.
* The caps handed in correspond to this codec_id, but are
* otherwise not checked beforehand.
*/
int (*caps_preference_cmp) (const struct a2dp_codec *codec, const void *caps1, size_t caps1_size,
const void *caps2, size_t caps2_size);
void *(*init) (const struct a2dp_codec *codec, uint32_t flags, void *config, size_t config_size,
const struct spa_audio_info *info, const struct spa_dict *settings, size_t mtu);
void (*deinit) (void *data);

View file

@ -1118,6 +1118,7 @@ static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct spa_bt_backend
else
t->codec = HFP_AUDIO_CODEC_CVSD;
t->enabled = true;
spa_bt_device_connect_profile(t->device, t->profile);
spa_log_debug(backend->log, NAME": Transport %s available for hsphfpd", endpoint->path);

View file

@ -130,6 +130,7 @@ static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm)
spa_list_append(&t->device->transport_list, &t->device_link);
t->profile = rfcomm->profile;
t->backend = backend;
t->enabled = true;
spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm);

View file

@ -126,6 +126,7 @@ static struct spa_bt_transport *_transport_create(struct spa_bt_backend *backend
t->backend = backend;
t->profile = profile;
t->codec = codec;
t->enabled = true;
finish:
return t;

View file

@ -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)

View file

@ -92,8 +92,11 @@ struct impl {
struct props props;
struct spa_bt_device *bt_dev;
struct spa_hook bt_dev_listener;
uint32_t profile;
const struct a2dp_codec *selected_a2dp_codec; /**< Codec wanted. NULL means any. */
struct node nodes[2];
};
@ -138,16 +141,33 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
t->n_channels * sizeof(uint32_t));
}
static struct spa_bt_transport *find_transport(struct impl *this, int profile)
static struct spa_bt_transport *find_transport(struct impl *this, int profile, const struct a2dp_codec *a2dp_codec)
{
struct spa_bt_device *device = this->bt_dev;
struct spa_bt_transport *t;
const struct a2dp_codec **codecs;
size_t i, num_codecs;
spa_list_for_each(t, &device->transport_list, device_link) {
if (t->profile & device->connected_profiles &&
(t->profile & profile) == t->profile)
return t;
codecs = &a2dp_codec;
num_codecs = 1;
if (a2dp_codec == NULL && (profile == SPA_BT_PROFILE_A2DP_SOURCE || profile == SPA_BT_PROFILE_A2DP_SINK)) {
codecs = a2dp_codecs;
num_codecs = 0;
while (codecs[num_codecs] != NULL)
++num_codecs;
}
for (i = 0; i < num_codecs; ++i) {
spa_list_for_each(t, &device->transport_list, device_link) {
if (t->enabled &&
(t->profile & device->connected_profiles) &&
(t->profile & profile) == t->profile &&
(codecs[i] == NULL || t->a2dp_codec == codecs[i]))
return t;
}
}
return NULL;
}
@ -160,13 +180,13 @@ static int emit_nodes(struct impl *this)
break;
case 1:
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE);
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_codec);
if (t)
emit_node(this, t, 0, SPA_NAME_API_BLUEZ5_A2DP_SOURCE);
}
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK);
t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->selected_a2dp_codec);
if (t)
emit_node(this, t, 1, SPA_NAME_API_BLUEZ5_A2DP_SINK);
}
@ -177,7 +197,7 @@ static int emit_nodes(struct impl *this)
int i;
for (i = SPA_BT_PROFILE_HSP_HS ; i <= SPA_BT_PROFILE_HFP_AG ; i <<= 1) {
t = find_transport(this, i);
t = find_transport(this, i, NULL);
if (t)
break;
}
@ -211,21 +231,50 @@ static void emit_info(struct impl *this, bool full)
}
}
static int set_profile(struct impl *this, uint32_t profile)
static void emit_remove_nodes(struct impl *this)
{
uint32_t i;
if (this->profile == profile)
return 0;
for (i = 0; i < 2; i++) {
if (this->nodes[i].active) {
spa_device_emit_object_info(&this->hooks, i, NULL);
this->nodes[i].active = false;
}
}
this->profile = profile;
}
static int set_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *a2dp_codec)
{
if (this->profile == profile && a2dp_codec == this->selected_a2dp_codec)
return 0;
emit_remove_nodes(this);
this->profile = profile;
this->selected_a2dp_codec = a2dp_codec;
if (profile == 1) {
int ret;
const struct a2dp_codec *codec_list[2], **codecs;
/* A2DP: ensure there's a transport with the selected codec (NULL means any) */
if (a2dp_codec == NULL) {
codecs = a2dp_codecs;
} else {
codec_list[0] = a2dp_codec;
codec_list[1] = NULL;
codecs = codec_list;
}
ret = spa_bt_device_ensure_a2dp_codec(this->bt_dev, codecs);
if (ret < 0)
spa_log_error(this->log, NAME": failed to switch codec (%d), setting basic profile", ret);
else
return 0;
}
this->switching_codec = false;
this->selected_a2dp_codec = NULL;
emit_nodes(this);
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
@ -237,6 +286,37 @@ static int set_profile(struct impl *this, uint32_t profile)
return 0;
}
static void codec_switched(void *userdata, int status)
{
struct impl *this = userdata;
spa_log_debug(this->log, NAME": codec switched (status %d)", status);
if (status < 0) {
/* Failed to switch: return to a fallback profile */
spa_log_error(this->log, NAME": failed to switch codec (%d), setting fallback profile", status);
if (this->selected_a2dp_codec != NULL) {
this->selected_a2dp_codec = NULL;
} else {
this->profile = 0;
}
}
emit_remove_nodes(this);
emit_nodes(this);
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
emit_info(this, false);
}
static const struct spa_bt_device_events bt_dev_events = {
SPA_VERSION_BT_DEVICE_EVENTS,
.codec_switched = codec_switched,
};
static int impl_add_listener(void *object,
struct spa_hook *listener,
const struct spa_device_events *events,
@ -762,7 +842,7 @@ static int impl_set_param(void *object,
spa_debug_pod(0, NULL, param);
return res;
}
set_profile(this, id);
set_profile(this, id, NULL);
break;
}
case SPA_PARAM_Route:
@ -825,6 +905,9 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void
static int impl_clear(struct spa_handle *handle)
{
struct impl *this = (struct impl *) handle;
if (this->bt_dev)
spa_hook_remove(&this->bt_dev_listener);
return 0;
}
@ -885,6 +968,8 @@ impl_init(const struct spa_handle_factory *factory,
this->info.params = this->params;
this->info.n_params = 4;
spa_bt_device_add_listener(this->bt_dev, &this->bt_dev_listener, &bt_dev_events, this);
return 0;
}

View file

@ -326,6 +326,17 @@ static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t blu
return SPA_BT_FORM_FACTOR_UNKNOWN;
}
struct spa_bt_a2dp_codec_switch;
struct spa_bt_transport;
struct spa_bt_device_events {
#define SPA_VERSION_BT_DEVICE_EVENTS 0
uint32_t version;
/** Codec switching completed */
void (*codec_switched) (void *data, int status);
};
struct spa_bt_device {
struct spa_list link;
struct spa_bt_monitor *monitor;
@ -349,13 +360,27 @@ struct spa_bt_device {
struct spa_source timer;
struct spa_list remote_endpoint_list;
struct spa_list transport_list;
struct spa_list codec_switch_list;
struct spa_bt_a2dp_codec_switch *active_codec_switch;
struct spa_hook_list listener_list;
bool added;
};
struct a2dp_codec;
struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path);
struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address);
int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile);
int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force);
int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec **codecs);
#define spa_bt_device_emit(d,m,v,...) spa_hook_list_call(&(d)->listener_list, \
struct spa_bt_device_events, \
m, v, ##__VA_ARGS__)
#define spa_bt_device_emit_codec_switched(d,...) spa_bt_device_emit(d, codec_switched, 0, __VA_ARGS__)
#define spa_bt_device_add_listener(d,listener,events,data) \
spa_hook_list_append(&(d)->listener_list, listener, events, data)
struct spa_bt_sco_io;
@ -403,6 +428,8 @@ struct spa_bt_transport {
void *configuration;
int configuration_len;
unsigned int enabled:1; /**< Transport ready for use in sink/source */
uint32_t n_channels;
uint32_t channels[64];