bluez5: implement HFP codec switching

Implement switching HFP codecs in bluez5-devices, currently only for
backend-native.

Codecs are exposed via profiles similarly as for A2DP.

Some hardware appears to not properly reply to the +BCS message. Catch
these cases with a timeout, in which case we fall back to previously
existing transports.
This commit is contained in:
Pauli Virtanen 2021-03-18 23:15:03 +02:00 committed by Wim Taymans
parent e18df4e344
commit 79e098bdf2
4 changed files with 348 additions and 71 deletions

View file

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