mirror of
https://git.kernel.org/pub/scm/bluetooth/bluez.git
synced 2024-11-29 15:14:31 +08:00
27f17d4083
audio_open_output_stream always tries to open 1st registered endpoint based on assumption that there is only one endpoint registered anyway (due to support for only one codec). With more endpoints available in future we need to be able to retrieve endpoint id which is connected and use it for streaming. This patch adds special case for id=0 in open_stream IPC to return 1st opened endpoint on BlueZ side which is enough for now since only one headset can be connected at any time (i.e. we should not have more than 1 endpoint opened).
1766 lines
38 KiB
C
1766 lines
38 KiB
C
/*
|
|
*
|
|
* BlueZ - Bluetooth protocol stack for Linux
|
|
*
|
|
* Copyright (C) 2013-2014 Intel Corporation. All rights reserved.
|
|
*
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <glib.h>
|
|
|
|
#include "btio/btio.h"
|
|
#include "lib/bluetooth.h"
|
|
#include "lib/sdp.h"
|
|
#include "lib/sdp_lib.h"
|
|
#include "profiles/audio/a2dp-codecs.h"
|
|
#include "src/log.h"
|
|
#include "hal-msg.h"
|
|
#include "ipc-common.h"
|
|
#include "ipc.h"
|
|
#include "a2dp.h"
|
|
#include "utils.h"
|
|
#include "bluetooth.h"
|
|
#include "avdtp.h"
|
|
#include "avrcp.h"
|
|
#include "audio-msg.h"
|
|
|
|
#define L2CAP_PSM_AVDTP 0x19
|
|
#define SVC_HINT_CAPTURING 0x08
|
|
#define IDLE_TIMEOUT 1
|
|
#define AUDIO_RETRY_TIMEOUT 2
|
|
|
|
static GIOChannel *server = NULL;
|
|
static GSList *devices = NULL;
|
|
static GSList *endpoints = NULL;
|
|
static GSList *setups = NULL;
|
|
static bdaddr_t adapter_addr;
|
|
static uint32_t record_id = 0;
|
|
static guint audio_retry_id = 0;
|
|
static bool audio_retrying = false;
|
|
|
|
static struct ipc *hal_ipc = NULL;
|
|
static struct ipc *audio_ipc = NULL;
|
|
|
|
struct a2dp_preset {
|
|
void *data;
|
|
int8_t len;
|
|
};
|
|
|
|
struct a2dp_endpoint {
|
|
uint8_t id;
|
|
uint8_t codec;
|
|
struct avdtp_local_sep *sep;
|
|
struct a2dp_preset *caps;
|
|
GSList *presets;
|
|
};
|
|
|
|
struct a2dp_device {
|
|
bdaddr_t dst;
|
|
uint8_t state;
|
|
GIOChannel *io;
|
|
struct avdtp *session;
|
|
guint idle_id;
|
|
};
|
|
|
|
struct a2dp_setup {
|
|
struct a2dp_device *dev;
|
|
struct a2dp_endpoint *endpoint;
|
|
struct a2dp_preset *preset;
|
|
struct avdtp_stream *stream;
|
|
uint8_t state;
|
|
};
|
|
|
|
static int device_cmp(gconstpointer s, gconstpointer user_data)
|
|
{
|
|
const struct a2dp_device *dev = s;
|
|
const bdaddr_t *dst = user_data;
|
|
|
|
return bacmp(&dev->dst, dst);
|
|
}
|
|
|
|
static void preset_free(void *data)
|
|
{
|
|
struct a2dp_preset *preset = data;
|
|
|
|
g_free(preset->data);
|
|
g_free(preset);
|
|
}
|
|
|
|
static void unregister_endpoint(void *data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = data;
|
|
|
|
if (endpoint->sep)
|
|
avdtp_unregister_sep(endpoint->sep);
|
|
|
|
if (endpoint->caps)
|
|
preset_free(endpoint->caps);
|
|
|
|
g_slist_free_full(endpoint->presets, preset_free);
|
|
|
|
g_free(endpoint);
|
|
}
|
|
|
|
static void setup_free(void *data)
|
|
{
|
|
struct a2dp_setup *setup = data;
|
|
|
|
if (!g_slist_find(setup->endpoint->presets, setup->preset))
|
|
preset_free(setup->preset);
|
|
|
|
g_free(setup);
|
|
}
|
|
|
|
static void setup_remove(struct a2dp_setup *setup)
|
|
{
|
|
setups = g_slist_remove(setups, setup);
|
|
setup_free(setup);
|
|
}
|
|
|
|
static void setup_remove_all_by_dev(struct a2dp_device *dev)
|
|
{
|
|
GSList *l = setups;
|
|
|
|
while (l) {
|
|
struct a2dp_setup *setup = l->data;
|
|
GSList *next = g_slist_next(l);
|
|
|
|
if (setup->dev == dev)
|
|
setup_remove(setup);
|
|
|
|
l = next;
|
|
}
|
|
}
|
|
|
|
static void a2dp_device_free(void *data)
|
|
{
|
|
struct a2dp_device *dev = data;
|
|
|
|
if (dev->idle_id > 0)
|
|
g_source_remove(dev->idle_id);
|
|
|
|
if (dev->session)
|
|
avdtp_unref(dev->session);
|
|
|
|
if (dev->io) {
|
|
g_io_channel_shutdown(dev->io, FALSE, NULL);
|
|
g_io_channel_unref(dev->io);
|
|
}
|
|
|
|
setup_remove_all_by_dev(dev);
|
|
|
|
g_free(dev);
|
|
}
|
|
|
|
static void a2dp_device_remove(struct a2dp_device *dev)
|
|
{
|
|
devices = g_slist_remove(devices, dev);
|
|
a2dp_device_free(dev);
|
|
}
|
|
|
|
static struct a2dp_device *a2dp_device_new(const bdaddr_t *dst)
|
|
{
|
|
struct a2dp_device *dev;
|
|
|
|
dev = g_new0(struct a2dp_device, 1);
|
|
bacpy(&dev->dst, dst);
|
|
devices = g_slist_prepend(devices, dev);
|
|
|
|
return dev;
|
|
}
|
|
|
|
static bool a2dp_device_connect(struct a2dp_device *dev, BtIOConnect cb)
|
|
{
|
|
GError *err = NULL;
|
|
|
|
dev->io = bt_io_connect(cb, dev, NULL, &err,
|
|
BT_IO_OPT_SOURCE_BDADDR, &adapter_addr,
|
|
BT_IO_OPT_DEST_BDADDR, &dev->dst,
|
|
BT_IO_OPT_PSM, L2CAP_PSM_AVDTP,
|
|
BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM,
|
|
BT_IO_OPT_INVALID);
|
|
if (err) {
|
|
error("%s", err->message);
|
|
g_error_free(err);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void bt_a2dp_notify_state(struct a2dp_device *dev, uint8_t state)
|
|
{
|
|
struct hal_ev_a2dp_conn_state ev;
|
|
char address[18];
|
|
|
|
if (dev->state == state)
|
|
return;
|
|
|
|
dev->state = state;
|
|
|
|
ba2str(&dev->dst, address);
|
|
DBG("device %s state %u", address, state);
|
|
|
|
bdaddr2android(&dev->dst, ev.bdaddr);
|
|
ev.state = state;
|
|
|
|
ipc_send_notif(hal_ipc, HAL_SERVICE_ID_A2DP, HAL_EV_A2DP_CONN_STATE,
|
|
sizeof(ev), &ev);
|
|
|
|
if (state != HAL_A2DP_STATE_DISCONNECTED)
|
|
return;
|
|
|
|
bt_avrcp_disconnect(&dev->dst);
|
|
|
|
a2dp_device_remove(dev);
|
|
}
|
|
|
|
static void bt_audio_notify_state(struct a2dp_setup *setup, uint8_t state)
|
|
{
|
|
struct hal_ev_a2dp_audio_state ev;
|
|
char address[18];
|
|
|
|
if (setup->state == state)
|
|
return;
|
|
|
|
setup->state = state;
|
|
|
|
ba2str(&setup->dev->dst, address);
|
|
DBG("device %s state %u", address, state);
|
|
|
|
bdaddr2android(&setup->dev->dst, ev.bdaddr);
|
|
ev.state = state;
|
|
|
|
ipc_send_notif(hal_ipc, HAL_SERVICE_ID_A2DP, HAL_EV_A2DP_AUDIO_STATE,
|
|
sizeof(ev), &ev);
|
|
}
|
|
|
|
static void disconnect_cb(void *user_data)
|
|
{
|
|
struct a2dp_device *dev = user_data;
|
|
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_DISCONNECTED);
|
|
}
|
|
|
|
static int sbc_check_config(void *caps, uint8_t caps_len, void *conf,
|
|
uint8_t conf_len)
|
|
{
|
|
a2dp_sbc_t *cap, *config;
|
|
|
|
if (conf_len != caps_len || conf_len != sizeof(a2dp_sbc_t)) {
|
|
error("SBC: Invalid configuration size (%u)", conf_len);
|
|
return -EINVAL;
|
|
}
|
|
|
|
cap = caps;
|
|
config = conf;
|
|
|
|
if (!(cap->frequency & config->frequency)) {
|
|
error("SBC: Unsupported frequency (%u) by endpoint",
|
|
config->frequency);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!(cap->channel_mode & config->channel_mode)) {
|
|
error("SBC: Unsupported channel mode (%u) by endpoint",
|
|
config->channel_mode);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!(cap->block_length & config->block_length)) {
|
|
error("SBC: Unsupported block length (%u) by endpoint",
|
|
config->block_length);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!(cap->allocation_method & config->allocation_method)) {
|
|
error("SBC: Unsupported allocation method (%u) by endpoint",
|
|
config->block_length);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config->max_bitpool < cap->min_bitpool) {
|
|
error("SBC: Invalid maximun bitpool (%u < %u)",
|
|
config->max_bitpool, cap->min_bitpool);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config->min_bitpool > cap->max_bitpool) {
|
|
error("SBC: Invalid minimun bitpool (%u > %u)",
|
|
config->min_bitpool, cap->min_bitpool);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config->max_bitpool > cap->max_bitpool)
|
|
return -ERANGE;
|
|
|
|
if (config->min_bitpool < cap->min_bitpool)
|
|
return -ERANGE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int aac_check_config(void *caps, uint8_t caps_len, void *conf,
|
|
uint8_t conf_len)
|
|
{
|
|
a2dp_aac_t *cap, *config;
|
|
|
|
if (conf_len != caps_len || conf_len != sizeof(a2dp_aac_t)) {
|
|
error("AAC: Invalid configuration size (%u)", conf_len);
|
|
return -EINVAL;
|
|
}
|
|
|
|
cap = caps;
|
|
config = conf;
|
|
|
|
if (!(cap->object_type & config->object_type)) {
|
|
error("AAC: Unsupported object type (%u) by endpoint",
|
|
config->object_type);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!(AAC_GET_FREQUENCY(*cap) & AAC_GET_FREQUENCY(*config))) {
|
|
error("AAC: Unsupported frequency (%u) by endpoint",
|
|
AAC_GET_FREQUENCY(*config));
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!(cap->channels & config->channels)) {
|
|
error("AAC: Unsupported channels (%u) by endpoint",
|
|
config->channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* VBR support in SNK is mandatory but let's make sure we don't try to
|
|
* have VBR on remote which for some reason does not support it
|
|
*/
|
|
if (!cap->vbr && config->vbr) {
|
|
error("AAC: Unsupported VBR (%u) by endpoint",
|
|
config->vbr);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (AAC_GET_BITRATE(*cap) < AAC_GET_BITRATE(*config))
|
|
return -ERANGE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int aptx_check_config(void *caps, uint8_t caps_len, void *conf,
|
|
uint8_t conf_len)
|
|
{
|
|
a2dp_aptx_t *cap, *config;
|
|
|
|
if (conf_len != caps_len || conf_len != sizeof(a2dp_aptx_t)) {
|
|
error("APTX: Invalid configuration size (%u)", conf_len);
|
|
return -EINVAL;
|
|
}
|
|
|
|
cap = caps;
|
|
config = conf;
|
|
|
|
if (!(cap->frequency & config->frequency)) {
|
|
error("APTX: Unsupported frequenct (%u) by endpoint",
|
|
config->frequency);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!(cap->channel_mode & config->channel_mode)) {
|
|
error("APTX: Unsupported channel mode (%u) by endpoint",
|
|
config->channel_mode);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int check_capabilities(struct a2dp_preset *preset,
|
|
struct avdtp_media_codec_capability *codec,
|
|
uint8_t codec_len)
|
|
{
|
|
a2dp_vendor_codec_t *vndcodec;
|
|
|
|
/* Codec specific */
|
|
switch (codec->media_codec_type) {
|
|
case A2DP_CODEC_SBC:
|
|
return sbc_check_config(codec->data, codec_len, preset->data,
|
|
preset->len);
|
|
case A2DP_CODEC_MPEG24:
|
|
return aac_check_config(codec->data, codec_len, preset->data,
|
|
preset->len);
|
|
case A2DP_CODEC_VENDOR:
|
|
vndcodec = (void *) codec->data;
|
|
if (btohl(vndcodec->vendor_id) == APTX_VENDOR_ID &&
|
|
btohs(vndcodec->codec_id) == APTX_CODEC_ID)
|
|
return aptx_check_config(codec->data, codec_len,
|
|
preset->data, preset->len);
|
|
return -EINVAL;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
static struct a2dp_preset *sbc_select_range(void *caps, uint8_t caps_len,
|
|
void *conf, uint8_t conf_len)
|
|
{
|
|
struct a2dp_preset *p;
|
|
a2dp_sbc_t *cap, *config;
|
|
|
|
cap = caps;
|
|
config = conf;
|
|
|
|
config->min_bitpool = MAX(config->min_bitpool, cap->min_bitpool);
|
|
config->max_bitpool = MIN(config->max_bitpool, cap->max_bitpool);
|
|
|
|
p = g_new0(struct a2dp_preset, 1);
|
|
p->len = conf_len;
|
|
p->data = g_memdup(conf, p->len);
|
|
|
|
return p;
|
|
}
|
|
|
|
static struct a2dp_preset *aac_select_range(void *caps, uint8_t caps_len,
|
|
void *conf, uint8_t conf_len)
|
|
{
|
|
struct a2dp_preset *p;
|
|
a2dp_aac_t *cap, *config;
|
|
uint32_t bitrate;
|
|
|
|
cap = caps;
|
|
config = conf;
|
|
|
|
bitrate = MIN(AAC_GET_BITRATE(*cap), AAC_GET_BITRATE(*config));
|
|
AAC_SET_BITRATE(*config, bitrate);
|
|
|
|
p = g_new0(struct a2dp_preset, 1);
|
|
p->len = conf_len;
|
|
p->data = g_memdup(conf, p->len);
|
|
|
|
return p;
|
|
}
|
|
|
|
static struct a2dp_preset *select_preset_range(struct a2dp_preset *preset,
|
|
struct avdtp_media_codec_capability *codec,
|
|
uint8_t codec_len)
|
|
{
|
|
/* Codec specific */
|
|
switch (codec->media_codec_type) {
|
|
case A2DP_CODEC_SBC:
|
|
return sbc_select_range(codec->data, codec_len, preset->data,
|
|
preset->len);
|
|
case A2DP_CODEC_MPEG24:
|
|
return aac_select_range(codec->data, codec_len, preset->data,
|
|
preset->len);
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static struct a2dp_preset *select_preset(struct a2dp_endpoint *endpoint,
|
|
struct avdtp_remote_sep *rsep)
|
|
{
|
|
struct avdtp_service_capability *service;
|
|
struct avdtp_media_codec_capability *codec;
|
|
GSList *l;
|
|
uint8_t codec_len;
|
|
|
|
service = avdtp_get_codec(rsep);
|
|
codec = (struct avdtp_media_codec_capability *) service->data;
|
|
codec_len = service->length - sizeof(*codec);
|
|
|
|
for (l = endpoint->presets; l; l = g_slist_next(l)) {
|
|
struct a2dp_preset *preset = l->data;
|
|
int err;
|
|
|
|
err = check_capabilities(preset, codec, codec_len);
|
|
if (err == 0)
|
|
return preset;
|
|
|
|
if (err == -ERANGE)
|
|
return select_preset_range(preset, codec, codec_len);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void setup_add(struct a2dp_device *dev, struct a2dp_endpoint *endpoint,
|
|
struct a2dp_preset *preset, struct avdtp_stream *stream)
|
|
{
|
|
struct a2dp_setup *setup;
|
|
|
|
setup = g_new0(struct a2dp_setup, 1);
|
|
setup->dev = dev;
|
|
setup->endpoint = endpoint;
|
|
setup->preset = preset;
|
|
setup->stream = stream;
|
|
setups = g_slist_append(setups, setup);
|
|
|
|
if (dev->idle_id > 0) {
|
|
g_source_remove(dev->idle_id);
|
|
dev->idle_id = 0;
|
|
}
|
|
}
|
|
|
|
static int select_configuration(struct a2dp_device *dev,
|
|
struct a2dp_endpoint *endpoint,
|
|
struct avdtp_remote_sep *rsep)
|
|
{
|
|
struct a2dp_preset *preset;
|
|
struct avdtp_stream *stream;
|
|
struct avdtp_service_capability *service;
|
|
struct avdtp_media_codec_capability *codec;
|
|
GSList *caps;
|
|
int err;
|
|
|
|
preset = select_preset(endpoint, rsep);
|
|
if (!preset) {
|
|
error("Unable to select codec preset");
|
|
return -EINVAL;
|
|
}
|
|
|
|
service = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, NULL, 0);
|
|
caps = g_slist_append(NULL, service);
|
|
|
|
codec = g_malloc0(sizeof(*codec) + preset->len);
|
|
codec->media_type = AVDTP_MEDIA_TYPE_AUDIO;
|
|
codec->media_codec_type = endpoint->codec;
|
|
memcpy(codec->data, preset->data, preset->len);
|
|
|
|
service = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, codec,
|
|
sizeof(*codec) + preset->len);
|
|
caps = g_slist_append(caps, service);
|
|
|
|
g_free(codec);
|
|
|
|
err = avdtp_set_configuration(dev->session, rsep, endpoint->sep, caps,
|
|
&stream);
|
|
g_slist_free_full(caps, g_free);
|
|
if (err < 0) {
|
|
error("avdtp_set_configuration: %s", strerror(-err));
|
|
return err;
|
|
}
|
|
|
|
setup_add(dev, endpoint, preset, stream);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void discover_cb(struct avdtp *session, GSList *seps,
|
|
struct avdtp_error *err, void *user_data)
|
|
{
|
|
struct a2dp_device *dev = user_data;
|
|
struct a2dp_endpoint *endpoint = NULL;
|
|
struct avdtp_remote_sep *rsep = NULL;
|
|
GSList *l;
|
|
|
|
for (l = endpoints; l; l = g_slist_next(l)) {
|
|
endpoint = l->data;
|
|
|
|
rsep = avdtp_find_remote_sep(session, endpoint->sep);
|
|
if (rsep)
|
|
break;
|
|
}
|
|
|
|
if (!rsep) {
|
|
error("Unable to find matching endpoint");
|
|
goto failed;
|
|
}
|
|
|
|
if (select_configuration(dev, endpoint, rsep) < 0)
|
|
goto failed;
|
|
|
|
return;
|
|
|
|
failed:
|
|
avdtp_shutdown(session);
|
|
}
|
|
|
|
static gboolean idle_timeout(gpointer user_data)
|
|
{
|
|
struct a2dp_device *dev = user_data;
|
|
int err;
|
|
|
|
dev->idle_id = 0;
|
|
|
|
err = avdtp_discover(dev->session, discover_cb, dev);
|
|
if (err == 0)
|
|
return FALSE;
|
|
|
|
error("avdtp_discover: %s", strerror(-err));
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_DISCONNECTED);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void signaling_connect_cb(GIOChannel *chan, GError *err,
|
|
gpointer user_data)
|
|
{
|
|
struct a2dp_device *dev = user_data;
|
|
uint16_t imtu, omtu;
|
|
GError *gerr = NULL;
|
|
int fd;
|
|
|
|
if (err) {
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_DISCONNECTED);
|
|
error("%s", err->message);
|
|
return;
|
|
}
|
|
|
|
bt_io_get(chan, &gerr,
|
|
BT_IO_OPT_IMTU, &imtu,
|
|
BT_IO_OPT_OMTU, &omtu,
|
|
BT_IO_OPT_INVALID);
|
|
if (gerr) {
|
|
error("%s", gerr->message);
|
|
g_error_free(gerr);
|
|
goto failed;
|
|
}
|
|
|
|
fd = g_io_channel_unix_get_fd(chan);
|
|
|
|
/* FIXME: Add proper version */
|
|
dev->session = avdtp_new(fd, imtu, omtu, 0x0100);
|
|
if (!dev->session)
|
|
goto failed;
|
|
|
|
avdtp_add_disconnect_cb(dev->session, disconnect_cb, dev);
|
|
|
|
/* Proceed to stream setup if initiator */
|
|
if (dev->io) {
|
|
int perr;
|
|
|
|
g_io_channel_unref(dev->io);
|
|
dev->io = NULL;
|
|
|
|
perr = avdtp_discover(dev->session, discover_cb, dev);
|
|
if (perr < 0) {
|
|
error("avdtp_discover: %s", strerror(-perr));
|
|
goto failed;
|
|
}
|
|
bt_avrcp_connect(&dev->dst);
|
|
} else /* Init idle timeout to discover */
|
|
dev->idle_id = g_timeout_add_seconds(IDLE_TIMEOUT, idle_timeout,
|
|
dev);
|
|
|
|
return;
|
|
|
|
failed:
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_DISCONNECTED);
|
|
}
|
|
|
|
static void bt_a2dp_connect(const void *buf, uint16_t len)
|
|
{
|
|
const struct hal_cmd_a2dp_connect *cmd = buf;
|
|
struct a2dp_device *dev;
|
|
uint8_t status;
|
|
char addr[18];
|
|
bdaddr_t dst;
|
|
GSList *l;
|
|
|
|
DBG("");
|
|
|
|
android2bdaddr(&cmd->bdaddr, &dst);
|
|
|
|
l = g_slist_find_custom(devices, &dst, device_cmp);
|
|
if (l) {
|
|
status = HAL_STATUS_FAILED;
|
|
goto failed;
|
|
}
|
|
|
|
dev = a2dp_device_new(&dst);
|
|
if (!a2dp_device_connect(dev, signaling_connect_cb)) {
|
|
a2dp_device_remove(dev);
|
|
status = HAL_STATUS_FAILED;
|
|
goto failed;
|
|
}
|
|
|
|
ba2str(&dev->dst, addr);
|
|
DBG("connecting to %s", addr);
|
|
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_CONNECTING);
|
|
|
|
status = HAL_STATUS_SUCCESS;
|
|
|
|
failed:
|
|
ipc_send_rsp(hal_ipc, HAL_SERVICE_ID_A2DP, HAL_OP_A2DP_CONNECT, status);
|
|
}
|
|
|
|
static void bt_a2dp_disconnect(const void *buf, uint16_t len)
|
|
{
|
|
const struct hal_cmd_a2dp_connect *cmd = buf;
|
|
uint8_t status;
|
|
struct a2dp_device *dev;
|
|
GSList *l;
|
|
bdaddr_t dst;
|
|
|
|
DBG("");
|
|
|
|
android2bdaddr(&cmd->bdaddr, &dst);
|
|
|
|
l = g_slist_find_custom(devices, &dst, device_cmp);
|
|
if (!l) {
|
|
status = HAL_STATUS_FAILED;
|
|
goto failed;
|
|
}
|
|
|
|
dev = l->data;
|
|
status = HAL_STATUS_SUCCESS;
|
|
|
|
if (dev->io) {
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_DISCONNECTED);
|
|
goto failed;
|
|
}
|
|
|
|
/* Wait AVDTP session to shutdown */
|
|
avdtp_shutdown(dev->session);
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_DISCONNECTING);
|
|
|
|
failed:
|
|
ipc_send_rsp(hal_ipc, HAL_SERVICE_ID_A2DP, HAL_OP_A2DP_DISCONNECT,
|
|
status);
|
|
}
|
|
|
|
static const struct ipc_handler cmd_handlers[] = {
|
|
/* HAL_OP_A2DP_CONNECT */
|
|
{ bt_a2dp_connect, false, sizeof(struct hal_cmd_a2dp_connect) },
|
|
/* HAL_OP_A2DP_DISCONNECT */
|
|
{ bt_a2dp_disconnect, false, sizeof(struct hal_cmd_a2dp_disconnect) },
|
|
};
|
|
|
|
static struct a2dp_setup *find_setup_by_device(struct a2dp_device *dev)
|
|
{
|
|
GSList *l;
|
|
|
|
for (l = setups; l; l = g_slist_next(l)) {
|
|
struct a2dp_setup *setup = l->data;
|
|
|
|
if (setup->dev == dev)
|
|
return setup;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void transport_connect_cb(GIOChannel *chan, GError *err,
|
|
gpointer user_data)
|
|
{
|
|
struct a2dp_device *dev = user_data;
|
|
struct a2dp_setup *setup;
|
|
uint16_t imtu, omtu;
|
|
GError *gerr = NULL;
|
|
int fd;
|
|
|
|
if (err) {
|
|
error("%s", err->message);
|
|
return;
|
|
}
|
|
|
|
setup = find_setup_by_device(dev);
|
|
if (!setup) {
|
|
error("Unable to find stream setup");
|
|
return;
|
|
}
|
|
|
|
bt_io_get(chan, &gerr,
|
|
BT_IO_OPT_IMTU, &imtu,
|
|
BT_IO_OPT_OMTU, &omtu,
|
|
BT_IO_OPT_INVALID);
|
|
if (gerr) {
|
|
error("%s", gerr->message);
|
|
g_error_free(gerr);
|
|
return;
|
|
}
|
|
|
|
fd = g_io_channel_unix_get_fd(chan);
|
|
|
|
if (!avdtp_stream_set_transport(setup->stream, fd, imtu, omtu)) {
|
|
error("avdtp_stream_set_transport: failed");
|
|
return;
|
|
}
|
|
|
|
g_io_channel_set_close_on_unref(chan, FALSE);
|
|
|
|
if (dev->io) {
|
|
g_io_channel_unref(dev->io);
|
|
dev->io = NULL;
|
|
}
|
|
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_CONNECTED);
|
|
}
|
|
|
|
static void connect_cb(GIOChannel *chan, GError *err, gpointer user_data)
|
|
{
|
|
struct a2dp_device *dev;
|
|
bdaddr_t dst;
|
|
char address[18];
|
|
GError *gerr = NULL;
|
|
GSList *l;
|
|
|
|
if (err) {
|
|
error("%s", err->message);
|
|
return;
|
|
}
|
|
|
|
bt_io_get(chan, &gerr,
|
|
BT_IO_OPT_DEST_BDADDR, &dst,
|
|
BT_IO_OPT_INVALID);
|
|
if (gerr) {
|
|
error("%s", gerr->message);
|
|
g_error_free(gerr);
|
|
g_io_channel_shutdown(chan, TRUE, NULL);
|
|
return;
|
|
}
|
|
|
|
ba2str(&dst, address);
|
|
DBG("Incoming connection from %s", address);
|
|
|
|
l = g_slist_find_custom(devices, &dst, device_cmp);
|
|
if (l) {
|
|
transport_connect_cb(chan, err, l->data);
|
|
return;
|
|
}
|
|
|
|
dev = a2dp_device_new(&dst);
|
|
bt_a2dp_notify_state(dev, HAL_A2DP_STATE_CONNECTING);
|
|
signaling_connect_cb(chan, err, dev);
|
|
}
|
|
|
|
static sdp_record_t *a2dp_record(void)
|
|
{
|
|
sdp_list_t *svclass_id, *pfseq, *apseq, *root;
|
|
uuid_t root_uuid, l2cap_uuid, avdtp_uuid, a2dp_uuid;
|
|
sdp_profile_desc_t profile[1];
|
|
sdp_list_t *aproto, *proto[2];
|
|
sdp_record_t *record;
|
|
sdp_data_t *psm, *version, *features;
|
|
uint16_t lp = AVDTP_UUID;
|
|
uint16_t a2dp_ver = 0x0103, avdtp_ver = 0x0103, feat = 0x000f;
|
|
|
|
record = sdp_record_alloc();
|
|
if (!record)
|
|
return NULL;
|
|
|
|
sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
|
|
root = sdp_list_append(NULL, &root_uuid);
|
|
sdp_set_browse_groups(record, root);
|
|
|
|
sdp_uuid16_create(&a2dp_uuid, AUDIO_SOURCE_SVCLASS_ID);
|
|
svclass_id = sdp_list_append(NULL, &a2dp_uuid);
|
|
sdp_set_service_classes(record, svclass_id);
|
|
|
|
sdp_uuid16_create(&profile[0].uuid, ADVANCED_AUDIO_PROFILE_ID);
|
|
profile[0].version = a2dp_ver;
|
|
pfseq = sdp_list_append(NULL, &profile[0]);
|
|
sdp_set_profile_descs(record, pfseq);
|
|
|
|
sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
|
|
proto[0] = sdp_list_append(NULL, &l2cap_uuid);
|
|
psm = sdp_data_alloc(SDP_UINT16, &lp);
|
|
proto[0] = sdp_list_append(proto[0], psm);
|
|
apseq = sdp_list_append(NULL, proto[0]);
|
|
|
|
sdp_uuid16_create(&avdtp_uuid, AVDTP_UUID);
|
|
proto[1] = sdp_list_append(NULL, &avdtp_uuid);
|
|
version = sdp_data_alloc(SDP_UINT16, &avdtp_ver);
|
|
proto[1] = sdp_list_append(proto[1], version);
|
|
apseq = sdp_list_append(apseq, proto[1]);
|
|
|
|
aproto = sdp_list_append(NULL, apseq);
|
|
sdp_set_access_protos(record, aproto);
|
|
|
|
features = sdp_data_alloc(SDP_UINT16, &feat);
|
|
sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features);
|
|
|
|
sdp_set_info_attr(record, "Audio Source", NULL, NULL);
|
|
|
|
sdp_data_free(psm);
|
|
sdp_data_free(version);
|
|
sdp_list_free(proto[0], NULL);
|
|
sdp_list_free(proto[1], NULL);
|
|
sdp_list_free(apseq, NULL);
|
|
sdp_list_free(pfseq, NULL);
|
|
sdp_list_free(aproto, NULL);
|
|
sdp_list_free(root, NULL);
|
|
sdp_list_free(svclass_id, NULL);
|
|
|
|
return record;
|
|
}
|
|
|
|
static gboolean sep_getcap_ind(struct avdtp *session,
|
|
struct avdtp_local_sep *sep,
|
|
GSList **caps, uint8_t *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_preset *cap = endpoint->caps;
|
|
struct avdtp_service_capability *service;
|
|
struct avdtp_media_codec_capability *codec;
|
|
|
|
*caps = NULL;
|
|
|
|
service = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, NULL, 0);
|
|
*caps = g_slist_append(*caps, service);
|
|
|
|
codec = g_malloc0(sizeof(*codec) + cap->len);
|
|
codec->media_type = AVDTP_MEDIA_TYPE_AUDIO;
|
|
codec->media_codec_type = endpoint->codec;
|
|
memcpy(codec->data, cap->data, cap->len);
|
|
|
|
service = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, codec,
|
|
sizeof(*codec) + cap->len);
|
|
*caps = g_slist_append(*caps, service);
|
|
g_free(codec);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static int check_config(struct a2dp_endpoint *endpoint,
|
|
struct a2dp_preset *config)
|
|
{
|
|
GSList *l;
|
|
struct a2dp_preset *caps;
|
|
|
|
for (l = endpoint->presets; l; l = g_slist_next(l)) {
|
|
struct a2dp_preset *preset = l->data;
|
|
|
|
if (preset->len != config->len)
|
|
continue;
|
|
|
|
if (memcmp(preset->data, config->data, preset->len) == 0)
|
|
return 0;
|
|
}
|
|
|
|
caps = endpoint->caps;
|
|
|
|
/* Codec specific */
|
|
switch (endpoint->codec) {
|
|
case A2DP_CODEC_SBC:
|
|
return sbc_check_config(caps->data, caps->len, config->data,
|
|
config->len);
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
static struct a2dp_device *find_device_by_session(struct avdtp *session)
|
|
{
|
|
GSList *l;
|
|
|
|
for (l = devices; l; l = g_slist_next(l)) {
|
|
struct a2dp_device *dev = l->data;
|
|
|
|
if (dev->session == session)
|
|
return dev;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct a2dp_setup *find_setup(uint8_t id)
|
|
{
|
|
GSList *l;
|
|
|
|
for (l = setups; l; l = g_slist_next(l)) {
|
|
struct a2dp_setup *setup = l->data;
|
|
|
|
if (setup->endpoint->id == id)
|
|
return setup;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void setup_remove_by_id(uint8_t id)
|
|
{
|
|
struct a2dp_setup *setup;
|
|
|
|
setup = find_setup(id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for endpoint %u", id);
|
|
return;
|
|
}
|
|
|
|
setup_remove(setup);
|
|
}
|
|
|
|
static gboolean sep_setconf_ind(struct avdtp *session,
|
|
struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream,
|
|
GSList *caps,
|
|
avdtp_set_configuration_cb cb,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_device *dev;
|
|
struct a2dp_preset *preset = NULL;
|
|
|
|
DBG("");
|
|
|
|
dev = find_device_by_session(session);
|
|
if (!dev) {
|
|
error("Unable to find device for session %p", session);
|
|
return FALSE;
|
|
}
|
|
|
|
for (; caps != NULL; caps = g_slist_next(caps)) {
|
|
struct avdtp_service_capability *cap = caps->data;
|
|
struct avdtp_media_codec_capability *codec;
|
|
|
|
if (cap->category == AVDTP_DELAY_REPORTING)
|
|
return FALSE;
|
|
|
|
if (cap->category != AVDTP_MEDIA_CODEC)
|
|
continue;
|
|
|
|
codec = (struct avdtp_media_codec_capability *) cap->data;
|
|
|
|
if (codec->media_codec_type != endpoint->codec)
|
|
return FALSE;
|
|
|
|
preset = g_new0(struct a2dp_preset, 1);
|
|
preset->len = cap->length - sizeof(*codec);
|
|
preset->data = g_memdup(codec->data, preset->len);
|
|
|
|
if (check_config(endpoint, preset) < 0) {
|
|
preset_free(preset);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (!preset)
|
|
return FALSE;
|
|
|
|
setup_add(dev, endpoint, preset, stream);
|
|
|
|
cb(session, stream, NULL);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean sep_open_ind(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream, uint8_t *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for endpoint %u",
|
|
endpoint->id);
|
|
*err = AVDTP_SEP_NOT_IN_USE;
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean sep_close_ind(struct avdtp *session,
|
|
struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream,
|
|
uint8_t *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for endpoint %u",
|
|
endpoint->id);
|
|
*err = AVDTP_SEP_NOT_IN_USE;
|
|
return FALSE;
|
|
}
|
|
|
|
bt_audio_notify_state(setup, HAL_AUDIO_STOPPED);
|
|
|
|
setup_remove(setup);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean sep_start_ind(struct avdtp *session,
|
|
struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream,
|
|
uint8_t *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for endpoint %u",
|
|
endpoint->id);
|
|
*err = AVDTP_SEP_NOT_IN_USE;
|
|
return FALSE;
|
|
}
|
|
|
|
bt_audio_notify_state(setup, HAL_AUDIO_STARTED);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean sep_suspend_ind(struct avdtp *session,
|
|
struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream,
|
|
uint8_t *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for endpoint %u",
|
|
endpoint->id);
|
|
*err = AVDTP_SEP_NOT_IN_USE;
|
|
return FALSE;
|
|
}
|
|
|
|
bt_audio_notify_state(setup, HAL_AUDIO_SUSPEND);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static struct avdtp_sep_ind sep_ind = {
|
|
.get_capability = sep_getcap_ind,
|
|
.set_configuration = sep_setconf_ind,
|
|
.open = sep_open_ind,
|
|
.close = sep_close_ind,
|
|
.start = sep_start_ind,
|
|
.suspend = sep_suspend_ind,
|
|
};
|
|
|
|
static void sep_setconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream,
|
|
struct avdtp_error *err, void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
int ret;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for endpoint %u",
|
|
endpoint->id);
|
|
return;
|
|
}
|
|
|
|
if (err)
|
|
goto failed;
|
|
|
|
ret = avdtp_open(session, stream);
|
|
if (ret < 0) {
|
|
error("avdtp_open: %s", strerror(-ret));
|
|
goto failed;
|
|
}
|
|
|
|
return;
|
|
|
|
failed:
|
|
setup_remove(setup);
|
|
}
|
|
|
|
static void sep_open_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream, struct avdtp_error *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_device *dev;
|
|
|
|
DBG("");
|
|
|
|
if (err)
|
|
goto failed;
|
|
|
|
dev = find_device_by_session(session);
|
|
if (!dev) {
|
|
error("Unable to find device for session");
|
|
goto failed;
|
|
}
|
|
|
|
a2dp_device_connect(dev, transport_connect_cb);
|
|
|
|
return;
|
|
|
|
failed:
|
|
setup_remove_by_id(endpoint->id);
|
|
}
|
|
|
|
static void sep_start_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream, struct avdtp_error *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
if (err) {
|
|
setup_remove_by_id(endpoint->id);
|
|
return;
|
|
}
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for %u endpoint",
|
|
endpoint->id);
|
|
return;
|
|
}
|
|
|
|
bt_audio_notify_state(setup, HAL_AUDIO_STARTED);
|
|
}
|
|
|
|
static void sep_suspend_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream, struct avdtp_error *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
if (err) {
|
|
setup_remove_by_id(endpoint->id);
|
|
return;
|
|
}
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for %u endpoint",
|
|
endpoint->id);
|
|
return;
|
|
}
|
|
|
|
bt_audio_notify_state(setup, HAL_AUDIO_STOPPED);
|
|
}
|
|
|
|
static void sep_close_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream, struct avdtp_error *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
struct a2dp_setup *setup;
|
|
|
|
DBG("");
|
|
|
|
if (err)
|
|
return;
|
|
|
|
setup = find_setup(endpoint->id);
|
|
if (!setup) {
|
|
error("Unable to find stream setup for %u endpoint",
|
|
endpoint->id);
|
|
return;
|
|
}
|
|
|
|
bt_audio_notify_state(setup, HAL_AUDIO_STOPPED);
|
|
|
|
setup_remove(setup);
|
|
}
|
|
|
|
static void sep_abort_cfm(struct avdtp *session, struct avdtp_local_sep *sep,
|
|
struct avdtp_stream *stream, struct avdtp_error *err,
|
|
void *user_data)
|
|
{
|
|
struct a2dp_endpoint *endpoint = user_data;
|
|
|
|
DBG("");
|
|
|
|
if (err)
|
|
return;
|
|
|
|
setup_remove_by_id(endpoint->id);
|
|
}
|
|
|
|
static struct avdtp_sep_cfm sep_cfm = {
|
|
.set_configuration = sep_setconf_cfm,
|
|
.open = sep_open_cfm,
|
|
.start = sep_start_cfm,
|
|
.suspend = sep_suspend_cfm,
|
|
.close = sep_close_cfm,
|
|
.abort = sep_abort_cfm,
|
|
};
|
|
|
|
static uint8_t register_endpoint(const uint8_t *uuid, uint8_t codec,
|
|
GSList *presets)
|
|
{
|
|
struct a2dp_endpoint *endpoint;
|
|
|
|
/* FIXME: Add proper check for uuid */
|
|
|
|
endpoint = g_new0(struct a2dp_endpoint, 1);
|
|
endpoint->id = g_slist_length(endpoints) + 1;
|
|
endpoint->codec = codec;
|
|
endpoint->sep = avdtp_register_sep(AVDTP_SEP_TYPE_SOURCE,
|
|
AVDTP_MEDIA_TYPE_AUDIO,
|
|
codec, FALSE, &sep_ind,
|
|
&sep_cfm, endpoint);
|
|
endpoint->caps = presets->data;
|
|
endpoint->presets = g_slist_copy(g_slist_nth(presets, 1));
|
|
|
|
if (endpoint->codec == A2DP_CODEC_VENDOR) {
|
|
a2dp_vendor_codec_t *vndcodec = (void *) endpoint->caps->data;
|
|
|
|
avdtp_sep_set_vendor_codec(endpoint->sep,
|
|
btohl(vndcodec->vendor_id),
|
|
btohs(vndcodec->codec_id));
|
|
}
|
|
|
|
endpoints = g_slist_append(endpoints, endpoint);
|
|
|
|
return endpoint->id;
|
|
}
|
|
|
|
static GSList *parse_presets(const struct audio_preset *p, uint8_t count,
|
|
uint16_t len)
|
|
{
|
|
GSList *l = NULL;
|
|
uint8_t i;
|
|
|
|
for (i = 0; count > i; i++) {
|
|
const uint8_t *ptr = (const uint8_t *) p;
|
|
struct a2dp_preset *preset;
|
|
|
|
if (len < sizeof(struct audio_preset)) {
|
|
DBG("Invalid preset index %u", i);
|
|
g_slist_free_full(l, preset_free);
|
|
return NULL;
|
|
}
|
|
|
|
len -= sizeof(struct audio_preset);
|
|
if (len == 0 || len < p->len) {
|
|
DBG("Invalid preset size of %u for index %u", len, i);
|
|
g_slist_free_full(l, preset_free);
|
|
return NULL;
|
|
}
|
|
|
|
preset = g_new0(struct a2dp_preset, 1);
|
|
preset->len = p->len;
|
|
preset->data = g_memdup(p->data, preset->len);
|
|
l = g_slist_append(l, preset);
|
|
|
|
len -= preset->len;
|
|
ptr += sizeof(*p) + preset->len;
|
|
p = (const struct audio_preset *) ptr;
|
|
}
|
|
|
|
return l;
|
|
}
|
|
|
|
static void bt_audio_open(const void *buf, uint16_t len)
|
|
{
|
|
const struct audio_cmd_open *cmd = buf;
|
|
struct audio_rsp_open rsp;
|
|
GSList *presets;
|
|
|
|
DBG("");
|
|
|
|
audio_retrying = false;
|
|
|
|
if (cmd->presets == 0) {
|
|
error("No audio presets found");
|
|
goto failed;
|
|
}
|
|
|
|
presets = parse_presets(cmd->preset, cmd->presets, len - sizeof(*cmd));
|
|
if (!presets) {
|
|
error("No audio presets found");
|
|
goto failed;
|
|
}
|
|
|
|
rsp.id = register_endpoint(cmd->uuid, cmd->codec, presets);
|
|
if (rsp.id == 0) {
|
|
g_slist_free_full(presets, preset_free);
|
|
error("Unable to register endpoint");
|
|
goto failed;
|
|
}
|
|
|
|
g_slist_free(presets);
|
|
|
|
ipc_send_rsp_full(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_OPEN,
|
|
sizeof(rsp), &rsp, -1);
|
|
|
|
return;
|
|
|
|
failed:
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_OPEN,
|
|
AUDIO_STATUS_FAILED);
|
|
}
|
|
|
|
static struct a2dp_endpoint *find_endpoint(uint8_t id)
|
|
{
|
|
GSList *l;
|
|
|
|
for (l = endpoints; l; l = g_slist_next(l)) {
|
|
struct a2dp_endpoint *endpoint = l->data;
|
|
|
|
if (endpoint->id == id)
|
|
return endpoint;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void bt_audio_close(const void *buf, uint16_t len)
|
|
{
|
|
const struct audio_cmd_close *cmd = buf;
|
|
struct a2dp_endpoint *endpoint;
|
|
|
|
DBG("");
|
|
|
|
endpoint = find_endpoint(cmd->id);
|
|
if (!endpoint) {
|
|
error("Unable to find endpoint %u", cmd->id);
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_CLOSE,
|
|
AUDIO_STATUS_FAILED);
|
|
return;
|
|
}
|
|
|
|
endpoints = g_slist_remove(endpoints, endpoint);
|
|
unregister_endpoint(endpoint);
|
|
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_CLOSE,
|
|
AUDIO_STATUS_SUCCESS);
|
|
}
|
|
|
|
static void bt_stream_open(const void *buf, uint16_t len)
|
|
{
|
|
const struct audio_cmd_open_stream *cmd = buf;
|
|
struct audio_rsp_open_stream *rsp;
|
|
struct a2dp_setup *setup;
|
|
int fd;
|
|
uint16_t omtu;
|
|
|
|
DBG("");
|
|
|
|
if (cmd->id)
|
|
setup = find_setup(cmd->id);
|
|
else
|
|
setup = setups ? setups->data : NULL;
|
|
if (!setup) {
|
|
error("Unable to find stream for endpoint %u", cmd->id);
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_OPEN_STREAM,
|
|
AUDIO_STATUS_FAILED);
|
|
return;
|
|
}
|
|
|
|
if (!avdtp_stream_get_transport(setup->stream, &fd, NULL, &omtu,
|
|
NULL)) {
|
|
error("avdtp_stream_get_transport: failed");
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_OPEN_STREAM,
|
|
AUDIO_STATUS_FAILED);
|
|
return;
|
|
}
|
|
|
|
len = sizeof(struct audio_rsp_open_stream) +
|
|
sizeof(struct audio_preset) + setup->preset->len;
|
|
rsp = g_malloc0(len);
|
|
rsp->id = setup->endpoint->id;
|
|
rsp->mtu = omtu;
|
|
rsp->preset->len = setup->preset->len;
|
|
memcpy(rsp->preset->data, setup->preset->data, setup->preset->len);
|
|
|
|
ipc_send_rsp_full(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_OPEN_STREAM,
|
|
len, rsp, fd);
|
|
|
|
g_free(rsp);
|
|
}
|
|
|
|
static void bt_stream_close(const void *buf, uint16_t len)
|
|
{
|
|
const struct audio_cmd_close_stream *cmd = buf;
|
|
struct a2dp_setup *setup;
|
|
int err;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(cmd->id);
|
|
if (!setup) {
|
|
error("Unable to find stream for endpoint %u", cmd->id);
|
|
goto failed;
|
|
}
|
|
|
|
err = avdtp_close(setup->dev->session, setup->stream, FALSE);
|
|
if (err < 0) {
|
|
error("avdtp_close: %s", strerror(-err));
|
|
goto failed;
|
|
}
|
|
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_CLOSE_STREAM,
|
|
AUDIO_STATUS_SUCCESS);
|
|
|
|
return;
|
|
|
|
failed:
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_CLOSE_STREAM,
|
|
AUDIO_STATUS_FAILED);
|
|
}
|
|
|
|
static void bt_stream_resume(const void *buf, uint16_t len)
|
|
{
|
|
const struct audio_cmd_resume_stream *cmd = buf;
|
|
struct a2dp_setup *setup;
|
|
int err;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(cmd->id);
|
|
if (!setup) {
|
|
error("Unable to find stream for endpoint %u", cmd->id);
|
|
goto failed;
|
|
}
|
|
|
|
if (setup->state != HAL_AUDIO_STARTED) {
|
|
err = avdtp_start(setup->dev->session, setup->stream);
|
|
if (err < 0) {
|
|
error("avdtp_start: %s", strerror(-err));
|
|
goto failed;
|
|
}
|
|
}
|
|
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_RESUME_STREAM,
|
|
AUDIO_STATUS_SUCCESS);
|
|
|
|
return;
|
|
|
|
failed:
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_RESUME_STREAM,
|
|
AUDIO_STATUS_FAILED);
|
|
}
|
|
|
|
static void bt_stream_suspend(const void *buf, uint16_t len)
|
|
{
|
|
const struct audio_cmd_suspend_stream *cmd = buf;
|
|
struct a2dp_setup *setup;
|
|
int err;
|
|
|
|
DBG("");
|
|
|
|
setup = find_setup(cmd->id);
|
|
if (!setup) {
|
|
error("Unable to find stream for endpoint %u", cmd->id);
|
|
goto failed;
|
|
}
|
|
|
|
err = avdtp_suspend(setup->dev->session, setup->stream);
|
|
if (err < 0) {
|
|
error("avdtp_suspend: %s", strerror(-err));
|
|
goto failed;
|
|
}
|
|
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_SUSPEND_STREAM,
|
|
AUDIO_STATUS_SUCCESS);
|
|
|
|
return;
|
|
|
|
failed:
|
|
ipc_send_rsp(audio_ipc, AUDIO_SERVICE_ID, AUDIO_OP_SUSPEND_STREAM,
|
|
AUDIO_STATUS_FAILED);
|
|
}
|
|
|
|
static const struct ipc_handler audio_handlers[] = {
|
|
/* AUDIO_OP_OPEN */
|
|
{ bt_audio_open, true, sizeof(struct audio_cmd_open) },
|
|
/* AUDIO_OP_CLOSE */
|
|
{ bt_audio_close, false, sizeof(struct audio_cmd_close) },
|
|
/* AUDIO_OP_OPEN_STREAM */
|
|
{ bt_stream_open, false, sizeof(struct audio_cmd_open_stream) },
|
|
/* AUDIO_OP_CLOSE_STREAM */
|
|
{ bt_stream_close, false, sizeof(struct audio_cmd_close_stream) },
|
|
/* AUDIO_OP_RESUME_STREAM */
|
|
{ bt_stream_resume, false, sizeof(struct audio_cmd_resume_stream) },
|
|
/* AUDIO_OP_SUSPEND_STREAM */
|
|
{ bt_stream_suspend, false, sizeof(struct audio_cmd_suspend_stream) },
|
|
};
|
|
|
|
static void bt_audio_unregister(void)
|
|
{
|
|
DBG("");
|
|
|
|
if (audio_retry_id > 0)
|
|
g_source_remove(audio_retry_id);
|
|
|
|
g_slist_free_full(endpoints, unregister_endpoint);
|
|
endpoints = NULL;
|
|
|
|
g_slist_free_full(setups, setup_free);
|
|
setups = NULL;
|
|
|
|
ipc_cleanup(audio_ipc);
|
|
audio_ipc = NULL;
|
|
}
|
|
|
|
static bool bt_audio_register(ipc_disconnect_cb disconnect)
|
|
{
|
|
DBG("");
|
|
|
|
audio_ipc = ipc_init(BLUEZ_AUDIO_SK_PATH, sizeof(BLUEZ_AUDIO_SK_PATH),
|
|
AUDIO_SERVICE_ID_MAX, false, disconnect, NULL);
|
|
if (!audio_ipc)
|
|
return false;
|
|
|
|
ipc_register(audio_ipc, AUDIO_SERVICE_ID, audio_handlers,
|
|
G_N_ELEMENTS(audio_handlers));
|
|
|
|
return true;
|
|
}
|
|
|
|
static gboolean audio_retry_register(void *data)
|
|
{
|
|
ipc_disconnect_cb cb = data;
|
|
|
|
audio_retry_id = 0;
|
|
audio_retrying = true;
|
|
|
|
bt_audio_register(cb);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void audio_disconnected(void *data)
|
|
{
|
|
GSList *l;
|
|
bool restart;
|
|
|
|
DBG("");
|
|
|
|
if (audio_retrying)
|
|
goto retry;
|
|
|
|
restart = endpoints != NULL ? true : false;
|
|
|
|
bt_audio_unregister();
|
|
|
|
for (l = devices; l; l = g_slist_next(l)) {
|
|
struct a2dp_device *dev = l->data;
|
|
|
|
avdtp_shutdown(dev->session);
|
|
}
|
|
|
|
if (!restart)
|
|
return;
|
|
|
|
retry:
|
|
audio_retry_id = g_timeout_add_seconds(AUDIO_RETRY_TIMEOUT,
|
|
audio_retry_register,
|
|
audio_disconnected);
|
|
}
|
|
|
|
bool bt_a2dp_register(struct ipc *ipc, const bdaddr_t *addr, uint8_t mode)
|
|
{
|
|
GError *err = NULL;
|
|
sdp_record_t *rec;
|
|
|
|
DBG("");
|
|
|
|
bacpy(&adapter_addr, addr);
|
|
|
|
server = bt_io_listen(connect_cb, NULL, NULL, NULL, &err,
|
|
BT_IO_OPT_SOURCE_BDADDR, &adapter_addr,
|
|
BT_IO_OPT_PSM, L2CAP_PSM_AVDTP,
|
|
BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM,
|
|
BT_IO_OPT_MASTER, true,
|
|
BT_IO_OPT_INVALID);
|
|
if (!server) {
|
|
error("Failed to listen on AVDTP channel: %s", err->message);
|
|
g_error_free(err);
|
|
return false;
|
|
}
|
|
|
|
rec = a2dp_record();
|
|
if (!rec) {
|
|
error("Failed to allocate A2DP record");
|
|
goto fail;
|
|
}
|
|
|
|
if (bt_adapter_add_record(rec, SVC_HINT_CAPTURING) < 0) {
|
|
error("Failed to register A2DP record");
|
|
sdp_record_free(rec);
|
|
goto fail;
|
|
}
|
|
record_id = rec->handle;
|
|
|
|
hal_ipc = ipc;
|
|
|
|
ipc_register(hal_ipc, HAL_SERVICE_ID_A2DP, cmd_handlers,
|
|
G_N_ELEMENTS(cmd_handlers));
|
|
|
|
if (bt_audio_register(audio_disconnected))
|
|
return true;
|
|
|
|
fail:
|
|
g_io_channel_shutdown(server, TRUE, NULL);
|
|
g_io_channel_unref(server);
|
|
server = NULL;
|
|
return false;
|
|
}
|
|
|
|
void bt_a2dp_unregister(void)
|
|
{
|
|
DBG("");
|
|
|
|
g_slist_free_full(setups, setup_free);
|
|
setups = NULL;
|
|
|
|
g_slist_free_full(endpoints, unregister_endpoint);
|
|
endpoints = NULL;
|
|
|
|
g_slist_free_full(devices, a2dp_device_free);
|
|
devices = NULL;
|
|
|
|
ipc_unregister(hal_ipc, HAL_SERVICE_ID_A2DP);
|
|
hal_ipc = NULL;
|
|
|
|
bt_adapter_remove_record(record_id);
|
|
record_id = 0;
|
|
|
|
if (server) {
|
|
g_io_channel_shutdown(server, TRUE, NULL);
|
|
g_io_channel_unref(server);
|
|
server = NULL;
|
|
}
|
|
|
|
if (audio_ipc) {
|
|
ipc_unregister(audio_ipc, AUDIO_SERVICE_ID);
|
|
ipc_cleanup(audio_ipc);
|
|
audio_ipc = NULL;
|
|
}
|
|
}
|