diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 1b980147c..342eba0fe 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -47,6 +47,8 @@ #define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles" +#define HFP_CODEC_SWITCH_TIMEOUT_MSEC 5000 + struct impl { struct spa_bt_backend this; @@ -54,6 +56,7 @@ struct impl { struct spa_log *log; struct spa_loop *main_loop; + struct spa_system *main_system; struct spa_dbus *dbus; DBusConnection *conn; @@ -88,12 +91,14 @@ struct rfcomm { struct spa_bt_transport *transport; struct spa_hook transport_listener; enum spa_bt_profile profile; + struct spa_source timer; char* path; #ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE unsigned int slc_configured:1; unsigned int codec_negotiation_supported:1; unsigned int msbc_support_enabled_in_config:1; unsigned int msbc_supported_by_hfp:1; + unsigned int hfp_ag_switching_codec:1; enum hfp_hf_state hf_state; unsigned int codec; #endif @@ -157,8 +162,11 @@ finish: return t; } +static int codec_switch_stop_timer(struct rfcomm *rfcomm); + static void rfcomm_free(struct rfcomm *rfcomm) { + codec_switch_stop_timer(rfcomm); spa_list_remove(&rfcomm->link); if (rfcomm->path) free(rfcomm->path); @@ -185,7 +193,7 @@ static void rfcomm_send_cmd(struct spa_source *source, char *data) spa_log_error(backend->log, NAME": RFCOMM write error: %s", strerror(errno)); } -static void rfcomm_send_reply(struct spa_source *source, char *data) +static int rfcomm_send_reply(struct spa_source *source, char *data) { struct rfcomm *rfcomm = source->data; struct impl *backend = rfcomm->backend; @@ -199,6 +207,7 @@ static void rfcomm_send_reply(struct spa_source *source, char *data) * be caught with the HANGUP and ERROR events handled above */ if (len < 0) spa_log_error(backend->log, NAME": RFCOMM write error: %s", strerror(errno)); + return len; } #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE @@ -425,28 +434,41 @@ static bool rfcomm_hfp_ag(struct spa_source *source, char* buf) return false; } else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) { /* parse BCS(=Bluetooth Codec Selection) reply */ + bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL); + rfcomm->hfp_ag_switching_codec = false; + codec_switch_stop_timer(rfcomm); + if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) { spa_log_warn(backend->log, NAME": unsupported codec negotiation: %d", selected_codec); rfcomm_send_reply(source, "ERROR"); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -EIO); return true; } - spa_log_debug(backend->log, NAME": RFCOMM selected_codec = %i", selected_codec); - if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) { - if (rfcomm->transport) - spa_bt_transport_free(rfcomm->transport); + rfcomm->codec = selected_codec; - rfcomm->transport = _transport_create(rfcomm); - if (rfcomm->transport == NULL) { - spa_log_warn(backend->log, NAME": can't create transport: %m"); - // TODO: We should manage the missing transport - rfcomm_send_reply(source, "ERROR"); - return true; - } - rfcomm->transport->codec = selected_codec; - spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + spa_log_debug(backend->log, NAME": RFCOMM selected_codec = %i", selected_codec); + + /* Recreate transport, since previous connection may now be invalid */ + if (rfcomm->transport) + spa_bt_transport_free(rfcomm->transport); + + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, NAME": can't create transport: %m"); + // TODO: We should manage the missing transport + rfcomm_send_reply(source, "ERROR"); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM); + return true; } + rfcomm->transport->codec = selected_codec; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + rfcomm_send_reply(source, "OK"); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, 0); } else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) { if (gain <= 15) { /* t->microphone_gain = gain; */ @@ -983,6 +1005,136 @@ static const struct spa_bt_transport_implementation sco_transport_impl = { .release = sco_release_cb, }; +static struct rfcomm *device_find_rfcomm(struct impl *backend, struct spa_bt_device *device) +{ + struct rfcomm *rfcomm; + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->device == device) + return rfcomm; + } + return NULL; +} + +static int backend_native_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct impl *backend = data; + struct rfcomm *rfcomm; + + rfcomm = device_find_rfcomm(backend, device); + if (rfcomm == NULL || rfcomm->profile != SPA_BT_PROFILE_HFP_HF) + return -ENOTSUP; + + if (codec == HFP_AUDIO_CODEC_CVSD) + return 1; + + return (codec == HFP_AUDIO_CODEC_MSBC && + (rfcomm->profile == SPA_BT_PROFILE_HFP_AG || + rfcomm->profile == SPA_BT_PROFILE_HFP_HF) && + rfcomm->msbc_support_enabled_in_config && + rfcomm->msbc_supported_by_hfp && + rfcomm->codec_negotiation_supported) ? 1 : 0; +#else + return -ENOTSUP; +#endif +} + +static int codec_switch_stop_timer(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct itimerspec ts; + + if (rfcomm->timer.data == NULL) + return 0; + + spa_loop_remove_source(backend->main_loop, &rfcomm->timer); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL); + spa_system_close(backend->main_system, rfcomm->timer.fd); + rfcomm->timer.data = NULL; + return 0; +} + +static void codec_switch_timer_event(struct spa_source *source) +{ + struct rfcomm *rfcomm = source->data; + struct impl *backend = rfcomm->backend; + uint64_t exp; + + if (spa_system_timerfd_read(backend->main_system, source->fd, &exp) < 0) + spa_log_warn(backend->log, "error reading timerfd: %s", strerror(errno)); + + codec_switch_stop_timer(rfcomm); + + spa_log_debug(backend->log, "rfcomm %p: codec switch timeout", rfcomm); + + if (rfcomm->hfp_ag_switching_codec) { + rfcomm->hfp_ag_switching_codec = false; + if (rfcomm->device) + spa_bt_device_emit_codec_switched(rfcomm->device, -EIO); + } +} + +static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec) +{ + struct impl *backend = rfcomm->backend; + struct itimerspec ts; + + spa_log_debug(backend->log, "rfcomm %p: start timer", rfcomm); + if (rfcomm->timer.data == NULL) { + rfcomm->timer.data = rfcomm; + rfcomm->timer.func = codec_switch_timer_event; + rfcomm->timer.fd = spa_system_timerfd_create(backend->main_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + rfcomm->timer.mask = SPA_IO_IN; + rfcomm->timer.rmask = 0; + spa_loop_add_source(backend->main_loop, &rfcomm->timer); + } + ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC; + ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL); + return 0; +} + +static int backend_native_ensure_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct impl *backend = data; + struct rfcomm *rfcomm; + int res; + char msg[16]; + + res = backend_native_supports_codec(data, device, codec); + if (res <= 0) + return -EINVAL; + + rfcomm = device_find_rfcomm(backend, device); + if (rfcomm == NULL) + return -ENOTSUP; + + if (rfcomm->codec == codec) { + spa_bt_device_emit_codec_switched(device, 0); + return 0; + } + + snprintf(msg, sizeof(msg), "+BCS: %d", codec); + if ((res = rfcomm_send_reply(&rfcomm->source, msg)) < 0) + return res; + + rfcomm->hfp_ag_switching_codec = true; + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + + return 0; +#else + return -ENOTSUP; +#endif +} + static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata) { struct impl *backend = userdata; @@ -1502,6 +1654,8 @@ static const struct spa_bt_backend_implementation backend_impl = { .free = backend_native_free, .register_profiles = backend_native_register_profiles, .unregister_profiles = backend_native_unregister_profiles, + .ensure_codec = backend_native_ensure_codec, + .supports_codec = backend_native_supports_codec, }; struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, @@ -1526,6 +1680,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); backend->conn = dbus_connection; backend->sco.fd = -1; diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 991aa83be..cf6798a02 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -2206,6 +2206,26 @@ int spa_bt_device_ensure_a2dp_codec(struct spa_bt_device *device, const struct a return 0; } +int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec) +{ + struct spa_bt_monitor *monitor = device->monitor; + if (monitor->backend_hsphfpd_registered) + return spa_bt_backend_ensure_codec(monitor->backend_hsphfpd, device, codec); + if (monitor->backend_ofono_registered) + return spa_bt_backend_ensure_codec(monitor->backend_ofono, device, codec); + return spa_bt_backend_ensure_codec(monitor->backend_native, device, codec); +} + +int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec) +{ + struct spa_bt_monitor *monitor = device->monitor; + if (monitor->backend_hsphfpd_registered) + return spa_bt_backend_supports_codec(monitor->backend_hsphfpd, device, codec); + if (monitor->backend_ofono_registered) + return spa_bt_backend_supports_codec(monitor->backend_ofono, device, codec); + return spa_bt_backend_supports_codec(monitor->backend_native, device, codec); +} + static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn, const char *path, DBusMessage *m, void *userdata) { diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c index fc56d28c8..72db18fff 100644 --- a/spa/plugins/bluez5/bluez5-device.c +++ b/spa/plugins/bluez5/bluez5-device.c @@ -118,6 +118,7 @@ struct impl { uint32_t profile; const struct a2dp_codec *selected_a2dp_codec; /**< Codec wanted. NULL means any. */ + int selected_hfp_codec; unsigned int switching_codec:1; uint32_t prev_bt_connected_profiles; @@ -145,6 +146,28 @@ static void init_node(struct impl *this, struct node *node, uint32_t id) node->volumes[i] = 1.0; } +static const char *get_hfp_codec_name(unsigned int codec) +{ + switch (codec) { + case HFP_AUDIO_CODEC_MSBC: + return "mSBC"; + case HFP_AUDIO_CODEC_CVSD: + return "CVSD"; + } + return "unknown"; +} + +static const char *get_hfp_codec_key(unsigned int codec) +{ + switch (codec) { + case HFP_AUDIO_CODEC_MSBC: + return "msbc"; + case HFP_AUDIO_CODEC_CVSD: + return "cvsd"; + } + return "unknown"; +} + static const char *get_codec_name(struct spa_bt_transport *t) { if (t->a2dp_codec != NULL) @@ -208,7 +231,7 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t, } } -static struct spa_bt_transport *find_transport(struct impl *this, int profile, const struct a2dp_codec *a2dp_codec) +static struct spa_bt_transport *find_transport(struct impl *this, int profile, const struct a2dp_codec *a2dp_codec, unsigned int hfp_codec) { struct spa_bt_device *device = this->bt_dev; struct spa_bt_transport *t; @@ -229,7 +252,8 @@ static struct spa_bt_transport *find_transport(struct impl *this, int profile, c spa_list_for_each(t, &device->transport_list, device_link) { if ((t->profile & device->connected_profiles) && (t->profile & profile) == t->profile && - (codecs[i] == NULL || t->a2dp_codec == codecs[i])) + (codecs[i] == NULL || t->a2dp_codec == codecs[i]) && + (hfp_codec == 0 || t->codec == hfp_codec)) return t; } } @@ -315,9 +339,9 @@ static int emit_nodes(struct impl *this) break; case DEVICE_PROFILE_AG: if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) { - t = find_transport(this, SPA_BT_PROFILE_HFP_AG, NULL); + t = find_transport(this, SPA_BT_PROFILE_HFP_AG, NULL, 0); if (!t) - t = find_transport(this, SPA_BT_PROFILE_HSP_AG, NULL); + t = find_transport(this, SPA_BT_PROFILE_HSP_AG, NULL, 0); if (t) { emit_dynamic_node(&this->dyn_sco_source, this, t, 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE); @@ -326,7 +350,7 @@ static int emit_nodes(struct impl *this) } } if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) { - t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_codec); + t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_codec, 0); if (t) emit_dynamic_node(&this->dyn_a2dp_source, this, t, 2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE); @@ -334,23 +358,23 @@ static int emit_nodes(struct impl *this) break; case DEVICE_PROFILE_A2DP: if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) { - t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_codec); + t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_codec, 0); if (t) emit_dynamic_node(&this->dyn_a2dp_source, this, t, DEVICE_ID_SOURCE, 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, this->selected_a2dp_codec); + t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->selected_a2dp_codec, 0); if (t) emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK); } 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, NULL); + t = find_transport(this, SPA_BT_PROFILE_HFP_HF, NULL, this->selected_hfp_codec); if (!t) - t = find_transport(this, SPA_BT_PROFILE_HSP_HS, NULL); + t = find_transport(this, SPA_BT_PROFILE_HSP_HS, NULL, 0); if (t) { emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE); emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK); @@ -397,9 +421,11 @@ static void emit_remove_nodes(struct impl *this) } } -static int set_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *a2dp_codec) +static int set_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *a2dp_codec, int hfp_codec) { - if (this->profile == profile && a2dp_codec == this->selected_a2dp_codec) + if (this->profile == profile && + (this->profile != DEVICE_PROFILE_A2DP || a2dp_codec == this->selected_a2dp_codec) && + (this->profile != DEVICE_PROFILE_HSP_HFP || hfp_codec == this->selected_hfp_codec)) return 0; emit_remove_nodes(this); @@ -407,7 +433,7 @@ static int set_profile(struct impl *this, uint32_t profile, const struct a2dp_co spa_bt_device_release_transports(this->bt_dev); this->profile = profile; - this->selected_a2dp_codec = a2dp_codec; + this->prev_bt_connected_profiles = this->bt_dev->connected_profiles; /* * A2DP: ensure there's a transport with the selected codec (NULL means any). @@ -427,17 +453,33 @@ static int set_profile(struct impl *this, uint32_t profile, const struct a2dp_co } this->switching_codec = true; - this->prev_bt_connected_profiles = this->bt_dev->connected_profiles; + this->selected_a2dp_codec = a2dp_codec; 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 + if (ret < 0) { + if (ret != -ENOTSUP) + spa_log_error(this->log, NAME": failed to switch codec (%d), setting basic profile", ret); + } else { return 0; + } + } else if (profile == DEVICE_PROFILE_HSP_HFP && hfp_codec != 0 && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) { + int ret; + + this->switching_codec = true; + this->selected_hfp_codec = hfp_codec; + + ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, hfp_codec); + if (ret < 0) { + if (ret != -ENOTSUP) + 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; + this->selected_hfp_codec = 0; emit_nodes(this); this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; @@ -460,8 +502,10 @@ static void codec_switched(void *userdata, int 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) { + if (this->profile == DEVICE_PROFILE_A2DP && this->selected_a2dp_codec != NULL) { this->selected_a2dp_codec = NULL; + } else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->selected_hfp_codec != 0) { + this->selected_hfp_codec = 0; } else { this->profile = DEVICE_PROFILE_OFF; } @@ -621,46 +665,72 @@ static uint32_t profile_direction_mask(struct impl *this, uint32_t index) return mask; } -static uint32_t get_profile_from_index(struct impl *this, uint32_t index, const struct a2dp_codec **codec) +static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, const struct a2dp_codec **codec, int *hfp_codec) { + uint32_t a2dp_codec_mask = 0x100; + uint32_t hfp_codec_mask = 0x200; + /* - * XXX: The A2DP codec should probably become a separate param, and not have + * XXX: The codecs should probably become a separate param, and not have * XXX: separate profiles for each one. */ *codec = NULL; + *hfp_codec = 0; + *next = index + 1; - if (index < 4) - return index; - - /* A2DP sources don't have codec profiles (device chooses it) */ - if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) - return SPA_ID_INVALID; - - if (index - 4 < this->supported_codec_count) { - *codec = this->supported_codecs[index - 4]; - return DEVICE_PROFILE_A2DP; - } else { - return SPA_ID_INVALID; + if (index < a2dp_codec_mask) { + if (index >= 3) + *next = a2dp_codec_mask; + if (index <= 3) + return index; + } else if (index & a2dp_codec_mask) { + uint32_t i = index & ~a2dp_codec_mask; + if (i + 1 >= this->supported_codec_count) + *next = hfp_codec_mask; + if (i < this->supported_codec_count) { + *codec = this->supported_codecs[i]; + return DEVICE_PROFILE_A2DP; + } + } else if (index & hfp_codec_mask) { + uint32_t i = index & ~hfp_codec_mask; + if (i == 0) { + *hfp_codec = HFP_AUDIO_CODEC_CVSD; + return DEVICE_PROFILE_HSP_HFP; + } else if (i == 1) { + *hfp_codec = HFP_AUDIO_CODEC_MSBC; + return DEVICE_PROFILE_HSP_HFP; + } } + + *next = SPA_ID_INVALID; + return SPA_ID_INVALID; } -static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *codec) +static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *codec, int hfp_codec) { - size_t i; + uint32_t a2dp_codec_mask = 0x100; + uint32_t hfp_codec_mask = 0x200; + uint32_t i; - if (profile != DEVICE_PROFILE_A2DP) + if (profile == DEVICE_PROFILE_OFF || profile == DEVICE_PROFILE_AG) return profile; - if (codec == NULL) - return DEVICE_PROFILE_A2DP; - /* A2DP sources don't have codec profiles (device chooses it) */ - if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) - return SPA_ID_INVALID; + if (profile == DEVICE_PROFILE_A2DP) { + if (codec == NULL) + return profile; - for (i = 0; i < this->supported_codec_count; ++i) { - if (this->supported_codecs[i] == codec) - return 4 + i; + for (i = 0; i < this->supported_codec_count; ++i) { + if (this->supported_codecs[i] == codec) + return a2dp_codec_mask | i; + } + } + + if (profile == DEVICE_PROFILE_HSP_HFP) { + if (hfp_codec == 0) + return profile; + + return hfp_codec_mask | ((hfp_codec == HFP_AUDIO_CODEC_MSBC) ? 1 : 0); } return SPA_ID_INVALID; @@ -683,10 +753,11 @@ static void set_initial_profile(struct impl *this) if (!(this->bt_dev->connected_profiles & i)) continue; - t = find_transport(this, i, NULL); + t = find_transport(this, i, NULL, 0); if (t) { this->profile = (i == SPA_BT_PROFILE_A2DP_SOURCE) ? DEVICE_PROFILE_AG : DEVICE_PROFILE_A2DP; + this->selected_hfp_codec = 0; /* Source devices don't have codec selection */ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) @@ -701,11 +772,17 @@ static void set_initial_profile(struct impl *this) if (!(this->bt_dev->connected_profiles & i)) continue; - t = find_transport(this, i, NULL); + t = find_transport(this, i, NULL, 0); if (t) { this->profile = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ? DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP; this->selected_a2dp_codec = NULL; + + /* Source devices don't have codec selection */ + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG) + this->selected_hfp_codec = 0; + else + this->selected_hfp_codec = t->codec; return; } } @@ -715,7 +792,7 @@ static void set_initial_profile(struct impl *this) } static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b, - uint32_t id, uint32_t index, uint32_t profile_index, const struct a2dp_codec *codec) + uint32_t id, uint32_t index, uint32_t profile_index, const struct a2dp_codec *codec, int hfp_codec) { struct spa_bt_device *device = this->bt_dev; struct spa_pod_frame f[2]; @@ -775,11 +852,27 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * if (profile == 0) { return NULL; } else { - desc = "Headset Head Unit (HSP/HFP)"; + desc = "Headset Head Unit (HSP/HFP%s%s)"; } name = spa_bt_profile_name(profile); n_source++; n_sink++; + if (hfp_codec != 0) { + bool codec_ok = !(profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + if (spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1) + codec_ok = false; + if (!codec_ok) { + errno = -EINVAL; + return NULL; + } + name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_key(hfp_codec)); + desc_and_codec = spa_aprintf(desc, ", codec ", get_hfp_codec_name(hfp_codec)); + name = name_and_codec; + desc = desc_and_codec; + } else { + desc_and_codec = spa_aprintf(desc, "", ""); + desc = desc_and_codec; + } break; } default: @@ -830,7 +923,8 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, enum spa_bt_form_factor ff; const struct a2dp_codec *codec; char name[128]; - uint32_t i, j, mask; + uint32_t i, j, mask, next; + int hfp_codec; ff = spa_bt_form_factor_from_class(device->bluetooth_class); @@ -930,7 +1024,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, spa_pod_builder_pop(b, &f[1]); spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0); spa_pod_builder_push_array(b, &f[1]); - for (i = 1; (j = get_profile_from_index(this, i, &codec)) != SPA_ID_INVALID; i++) + for (i = 1; (j = get_profile_from_index(this, i, &next, &codec, &hfp_codec)) != SPA_ID_INVALID; i = next) if (profile_direction_mask(this, j) & (1 << direction)) spa_pod_builder_int(b, i); spa_pod_builder_pop(b, &f[1]); @@ -1002,15 +1096,16 @@ static int impl_enum_params(void *object, int seq, { uint32_t profile; const struct a2dp_codec *codec; + int hfp_codec; - profile = get_profile_from_index(this, result.index, &codec); + profile = get_profile_from_index(this, result.index, &result.next, &codec, &hfp_codec); switch (profile) { case DEVICE_PROFILE_OFF: case DEVICE_PROFILE_AG: case DEVICE_PROFILE_A2DP: case DEVICE_PROFILE_HSP_HFP: - param = build_profile(this, &b, id, result.index, profile, codec); + param = build_profile(this, &b, id, result.index, profile, codec, hfp_codec); if (param == NULL) goto next; break; @@ -1025,8 +1120,8 @@ static int impl_enum_params(void *object, int seq, switch (result.index) { case 0: - index = get_index_from_profile(this, this->profile, this->selected_a2dp_codec); - param = build_profile(this, &b, id, index, this->profile, this->selected_a2dp_codec); + index = get_index_from_profile(this, this->profile, this->selected_a2dp_codec, this->selected_hfp_codec); + param = build_profile(this, &b, id, index, this->profile, this->selected_a2dp_codec, this->selected_hfp_codec); if (param == NULL) return 0; break; @@ -1246,9 +1341,10 @@ static int impl_set_param(void *object, switch (id) { case SPA_PARAM_Profile: { - uint32_t id; + uint32_t id, next; uint32_t profile; const struct a2dp_codec *codec; + int hfp_codec; if ((res = spa_pod_parse_object(param, SPA_TYPE_OBJECT_ParamProfile, NULL, @@ -1258,13 +1354,13 @@ static int impl_set_param(void *object, return res; } - profile = get_profile_from_index(this, id, &codec); + profile = get_profile_from_index(this, id, &next, &codec, &hfp_codec); if (profile == SPA_ID_INVALID) return -EINVAL; - spa_log_debug(this->log, NAME": setting profile %d codec %s", profile, - codec ? codec->name : ""); - set_profile(this, profile, codec); + spa_log_debug(this->log, NAME": setting profile %d codec:%s hfp-codec:%d", profile, + codec ? codec->name : "", hfp_codec); + set_profile(this, profile, codec, hfp_codec); break; } case SPA_PARAM_Route: diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index b978e73e5..373e006fb 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -430,6 +430,8 @@ 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); bool spa_bt_device_supports_a2dp_codec(struct spa_bt_device *device, const struct a2dp_codec *codec); const struct a2dp_codec **spa_bt_device_get_supported_a2dp_codecs(struct spa_bt_device *device, size_t *count); +int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec); +int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec); int spa_bt_device_release_transports(struct spa_bt_device *device); int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage); @@ -562,6 +564,8 @@ struct spa_bt_backend_implementation { int (*unregister_profiles) (void *data); int (*unregistered) (void *data); int (*add_filters) (void *data); + int (*ensure_codec) (void *data, struct spa_bt_device *device, unsigned int codec); + int (*supports_codec) (void *data, struct spa_bt_device *device, unsigned int codec); }; struct spa_bt_backend { @@ -586,6 +590,8 @@ struct spa_bt_backend { #define spa_bt_backend_unregister_profiles(b) spa_bt_backend_impl(b, unregister_profiles, 0) #define spa_bt_backend_unregistered(b) spa_bt_backend_impl(b, unregistered, 0) #define spa_bt_backend_add_filters(b) spa_bt_backend_impl(b, add_filters, 0) +#define spa_bt_backend_ensure_codec(b,...) spa_bt_backend_impl(b, ensure_codec, 0, __VA_ARGS__) +#define spa_bt_backend_supports_codec(b,...) spa_bt_backend_impl(b, supports_codec, 0, __VA_ARGS__) static inline struct spa_bt_backend *dummy_backend_new(struct spa_bt_monitor *monitor, void *dbus_connection,