1
Fork 0

bluez5: support multiple BAP transports per device

In multi-ASE configurations there can be multiple transports per device,
each corresponding to different channels.

Emit sink/source nodes for each BAP transport present.

Combine them into a single sink/source in the same way as we do for
device sets.
This commit is contained in:
Pauli Virtanen 2024-01-07 16:32:03 +02:00 committed by Wim Taymans
parent d487dc0fc6
commit a154169942
1 changed files with 152 additions and 90 deletions

View File

@ -38,12 +38,14 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.device");
#undef SPA_LOG_TOPIC_DEFAULT
#define SPA_LOG_TOPIC_DEFAULT &log_topic
#define MAX_DEVICES 64
#define MAX_NODES (2*SPA_AUDIO_MAX_CHANNELS)
#define DEVICE_ID_SOURCE 0
#define DEVICE_ID_SINK 1
#define DEVICE_ID_SOURCE_SET 2
#define DEVICE_ID_SINK_SET 3
#define DEVICE_ID_SOURCE_SET (MAX_NODES + 0)
#define DEVICE_ID_SINK_SET (MAX_NODES + 1)
#define SINK_ID_FLAG 0x1
#define DYNAMIC_NODE_ID_FLAG 0x1000
static struct spa_i18n *_i18n;
@ -104,6 +106,7 @@ struct device_set_member {
struct impl *impl;
struct spa_bt_transport *transport;
struct spa_hook listener;
uint32_t id;
};
struct device_set {
@ -149,16 +152,13 @@ struct impl {
const struct media_codec **supported_codecs;
size_t supported_codec_count;
struct dynamic_node dyn_media_source;
struct dynamic_node dyn_media_sink;
struct dynamic_node dyn_sco_source;
struct dynamic_node dyn_sco_sink;
struct dynamic_node dyn_nodes[MAX_NODES + 2];
#define MAX_SETTINGS 32
struct spa_dict_item setting_items[MAX_SETTINGS];
struct spa_dict setting_dict;
struct node nodes[4];
struct node nodes[MAX_NODES + 2];
};
static void init_node(struct impl *this, struct node *node, uint32_t id)
@ -356,7 +356,7 @@ static float get_soft_volume_boost(struct node *node)
*/
if (node->a2dp_duplex && node->transport && codec && codec->info &&
spa_atob(spa_dict_lookup(codec->info, "duplex.boost")) &&
node->id == DEVICE_ID_SOURCE &&
!(node->id & SINK_ID_FLAG) &&
!node->transport->volumes[SPA_BT_VOLUME_ID_RX].active)
return 10.0f; /* 20 dB boost */
@ -581,13 +581,13 @@ static void emit_device_set_node(struct impl *this, uint32_t id)
spa_strbuf_append(&json, "[");
for (i = 0; i < n_members; ++i) {
struct spa_bt_transport *t = members[i].transport;
uint32_t member_id = members[i].id;
char object_path[512];
unsigned int j;
int member_id = (id == DEVICE_ID_SINK_SET) ? DEVICE_ID_SINK : DEVICE_ID_SOURCE;
if (i > 0)
spa_strbuf_append(&json, ",");
spa_scnprintf(object_path, sizeof(object_path), "%s/%s-%d",
spa_scnprintf(object_path, sizeof(object_path), "%s/%s-%"PRIu32,
this->device_set.path, t->device->address, member_id);
spa_strbuf_append(&json, "{\"object.path\":\"%s\",\"channels\":[", object_path);
for (j = 0; j < t->n_channels; ++j) {
@ -633,9 +633,15 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
uint32_t n_items = 0;
char transport[32], str_id[32], object_path[512];
bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG);
bool in_device_set = false;
spa_log_debug(this->log, "%p: node, transport:%p id:%08x factory:%s", this, t, id, factory_name);
if (id & SINK_ID_FLAG)
in_device_set = this->device_set.path && (this->device_set.sinks > 1);
else
in_device_set = this->device_set.path && (this->device_set.sources > 1);
snprintf(transport, sizeof(transport), "pointer:%p", t);
items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport);
items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PROFILE, spa_bt_profile_name(t->profile));
@ -643,7 +649,7 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
items[4] = SPA_DICT_ITEM_INIT("device.routes", "1");
n_items = 5;
if (!is_dyn_node && !this->device_set.path) {
if (!is_dyn_node && !in_device_set) {
snprintf(str_id, sizeof(str_id), "%d", id);
items[n_items] = SPA_DICT_ITEM_INIT("card.profile.device", str_id);
n_items++;
@ -656,7 +662,7 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.a2dp-duplex", "true");
n_items++;
}
if (this->device_set.path) {
if (in_device_set) {
items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.set", this->device_set.path);
n_items++;
items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.internal", "true");
@ -676,6 +682,8 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t,
info.props = &SPA_DICT_INIT(items, n_items);
SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
spa_assert(id < SPA_N_ELEMENTS(this->nodes));
spa_device_emit_object_info(&this->hooks, id, &info);
if (this->device_set.path) {
@ -747,19 +755,24 @@ static void init_dummy_input_node(struct impl *this, uint32_t id)
}
}
static bool transport_enabled(struct spa_bt_transport *t, int profile, enum spa_bluetooth_audio_codec codec)
{
bool codec_ok = codec == 0 ||
(t->media_codec != NULL && t->media_codec->id == codec) ||
get_hfp_codec_id(t->codec) == codec;
return (t->profile & t->device->connected_profiles) &&
(t->profile & profile) == t->profile &&
codec_ok;
}
static struct spa_bt_transport *find_device_transport(struct spa_bt_device *device, int profile,
enum spa_bluetooth_audio_codec codec)
{
struct spa_bt_transport *t;
spa_list_for_each(t, &device->transport_list, device_link) {
bool codec_ok = codec == 0 ||
(t->media_codec != NULL && t->media_codec->id == codec) ||
get_hfp_codec_id(t->codec) == codec;
if ((t->profile & device->connected_profiles) &&
(t->profile & profile) == t->profile &&
codec_ok)
if (transport_enabled(t, profile, codec))
return t;
}
@ -857,9 +870,13 @@ static const struct spa_bt_transport_events dynamic_node_transport_events = {
.volume_changed = dynamic_node_volume_changed,
};
static void emit_dynamic_node(struct dynamic_node *this, struct impl *impl,
static void emit_dynamic_node(struct impl *impl,
struct spa_bt_transport *t, uint32_t id, const char *factory_name, bool a2dp_duplex)
{
struct dynamic_node *this = &impl->dyn_nodes[id];
spa_assert(id < SPA_N_ELEMENTS(impl->dyn_nodes));
spa_log_debug(impl->log, "%p: dynamic node, transport: %p->%p id: %08x->%08x",
this, this->transport, t, this->id, id);
@ -903,16 +920,22 @@ static void device_set_clear(struct impl *impl)
struct device_set *set = &impl->device_set;
unsigned int i;
for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i) {
for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i)
if (set->sink[i].transport)
spa_hook_remove(&set->sink[i].listener);
for (i = 0; i < SPA_N_ELEMENTS(set->source); ++i)
if (set->source[i].transport)
spa_hook_remove(&set->source[i].listener);
}
free(set->path);
spa_zero(*set);
set->impl = impl;
for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i)
set->sink[i].impl = impl;
for (i = 0; i < SPA_N_ELEMENTS(set->source); ++i)
set->source[i].impl = impl;
}
static void device_set_transport_destroy(void *data)
@ -933,36 +956,71 @@ static void device_set_update(struct impl *this)
struct spa_bt_device *device = this->bt_dev;
struct device_set *dset = &this->device_set;
struct spa_bt_set_membership *set;
struct spa_bt_set_membership tmp_set = {
.device = device,
.rank = 0,
.leader = true,
.path = device->path,
.others = SPA_LIST_INIT(&tmp_set.others),
};
struct spa_list tmp_set_list = SPA_LIST_INIT(&tmp_set_list);
struct spa_list *membership_list = &device->set_membership_list;
device_set_clear(this);
/*
* If no device set, use a dummy one, so that we can handle also those devices
* here (they may have multiple transports regardless).
*/
if (spa_list_is_empty(membership_list)) {
spa_list_append(&tmp_set_list, &tmp_set.link);
membership_list = &tmp_set_list;
}
if (!is_bap_client(this))
return;
spa_list_for_each(set, &device->set_membership_list, link) {
spa_list_for_each(set, membership_list, link) {
struct spa_bt_set_membership *s;
int num_devices = 0;
device_set_clear(this);
spa_bt_for_each_set_member(s, set) {
struct spa_bt_transport *t;
bool active = false;
uint32_t source_id = DEVICE_ID_SOURCE;
uint32_t sink_id = DEVICE_ID_SINK;
if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_DUPLEX))
continue;
t = find_device_transport(s->device, SPA_BT_PROFILE_BAP_SOURCE, 0);
if (t && t->bap_initiator) {
spa_list_for_each(t, &s->device->transport_list, device_link) {
if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE))
continue;
if (!transport_enabled(t, SPA_BT_PROFILE_BAP_SOURCE, 0))
continue;
if (dset->sources >= SPA_N_ELEMENTS(dset->source))
break;
active = true;
dset->source[dset->sources].impl = this;
dset->source[dset->sources].transport = t;
dset->source[dset->sources].id = source_id;
source_id += 2;
spa_bt_transport_add_listener(t, &dset->source[dset->sources].listener,
&device_set_transport_events, &dset->source[dset->sources]);
++dset->sources;
}
t = find_device_transport(s->device, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
if (t && t->bap_initiator) {
spa_list_for_each(t, &s->device->transport_list, device_link) {
if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_SINK))
continue;
if (!transport_enabled(t, SPA_BT_PROFILE_BAP_SINK, this->props.codec))
continue;
if (dset->sinks >= SPA_N_ELEMENTS(dset->sink))
break;
active = true;
dset->sink[dset->sinks].impl = this;
dset->sink[dset->sinks].transport = t;
dset->sink[dset->sinks].id = sink_id;
sink_id += 2;
spa_bt_transport_add_listener(t, &dset->sink[dset->sinks].listener,
&device_set_transport_events, &dset->sink[dset->sinks]);
++dset->sinks;
@ -972,17 +1030,22 @@ static void device_set_update(struct impl *this)
++num_devices;
}
if (num_devices <= 1 || (dset->sinks <= 1 && dset->sources <= 1)) {
device_set_clear(this);
continue;
}
spa_log_debug(this->log, "%p: %s belongs to set %s leader:%d", this,
device->path, set->path, set->leader);
dset->path = strdup(set->path);
dset->leader = set->leader;
break;
if (is_bap_client(this)) {
dset->path = strdup(set->path);
dset->leader = set->leader;
} else {
/* XXX: device set nodes for BAP server not supported,
* XXX: it'll appear as multiple streams
*/
dset->path = NULL;
dset->leader = false;
}
if (num_devices > 1)
break;
}
}
@ -1005,23 +1068,18 @@ static int emit_nodes(struct impl *this)
this->props.codec = 0;
else
this->props.codec = get_hfp_codec_id(t->codec);
emit_dynamic_node(&this->dyn_sco_source, this, t,
0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
emit_dynamic_node(&this->dyn_sco_sink, this, t,
1, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
emit_dynamic_node(this, t, 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
emit_dynamic_node(this, t, 1, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
}
}
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_A2DP_SOURCE)) {
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
if (t) {
this->props.codec = t->media_codec->id;
emit_dynamic_node(&this->dyn_media_source, this, t,
2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
emit_dynamic_node(this, t, 2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
if (t->media_codec->duplex_codec) {
emit_dynamic_node(&this->dyn_media_sink, this, t,
3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
}
if (t->media_codec->duplex_codec)
emit_dynamic_node(this, t, 3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
}
}
break;
@ -1030,13 +1088,10 @@ static int emit_nodes(struct impl *this)
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
if (t) {
this->props.codec = t->media_codec->id;
emit_dynamic_node(&this->dyn_media_source, this, t,
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
emit_dynamic_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
if (t->media_codec->duplex_codec) {
emit_node(this, t,
DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
}
if (t->media_codec->duplex_codec)
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
}
}
@ -1060,37 +1115,48 @@ static int emit_nodes(struct impl *this)
!this->nodes[DEVICE_ID_SOURCE].active)
init_dummy_input_node(this, DEVICE_ID_SOURCE);
break;
case DEVICE_PROFILE_BAP:
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SOURCE)) {
t = find_transport(this, SPA_BT_PROFILE_BAP_SOURCE, 0);
if (t) {
this->props.codec = t->media_codec->id;
if (t->bap_initiator)
emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
else
emit_dynamic_node(&this->dyn_media_source, this, t,
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
}
case DEVICE_PROFILE_BAP: {
struct device_set *set = &this->device_set;
unsigned int i;
if (this->device_set.leader && this->device_set.sources > 0)
emit_device_set_node(this, DEVICE_ID_SOURCE_SET);
for (i = 0; i < set->sources; ++i) {
struct spa_bt_transport *t = set->source[i].transport;
uint32_t id = set->source[i].id;
if (id >= MAX_NODES)
continue;
if (t->device != this->bt_dev)
continue;
this->props.codec = t->media_codec->id;
if (t->bap_initiator)
emit_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
else
emit_dynamic_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
}
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SINK)) {
t = find_transport(this, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
if (t) {
this->props.codec = t->media_codec->id;
if (t->bap_initiator)
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
else
emit_dynamic_node(&this->dyn_media_sink, this, t,
DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
}
if (set->path && set->leader && set->sources > 1)
emit_device_set_node(this, DEVICE_ID_SOURCE_SET);
if (this->device_set.leader && this->device_set.sinks > 0)
emit_device_set_node(this, DEVICE_ID_SINK_SET);
for (i = 0; i < set->sinks; ++i) {
struct spa_bt_transport *t = set->sink[i].transport;
uint32_t id = set->sink[i].id;
if (id >= MAX_NODES)
continue;
if (t->device != this->bt_dev)
continue;
this->props.codec = t->media_codec->id;
if (t->bap_initiator)
emit_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
else
emit_dynamic_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
}
if (set->path && set->leader && set->sinks > 1)
emit_device_set_node(this, DEVICE_ID_SINK_SET);
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SINK)) {
t = find_transport(this, SPA_BT_PROFILE_BAP_BROADCAST_SINK, this->props.codec);
if (t) {
@ -1106,11 +1172,11 @@ static int emit_nodes(struct impl *this)
t = find_transport(this, SPA_BT_PROFILE_BAP_BROADCAST_SOURCE, this->props.codec);
if (t) {
this->props.codec = t->media_codec->id;
emit_dynamic_node(&this->dyn_media_source, this, t,
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
emit_dynamic_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
}
}
break;
};
case DEVICE_PROFILE_HSP_HFP:
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) {
t = find_transport(this, SPA_BT_PROFILE_HFP_HF, this->props.codec);
@ -1158,10 +1224,8 @@ static void emit_remove_nodes(struct impl *this)
{
spa_log_debug(this->log, "%p: remove nodes", this);
remove_dynamic_node (&this->dyn_media_source);
remove_dynamic_node (&this->dyn_media_sink);
remove_dynamic_node (&this->dyn_sco_source);
remove_dynamic_node (&this->dyn_sco_sink);
for (uint32_t i = 0; i < SPA_N_ELEMENTS(this->dyn_nodes); i++)
remove_dynamic_node (&this->dyn_nodes[i]);
for (uint32_t i = 0; i < SPA_N_ELEMENTS(this->nodes); i++) {
struct node * node = &this->nodes[i];
@ -1369,8 +1433,6 @@ static void device_set_changed(void *userdata)
if (this->profile != DEVICE_PROFILE_BAP)
return;
if (!is_bap_client(this))
return;
spa_log_debug(this->log, "%p: device set changed", this);
@ -2142,7 +2204,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
node->n_channels, node->channels);
if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) &&
dev == DEVICE_ID_SINK) {
(dev & SINK_ID_FLAG)) {
spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
spa_pod_builder_long(b, node->latency_offset);
}