mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
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:
parent
e18df4e344
commit
79e098bdf2
4 changed files with 348 additions and 71 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue