diff --git a/spa/plugins/bluez5/a2dp-codecs.h b/spa/plugins/bluez5/a2dp-codecs.h index 79b5a7fce..af6776b12 100644 --- a/spa/plugins/bluez5/a2dp-codecs.h +++ b/spa/plugins/bluez5/a2dp-codecs.h @@ -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); diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c index be2e8737b..64bab4ae8 100644 --- a/spa/plugins/bluez5/backend-hsphfpd.c +++ b/spa/plugins/bluez5/backend-hsphfpd.c @@ -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); diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index b811facb5..3f5713812 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -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); diff --git a/spa/plugins/bluez5/backend-ofono.c b/spa/plugins/bluez5/backend-ofono.c index 031b2ac0c..2120195c4 100644 --- a/spa/plugins/bluez5/backend-ofono.c +++ b/spa/plugins/bluez5/backend-ofono.c @@ -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; diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index ad8c2999d..df022a449 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -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) diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c index 0c2c47078..a4aea15b1 100644 --- a/spa/plugins/bluez5/bluez5-device.c +++ b/spa/plugins/bluez5/bluez5-device.c @@ -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; } diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 5078c6a57..cd144b57b 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -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];