From 4e4648996465f02284fefa9218cb87b57e2c722b Mon Sep 17 00:00:00 2001 From: George Kiagiadakis Date: Tue, 30 Jun 2026 17:39:53 +0300 Subject: [PATCH] bluez5: fix shared A2DP endpoint caps including disabled companion codecs Codecs that share an A2DP endpoint (e.g. aac + aac_eld) previously used an asymmetric pattern: one codec (the "owner") had fill_caps and quietly advertised the union of all siblings' capability bits; companions had fill_caps = NULL. This caused a bug: when aac_eld is absent from bluez5.codecs, the "aac" endpoint still advertised the ELD object-type bit because a2dp_codec_aac's fill_caps emitted it unconditionally (gated only by eld_supported(), never by enabled_codecs). A remote could then select ELD; SetConfiguration resolved the transport to a2dp_codec_aac (the only fill_caps holder for "aac") Refactor to a symmetric, composable model: - Add bool endpoint_companion to struct media_codec (default false = owner). Companion codec objects set this flag instead of fill_caps = NULL. - Add combine_caps() function to struct media_codec. Owners that share an endpoint implement this to union two capability blobs of the same codec_id. - Each codec's fill_caps now describes only its own bits. AAC LC advertises only LC object types; AAC ELD advertises only the ELD type (or returns -ENOTSUP when FDK-AAC lacks ELD support). - Bump SPA_VERSION_BLUEZ5_CODEC_MEDIA 16 -> 17. On the monitor side (bluez5-dbus.c): - New media_codec_fill_endpoint_caps() helper fills the owner's caps then walks enabled companions on the same endpoint and merges each via combine_caps. All five endpoint-registration fill_caps call sites are routed through this helper. - endpoint_should_be_registered() gates on !endpoint_companion (not on fill_caps != NULL), so companions correctly skip endpoint registration. - media_endpoint_to_codec() now filters by is_media_codec_enabled and prefers owners as a tiebreaker. - New media_endpoint_to_codec_for_config() resolves SetConfiguration by calling each enabled candidate's validate_config against the negotiated config bytes. This ensures that an "aac" endpoint carrying ELD bytes is mapped to a2dp_codec_aac_eld (not a2dp_codec_aac). - AAC validate_config and codec_init are id-gated so a stale ELD config can never be decoded by the LC codec object. Assisted-by: Claude Opus 4.7 --- spa/plugins/bluez5/a2dp-codec-aac.c | 95 +++++++++--- spa/plugins/bluez5/a2dp-codec-opus.c | 6 +- spa/plugins/bluez5/a2dp-codec-sbc.c | 2 +- spa/plugins/bluez5/bluez5-dbus.c | 223 +++++++++++++++++++++------ spa/plugins/bluez5/codec-loader.c | 6 +- spa/plugins/bluez5/media-codecs.h | 27 +++- 6 files changed, 281 insertions(+), 78 deletions(-) diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c index 49bed720d..2c08f0345 100644 --- a/spa/plugins/bluez5/a2dp-codec-aac.c +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -73,13 +73,21 @@ done: static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) { + uint8_t object_type; + + if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD) { + if (!eld_supported()) + return -ENOTSUP; + object_type = AAC_OBJECT_TYPE_MPEG4_AAC_ELD; + } else { + /* NOTE: AAC Long Term Prediction and AAC Scalable are + * not supported by the FDK-AAC library. */ + object_type = AAC_OBJECT_TYPE_MPEG2_AAC_LC | + AAC_OBJECT_TYPE_MPEG4_AAC_LC; + } + const a2dp_aac_t a2dp_aac = { - .object_type = - /* NOTE: AAC Long Term Prediction and AAC Scalable are - * not supported by the FDK-AAC library. */ - AAC_OBJECT_TYPE_MPEG2_AAC_LC | - AAC_OBJECT_TYPE_MPEG4_AAC_LC | - (eld_supported() ? AAC_OBJECT_TYPE_MPEG4_AAC_ELD : 0), + .object_type = object_type, AAC_INIT_FREQUENCY( AAC_SAMPLING_FREQ_8000 | AAC_SAMPLING_FREQ_11025 | @@ -104,6 +112,32 @@ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, return sizeof(a2dp_aac); } +static int codec_combine_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE], int caps_size, + const uint8_t *other, int other_size) +{ + a2dp_aac_t *dst = (a2dp_aac_t *)caps; + const a2dp_aac_t *src = (const a2dp_aac_t *)other; + uint32_t freq; + uint32_t dst_bitrate, src_bitrate; + + if (caps_size != (int)sizeof(*dst) || other_size != (int)sizeof(*src)) + return -EINVAL; + + dst->object_type |= src->object_type; + dst->channels |= src->channels; + dst->vbr |= src->vbr; + + freq = AAC_GET_FREQUENCY(*dst) | AAC_GET_FREQUENCY(*src); + AAC_SET_FREQUENCY(*dst, freq); + + dst_bitrate = AAC_GET_BITRATE(*dst); + src_bitrate = AAC_GET_BITRATE(*src); + AAC_SET_BITRATE(*dst, SPA_MAX(dst_bitrate, src_bitrate)); + + return caps_size; +} + static const struct media_codec_config aac_frequencies[] = { { AAC_SAMPLING_FREQ_44100, 44100, 10 }, @@ -153,7 +187,7 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags, if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD) { if (!eld_supported()) return -ENOTSUP; - + if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) conf.object_type = AAC_OBJECT_TYPE_MPEG4_AAC_ELD; else @@ -218,11 +252,20 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags * * Some devices also set multiple bits in frequencies & channels. * For these, pick a "preferred" choice. + * + * Each codec object only accepts its own object types: a2dp_codec_aac + * accepts LC variants, a2dp_codec_aac_eld accepts ELD. This lets the + * monitor's config-aware resolver attribute the transport to the + * correct codec object. */ - if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | - AAC_OBJECT_TYPE_MPEG4_AAC_LC | - AAC_OBJECT_TYPE_MPEG4_AAC_ELD))) - return -EINVAL; + if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD) { + if (!(conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD)) + return -EINVAL; + } else { + if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | + AAC_OBJECT_TYPE_MPEG4_AAC_LC))) + return -EINVAL; + } j = 0; SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) { if (AAC_GET_FREQUENCY(conf) & f->config) { @@ -331,17 +374,15 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, goto error; /* If object type has multiple bits set (invalid per spec, see above), - * assume the device usually means MPEG2 AAC LC which is mandatory. + * the validate_config step is gated by codec->id so we trust the + * configuration matches this codec object. Within the LC family, + * prefer MPEG2 AAC LC which is mandatory. */ - if (conf->object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC) { - res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_MP2_AAC_LC); - if (res != AACENC_OK) + if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD) { + if (!(conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD)) { + res = -EINVAL; goto error; - } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC) { - res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC); - if (res != AACENC_OK) - goto error; - } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) { + } res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_ER_AAC_ELD); if (res != AACENC_OK) goto error; @@ -349,7 +390,15 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, res = aacEncoder_SetParam(this->aacenc, AACENC_SBR_MODE, 1); if (res != AACENC_OK) goto error; - } else { + } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC) { + res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_MP2_AAC_LC); + if (res != AACENC_OK) + goto error; + } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC) { + res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC); + if (res != AACENC_OK) + goto error; + } else { res = -EINVAL; goto error; } @@ -665,6 +714,7 @@ const struct media_codec a2dp_codec_aac = { .name = "aac", .description = "AAC", .fill_caps = codec_fill_caps, + .combine_caps = codec_combine_caps, .select_config = codec_select_config, .enum_config = codec_enum_config, .validate_config = codec_validate_config, @@ -691,7 +741,8 @@ const struct media_codec a2dp_codec_aac_eld = { .name = "aac_eld", .description = "AAC-ELD", .endpoint_name = "aac", - .fill_caps = NULL, + .endpoint_companion = true, + .fill_caps = codec_fill_caps, .select_config = codec_select_config, .enum_config = codec_enum_config, .validate_config = codec_validate_config, diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c index 4034bd502..e881c1f58 100644 --- a/spa/plugins/bluez5/a2dp-codec-opus.c +++ b/spa/plugins/bluez5/a2dp-codec-opus.c @@ -1390,7 +1390,7 @@ const struct media_codec a2dp_codec_opus_05_51 = { .name = "opus_05_51", .description = "Opus 05 5.1 Surround", .endpoint_name = "opus_05", - .fill_caps = NULL, + .endpoint_companion = true, }; const struct media_codec a2dp_codec_opus_05_71 = { @@ -1399,7 +1399,7 @@ const struct media_codec a2dp_codec_opus_05_71 = { .name = "opus_05_71", .description = "Opus 05 7.1 Surround", .endpoint_name = "opus_05", - .fill_caps = NULL, + .endpoint_companion = true, }; /* Bidi return channel codec: doesn't have endpoints */ @@ -1428,7 +1428,7 @@ const struct media_codec a2dp_codec_opus_05_pro = { .clear_props = codec_clear_props, .duplex_codec = &a2dp_codec_opus_05_return, .endpoint_name = "opus_05_duplex", - .fill_caps = NULL, + .endpoint_companion = true, }; MEDIA_CODEC_EXPORT_DEF( diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c index f1b86e76b..c89c474b0 100644 --- a/spa/plugins/bluez5/a2dp-codec-sbc.c +++ b/spa/plugins/bluez5/a2dp-codec-sbc.c @@ -615,7 +615,7 @@ const struct media_codec a2dp_codec_sbc_xq = { .name = "sbc_xq", .description = "SBC-XQ", .endpoint_name = "sbc", - .fill_caps = NULL, + .endpoint_companion = true, .select_config = codec_select_config, .enum_config = codec_enum_config, .validate_config = codec_validate_config, diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index f35a698cc..86cde4bdf 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -507,6 +507,41 @@ static int media_codec_to_endpoint(const struct media_codec *codec, return 0; } +static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec) +{ + /* Mandatory codecs are always enabled */ + switch (codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_SBC: + case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: + case SPA_BLUETOOTH_AUDIO_CODEC_LC3: + return true; + default: + return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL; + } +} + +static const char *media_endpoint_to_ep_name(const char *endpoint, bool *sink) +{ + static const struct { const char *prefix; bool sink; } prefixes[] = { + { A2DP_SINK_ENDPOINT "/", true }, + { A2DP_SOURCE_ENDPOINT "/", false }, + { BAP_SOURCE_ENDPOINT "/", false }, + { BAP_SINK_ENDPOINT "/", true }, + { BAP_BROADCAST_SOURCE_ENDPOINT "/", false }, + { BAP_BROADCAST_SINK_ENDPOINT "/", true }, + }; + size_t i; + + for (i = 0; i < SPA_N_ELEMENTS(prefixes); i++) { + if (spa_strstartswith(endpoint, prefixes[i].prefix)) { + *sink = prefixes[i].sink; + return endpoint + strlen(prefixes[i].prefix); + } + } + *sink = true; + return NULL; +} + static const struct media_codec *media_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint, bool *sink, const struct media_codec *preferred) { const char *ep_name; @@ -514,51 +549,97 @@ static const struct media_codec *media_endpoint_to_codec(struct spa_bt_monitor * const struct media_codec *found = NULL; int i; - if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) { - ep_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/"); - *sink = true; - } else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) { - ep_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/"); - *sink = false; - } else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/")) { - ep_name = endpoint + strlen(BAP_SOURCE_ENDPOINT "/"); - *sink = false; - } else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/")) { - ep_name = endpoint + strlen(BAP_SINK_ENDPOINT "/"); - *sink = true; - } else if (spa_strstartswith(endpoint, BAP_BROADCAST_SOURCE_ENDPOINT "/")) { - ep_name = endpoint + strlen(BAP_BROADCAST_SOURCE_ENDPOINT "/"); - *sink = false; - } else if (spa_strstartswith(endpoint, BAP_BROADCAST_SINK_ENDPOINT "/")) { - ep_name = endpoint + strlen(BAP_BROADCAST_SINK_ENDPOINT "/"); - *sink = true; - } else { - *sink = true; + ep_name = media_endpoint_to_ep_name(endpoint, sink); + if (ep_name == NULL) return NULL; - } for (i = 0; media_codecs[i]; i++) { const struct media_codec *codec = media_codecs[i]; const char *codec_ep_name = codec->endpoint_name ? codec->endpoint_name : codec->name; - if (!preferred && !codec->fill_caps) + if (!is_media_codec_enabled(monitor, codec)) continue; if (!spa_streq(ep_name, codec_ep_name)) continue; if ((*sink && !codec->decode) || (!*sink && !codec->encode)) continue; - /* Same endpoint may be shared with multiple codec objects, - * which may e.g. correspond to different encoder settings. - * Look up which one we selected. + /* Same endpoint may be shared with multiple codec objects. + * Prefer the requested one, then the endpoint owner, else + * fall back to the first match. */ - if ((preferred && codec == preferred) || found == NULL) + if (preferred && codec == preferred) + return codec; + if (found == NULL || (found->endpoint_companion && !codec->endpoint_companion)) found = codec; } return found; } +/* Like media_endpoint_to_codec, but when multiple codecs share the endpoint, + * pick the one whose validate_config accepts the given on-the-wire config. + * Falls back to the preferred/owner heuristic if none accepts (or if no codec + * implements validate_config). + */ +static const struct media_codec *media_endpoint_to_codec_for_config( + struct spa_bt_monitor *monitor, const char *endpoint, bool *sink, + const struct media_codec *preferred, + const void *config, size_t config_size) +{ + const char *ep_name; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const struct media_codec *fallback; + const struct media_codec *accepted_preferred = NULL; + const struct media_codec *accepted_owner = NULL; + const struct media_codec *accepted_first = NULL; + int i; + + fallback = media_endpoint_to_codec(monitor, endpoint, sink, preferred); + if (fallback == NULL || config == NULL || config_size == 0) + return fallback; + + ep_name = media_endpoint_to_ep_name(endpoint, sink); + if (ep_name == NULL) + return fallback; + + for (i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + const char *codec_ep_name = + codec->endpoint_name ? codec->endpoint_name : codec->name; + struct spa_audio_info info; + + if (!is_media_codec_enabled(monitor, codec)) + continue; + if (!spa_streq(ep_name, codec_ep_name)) + continue; + if ((*sink && !codec->decode) || (!*sink && !codec->encode)) + continue; + if (!codec->validate_config) + continue; + if (codec->validate_config(codec, *sink ? MEDIA_CODEC_FLAG_SINK : 0, + config, config_size, &info) < 0) + continue; + + if (preferred && codec == preferred) { + accepted_preferred = codec; + break; + } + if (accepted_owner == NULL && !codec->endpoint_companion) + accepted_owner = codec; + if (accepted_first == NULL) + accepted_first = codec; + } + + if (accepted_preferred) + return accepted_preferred; + if (accepted_owner) + return accepted_owner; + if (accepted_first) + return accepted_first; + return fallback; +} + static int media_endpoint_to_profile(const char *endpoint) { @@ -578,19 +659,6 @@ static int media_endpoint_to_profile(const char *endpoint) return SPA_BT_PROFILE_NULL; } -static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec) -{ - /* Mandatory codecs are always enabled */ - switch (codec->id) { - case SPA_BLUETOOTH_AUDIO_CODEC_SBC: - case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: - case SPA_BLUETOOTH_AUDIO_CODEC_LC3: - return true; - default: - return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL; - } -} - static enum spa_bt_profile get_codec_profile(const struct media_codec *codec, enum spa_bt_media_direction direction) { @@ -686,10 +754,11 @@ static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor, const struct media_codec *codec, enum spa_bt_media_direction direction) { - /* Codecs with fill_caps == NULL share endpoint with another codec, - * and don't have their own endpoint + /* Companion codecs share another codec's endpoint and don't get + * registered themselves; the owner's registration covers them. */ return codec_has_direction(monitor, codec, direction) && + !codec->endpoint_companion && codec->fill_caps; } @@ -767,6 +836,60 @@ const struct spa_dict *get_device_codec_settings(struct spa_bt_device *device, b return bap ? device->settings : &device->monitor->global_settings; } +/* Build the endpoint's advertised caps by filling the owner's own caps and + * merging in those of every enabled companion sharing the same endpoint. + * \a owner must satisfy endpoint_should_be_registered() for \a direction. + */ +static int media_codec_fill_endpoint_caps(struct spa_bt_monitor *monitor, + const struct media_codec *owner, + enum spa_bt_media_direction direction, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const char *owner_ep = owner->endpoint_name ? owner->endpoint_name : owner->name; + uint32_t flags = (direction == SPA_BT_MEDIA_SINK || + direction == SPA_BT_MEDIA_SINK_BROADCAST) ? MEDIA_CODEC_FLAG_SINK : 0; + int caps_size; + int i; + + caps_size = owner->fill_caps(owner, flags, &monitor->global_settings, caps); + if (caps_size < 0) + return caps_size; + + if (!owner->combine_caps) + return caps_size; + + for (i = 0; media_codecs[i]; i++) { + const struct media_codec *c = media_codecs[i]; + const char *c_ep = c->endpoint_name ? c->endpoint_name : c->name; + uint8_t other[A2DP_MAX_CAPS_SIZE]; + int other_size; + + if (c == owner || !c->endpoint_companion) + continue; + if (!spa_streq(c_ep, owner_ep)) + continue; + if (c->codec_id != owner->codec_id || c->kind != owner->kind) + continue; + if (!codec_has_direction(monitor, c, direction)) + continue; + if (!is_media_codec_enabled(monitor, c)) + continue; + if (!c->fill_caps) + continue; + + other_size = c->fill_caps(c, flags, &monitor->global_settings, other); + if (other_size < 0) + continue; + + caps_size = owner->combine_caps(owner, flags, caps, caps_size, other, other_size); + if (caps_size < 0) + return caps_size; + } + + return caps_size; +} + static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { struct spa_bt_monitor *monitor = userdata; @@ -5351,9 +5474,13 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn, return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } - /* If multiple codecs share the endpoint, pick the one we wanted */ - transport->media_codec = codec = media_endpoint_to_codec(monitor, endpoint, &sink, - transport->device->preferred_codec); + /* If multiple codecs share the endpoint, pick the one whose + * validate_config accepts the configuration BlueZ sent us. This is + * how we tell e.g. AAC LC from AAC ELD on the shared "aac" endpoint. + */ + transport->media_codec = codec = media_endpoint_to_codec_for_config(monitor, endpoint, &sink, + transport->device->preferred_codec, + transport->configuration, transport->configuration_len); spa_assert(codec != NULL); spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : ""); @@ -5700,7 +5827,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * continue; if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) { - caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, &monitor->global_settings, caps); + caps_size = media_codec_fill_endpoint_caps(monitor, codec, SPA_BT_MEDIA_SINK, caps); if (caps_size < 0) continue; @@ -5715,7 +5842,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * } if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) { - caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps); + caps_size = media_codec_fill_endpoint_caps(monitor, codec, SPA_BT_MEDIA_SOURCE, caps); if (caps_size < 0) continue; @@ -5731,7 +5858,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * if (is_bap && register_bcast) { if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST)) { - caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps); + caps_size = media_codec_fill_endpoint_caps(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST, caps); if (caps_size < 0) continue; @@ -5746,7 +5873,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * } if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST)) { - caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, &monitor->global_settings, caps); + caps_size = media_codec_fill_endpoint_caps(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST, caps); if (caps_size < 0) continue; diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c index 68e6af756..88216ab14 100644 --- a/spa/plugins/bluez5/codec-loader.c +++ b/spa/plugins/bluez5/codec-loader.c @@ -136,11 +136,13 @@ static int load_media_codecs_from(struct impl *impl, const char *factory_name, c continue; } - /* Don't load duplicate endpoints */ + /* Don't load duplicate endpoints. Two owners on the same + * endpoint is a conflict; companions sharing an owner are not. + */ for (j = 0; j < impl->n_codecs; ++j) { const struct media_codec *c2 = impl->codecs[j]; const char *ep2 = c2->endpoint_name ? c2->endpoint_name : c2->name; - if (spa_streq(ep, ep2) && c->fill_caps && c2->fill_caps) { + if (spa_streq(ep, ep2) && !c->endpoint_companion && !c2->endpoint_companion) { spa_log_debug(impl->log, "media codec %s from %s duplicate endpoint %s", c->name, factory_name, ep); goto next_codec; diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h index 76bd00571..919d91725 100644 --- a/spa/plugins/bluez5/media-codecs.h +++ b/spa/plugins/bluez5/media-codecs.h @@ -27,7 +27,7 @@ #define SPA_TYPE_INTERFACE_Bluez5CodecMedia SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:Media:Private" -#define SPA_VERSION_BLUEZ5_CODEC_MEDIA 16 +#define SPA_VERSION_BLUEZ5_CODEC_MEDIA 17 struct spa_bluez5_codec_a2dp { struct spa_interface iface; @@ -93,14 +93,37 @@ struct media_codec { * After successful decode, start_decode() should be * called again to parse the remaining data. */ + /** True for codecs that share an endpoint with another codec and let + * that other codec own the endpoint registration. The owner's caps + * are advertised, with enabled companions merged in via combine_caps. + * The default (false) means this codec owns its endpoint; set this + * to true only on companion codec objects (e.g. aac_eld, sbc_xq). + */ + bool endpoint_companion; + int (*get_bis_config)(const struct media_codec *codec, uint8_t *caps, uint8_t *caps_size, struct spa_dict *settings, struct bap_codec_qos *qos); - /** If fill_caps is NULL, no endpoint is registered (for sharing with another codec). */ + /** Fill in this codec's own capability bits. + * For codecs that share an endpoint, only the bits belonging to this + * codec object are advertised; the monitor merges siblings using + * combine_caps on the endpoint owner. May be NULL on a companion + * codec whose advertised caps are identical to the owner's. + */ int (*fill_caps) (const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]); + /** Merge another sibling codec's caps blob (same codec_id) into caps. + * Only required on the endpoint owner when an enabled companion's + * caps differ from the owner's. If NULL, only the owner's caps are + * advertised and companions' fill_caps is not called. + * Returns the new caps_size, or < 0 on error. + */ + int (*combine_caps) (const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE], int caps_size, + const uint8_t *other, int other_size); + int (*select_config) (const struct media_codec *codec, uint32_t flags, const void *caps, size_t caps_size, const struct media_codec_audio_info *info,