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;

View file

@ -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)
{

View file

@ -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 : "<null>");
set_profile(this, profile, codec);
spa_log_debug(this->log, NAME": setting profile %d codec:%s hfp-codec:%d", profile,
codec ? codec->name : "<null>", hfp_codec);
set_profile(this, profile, codec, hfp_codec);
break;
}
case SPA_PARAM_Route:

View file

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