diff options
Diffstat (limited to 'src/modules/bluetooth')
-rw-r--r-- | src/modules/bluetooth/a2dp-codec-api.h | 98 | ||||
-rw-r--r-- | src/modules/bluetooth/a2dp-codec-sbc.c | 682 | ||||
-rw-r--r-- | src/modules/bluetooth/a2dp-codec-util.c | 56 | ||||
-rw-r--r-- | src/modules/bluetooth/a2dp-codec-util.h | 34 | ||||
-rw-r--r-- | src/modules/bluetooth/a2dp-codecs.h | 435 | ||||
-rw-r--r-- | src/modules/bluetooth/backend-native.c | 714 | ||||
-rw-r--r-- | src/modules/bluetooth/backend-ofono.c | 764 | ||||
-rw-r--r-- | src/modules/bluetooth/bluez5-util.c | 1744 | ||||
-rw-r--r-- | src/modules/bluetooth/bluez5-util.h | 185 | ||||
-rw-r--r-- | src/modules/bluetooth/meson.build | 33 | ||||
-rw-r--r-- | src/modules/bluetooth/module-bluetooth-discover.c | 76 | ||||
-rw-r--r-- | src/modules/bluetooth/module-bluetooth-policy.c | 517 | ||||
-rw-r--r-- | src/modules/bluetooth/module-bluez5-device.c | 2417 | ||||
-rw-r--r-- | src/modules/bluetooth/module-bluez5-discover.c | 173 | ||||
-rw-r--r-- | src/modules/bluetooth/rtp.h | 80 |
15 files changed, 8008 insertions, 0 deletions
diff --git a/src/modules/bluetooth/a2dp-codec-api.h b/src/modules/bluetooth/a2dp-codec-api.h new file mode 100644 index 0000000..a3123f4 --- /dev/null +++ b/src/modules/bluetooth/a2dp-codec-api.h @@ -0,0 +1,98 @@ +#ifndef fooa2dpcodechfoo +#define fooa2dpcodechfoo + +/*** + This file is part of PulseAudio. + + Copyright 2018-2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#include <pulsecore/core.h> + +#define MAX_A2DP_CAPS_SIZE 254 + +typedef struct pa_a2dp_codec_capabilities { + uint8_t size; + uint8_t buffer[]; /* max size is 254 bytes */ +} pa_a2dp_codec_capabilities; + +typedef struct pa_a2dp_codec_id { + uint8_t codec_id; + uint32_t vendor_id; + uint16_t vendor_codec_id; +} pa_a2dp_codec_id; + +typedef struct pa_a2dp_codec { + /* Unique name of the codec, lowercase and without whitespaces, used for + * constructing identifier, D-Bus paths, ... */ + const char *name; + /* Human readable codec description */ + const char *description; + + /* A2DP codec id */ + pa_a2dp_codec_id id; + + /* True if codec is bi-directional and supports backchannel */ + bool support_backchannel; + + /* Returns true if codec accepts capabilities, for_encoding is true when + * capabilities are used for encoding */ + bool (*can_accept_capabilities)(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding); + /* Choose remote endpoint based on capabilities from hash map + * (const char *endpoint -> const pa_a2dp_codec_capabilities *capability) + * and returns corresponding endpoint key (or NULL when there is no valid), + * for_encoder is true when capabilities hash map is used for encoding */ + const char *(*choose_remote_endpoint)(const pa_hashmap *capabilities_hashmap, const pa_sample_spec *default_sample_spec, bool for_encoding); + /* Fill codec capabilities, returns size of filled buffer */ + uint8_t (*fill_capabilities)(uint8_t capabilities_buffer[MAX_A2DP_CAPS_SIZE]); + /* Validate codec configuration, returns true on success */ + bool (*is_configuration_valid)(const uint8_t *config_buffer, uint8_t config_size); + /* Fill preferred codec configuration, returns size of filled buffer or 0 on failure */ + uint8_t (*fill_preferred_configuration)(const pa_sample_spec *default_sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[MAX_A2DP_CAPS_SIZE]); + + /* Initialize codec, returns codec info data and set sample_spec, + * for_encoding is true when codec_info is used for encoding, + * for_backchannel is true when codec_info is used for backchannel */ + void *(*init)(bool for_encoding, bool for_backchannel, const uint8_t *config_buffer, uint8_t config_size, pa_sample_spec *sample_spec); + /* Deinitialize and release codec info data in codec_info */ + void (*deinit)(void *codec_info); + /* Reset internal state of codec info data in codec_info, returns + * a negative value on failure */ + int (*reset)(void *codec_info); + + /* Get read block size for codec, it is minimal size of buffer + * needed to decode read_link_mtu bytes of encoded data */ + size_t (*get_read_block_size)(void *codec_info, size_t read_link_mtu); + /* Get write block size for codec, it is maximal size of buffer + * which can produce at most write_link_mtu bytes of encoded data */ + size_t (*get_write_block_size)(void *codec_info, size_t write_link_mtu); + + /* Reduce encoder bitrate for codec, returns new write block size or zero + * if not changed, called when socket is not accepting encoded data fast + * enough */ + size_t (*reduce_encoder_bitrate)(void *codec_info, size_t write_link_mtu); + + /* Encode input_buffer of input_size to output_buffer of output_size, + * returns size of filled ouput_buffer and set processed to size of + * processed input_buffer */ + size_t (*encode_buffer)(void *codec_info, uint32_t timestamp, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed); + /* Decode input_buffer of input_size to output_buffer of output_size, + * returns size of filled ouput_buffer and set processed to size of + * processed input_buffer */ + size_t (*decode_buffer)(void *codec_info, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed); +} pa_a2dp_codec; + +#endif diff --git a/src/modules/bluetooth/a2dp-codec-sbc.c b/src/modules/bluetooth/a2dp-codec-sbc.c new file mode 100644 index 0000000..89c647f --- /dev/null +++ b/src/modules/bluetooth/a2dp-codec-sbc.c @@ -0,0 +1,682 @@ +/*** + This file is part of PulseAudio. + + Copyright 2018-2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulsecore/core-util.h> +#include <pulsecore/log.h> +#include <pulsecore/macro.h> +#include <pulsecore/once.h> +#include <pulse/sample.h> +#include <pulse/xmalloc.h> + +#include <arpa/inet.h> + +#include <sbc/sbc.h> + +#include "a2dp-codecs.h" +#include "a2dp-codec-api.h" +#include "rtp.h" + +#define SBC_BITPOOL_DEC_LIMIT 32 +#define SBC_BITPOOL_DEC_STEP 5 + +struct sbc_info { + sbc_t sbc; /* Codec data */ + size_t codesize, frame_length; /* SBC Codesize, frame_length. We simply cache those values here */ + uint16_t seq_num; /* Cumulative packet sequence */ + uint8_t frequency; + uint8_t blocks; + uint8_t subbands; + uint8_t mode; + uint8_t allocation; + uint8_t initial_bitpool; + uint8_t min_bitpool; + uint8_t max_bitpool; +}; + +static bool can_accept_capabilities(const uint8_t *capabilities_buffer, uint8_t capabilities_size, bool for_encoding) { + const a2dp_sbc_t *capabilities = (const a2dp_sbc_t *) capabilities_buffer; + + if (capabilities_size != sizeof(*capabilities)) + return false; + + if (!(capabilities->frequency & (SBC_SAMPLING_FREQ_16000 | SBC_SAMPLING_FREQ_32000 | SBC_SAMPLING_FREQ_44100 | SBC_SAMPLING_FREQ_48000))) + return false; + + if (!(capabilities->channel_mode & (SBC_CHANNEL_MODE_MONO | SBC_CHANNEL_MODE_DUAL_CHANNEL | SBC_CHANNEL_MODE_STEREO | SBC_CHANNEL_MODE_JOINT_STEREO))) + return false; + + if (!(capabilities->allocation_method & (SBC_ALLOCATION_SNR | SBC_ALLOCATION_LOUDNESS))) + return false; + + if (!(capabilities->subbands & (SBC_SUBBANDS_4 | SBC_SUBBANDS_8))) + return false; + + if (!(capabilities->block_length & (SBC_BLOCK_LENGTH_4 | SBC_BLOCK_LENGTH_8 | SBC_BLOCK_LENGTH_12 | SBC_BLOCK_LENGTH_16))) + return false; + + return true; +} + +static const char *choose_remote_endpoint(const pa_hashmap *capabilities_hashmap, const pa_sample_spec *default_sample_spec, bool for_encoding) { + const pa_a2dp_codec_capabilities *a2dp_capabilities; + const char *key; + void *state; + + /* There is no preference, just choose random valid entry */ + PA_HASHMAP_FOREACH_KV(key, a2dp_capabilities, capabilities_hashmap, state) { + if (can_accept_capabilities(a2dp_capabilities->buffer, a2dp_capabilities->size, for_encoding)) + return key; + } + + return NULL; +} + +static uint8_t fill_capabilities(uint8_t capabilities_buffer[MAX_A2DP_CAPS_SIZE]) { + a2dp_sbc_t *capabilities = (a2dp_sbc_t *) capabilities_buffer; + + pa_zero(*capabilities); + + capabilities->channel_mode = SBC_CHANNEL_MODE_MONO | SBC_CHANNEL_MODE_DUAL_CHANNEL | SBC_CHANNEL_MODE_STEREO | + SBC_CHANNEL_MODE_JOINT_STEREO; + capabilities->frequency = SBC_SAMPLING_FREQ_16000 | SBC_SAMPLING_FREQ_32000 | SBC_SAMPLING_FREQ_44100 | + SBC_SAMPLING_FREQ_48000; + capabilities->allocation_method = SBC_ALLOCATION_SNR | SBC_ALLOCATION_LOUDNESS; + capabilities->subbands = SBC_SUBBANDS_4 | SBC_SUBBANDS_8; + capabilities->block_length = SBC_BLOCK_LENGTH_4 | SBC_BLOCK_LENGTH_8 | SBC_BLOCK_LENGTH_12 | SBC_BLOCK_LENGTH_16; + capabilities->min_bitpool = SBC_MIN_BITPOOL; + capabilities->max_bitpool = SBC_BITPOOL_HQ_JOINT_STEREO_44100; + + return sizeof(*capabilities); +} + +static bool is_configuration_valid(const uint8_t *config_buffer, uint8_t config_size) { + const a2dp_sbc_t *config = (const a2dp_sbc_t *) config_buffer; + + if (config_size != sizeof(*config)) { + pa_log_error("Invalid size of config buffer"); + return false; + } + + if (config->frequency != SBC_SAMPLING_FREQ_16000 && config->frequency != SBC_SAMPLING_FREQ_32000 && + config->frequency != SBC_SAMPLING_FREQ_44100 && config->frequency != SBC_SAMPLING_FREQ_48000) { + pa_log_error("Invalid sampling frequency in configuration"); + return false; + } + + if (config->channel_mode != SBC_CHANNEL_MODE_MONO && config->channel_mode != SBC_CHANNEL_MODE_DUAL_CHANNEL && + config->channel_mode != SBC_CHANNEL_MODE_STEREO && config->channel_mode != SBC_CHANNEL_MODE_JOINT_STEREO) { + pa_log_error("Invalid channel mode in configuration"); + return false; + } + + if (config->allocation_method != SBC_ALLOCATION_SNR && config->allocation_method != SBC_ALLOCATION_LOUDNESS) { + pa_log_error("Invalid allocation method in configuration"); + return false; + } + + if (config->subbands != SBC_SUBBANDS_4 && config->subbands != SBC_SUBBANDS_8) { + pa_log_error("Invalid SBC subbands in configuration"); + return false; + } + + if (config->block_length != SBC_BLOCK_LENGTH_4 && config->block_length != SBC_BLOCK_LENGTH_8 && + config->block_length != SBC_BLOCK_LENGTH_12 && config->block_length != SBC_BLOCK_LENGTH_16) { + pa_log_error("Invalid block length in configuration"); + return false; + } + + if (config->min_bitpool > config->max_bitpool) { + pa_log_error("Invalid bitpool in configuration"); + return false; + } + + return true; +} + +static uint8_t default_bitpool(uint8_t freq, uint8_t mode) { + /* These bitpool values were chosen based on the A2DP spec recommendation */ + switch (freq) { + case SBC_SAMPLING_FREQ_16000: + case SBC_SAMPLING_FREQ_32000: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return SBC_BITPOOL_HQ_JOINT_STEREO_44100; + } + break; + + case SBC_SAMPLING_FREQ_44100: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return SBC_BITPOOL_HQ_MONO_44100; + + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return SBC_BITPOOL_HQ_JOINT_STEREO_44100; + } + break; + + case SBC_SAMPLING_FREQ_48000: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return SBC_BITPOOL_HQ_MONO_48000; + + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return SBC_BITPOOL_HQ_JOINT_STEREO_48000; + } + break; + } + + pa_assert_not_reached(); +} + +static uint8_t fill_preferred_configuration(const pa_sample_spec *default_sample_spec, const uint8_t *capabilities_buffer, uint8_t capabilities_size, uint8_t config_buffer[MAX_A2DP_CAPS_SIZE]) { + a2dp_sbc_t *config = (a2dp_sbc_t *) config_buffer; + const a2dp_sbc_t *capabilities = (const a2dp_sbc_t *) capabilities_buffer; + int i; + + static const struct { + uint32_t rate; + uint8_t cap; + } freq_table[] = { + { 16000U, SBC_SAMPLING_FREQ_16000 }, + { 32000U, SBC_SAMPLING_FREQ_32000 }, + { 44100U, SBC_SAMPLING_FREQ_44100 }, + { 48000U, SBC_SAMPLING_FREQ_48000 } + }; + + if (capabilities_size != sizeof(*capabilities)) { + pa_log_error("Invalid size of capabilities buffer"); + return 0; + } + + pa_zero(*config); + + /* Find the lowest freq that is at least as high as the requested sampling rate */ + for (i = 0; (unsigned) i < PA_ELEMENTSOF(freq_table); i++) + if (freq_table[i].rate >= default_sample_spec->rate && (capabilities->frequency & freq_table[i].cap)) { + config->frequency = freq_table[i].cap; + break; + } + + if ((unsigned) i == PA_ELEMENTSOF(freq_table)) { + for (--i; i >= 0; i--) { + if (capabilities->frequency & freq_table[i].cap) { + config->frequency = freq_table[i].cap; + break; + } + } + + if (i < 0) { + pa_log_error("Not suitable sample rate"); + return 0; + } + } + + pa_assert((unsigned) i < PA_ELEMENTSOF(freq_table)); + + if (default_sample_spec->channels <= 1) { + if (capabilities->channel_mode & SBC_CHANNEL_MODE_MONO) + config->channel_mode = SBC_CHANNEL_MODE_MONO; + else if (capabilities->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO) + config->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO; + else if (capabilities->channel_mode & SBC_CHANNEL_MODE_STEREO) + config->channel_mode = SBC_CHANNEL_MODE_STEREO; + else if (capabilities->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL) + config->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL; + else { + pa_log_error("No supported channel modes"); + return 0; + } + } else { + if (capabilities->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO) + config->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO; + else if (capabilities->channel_mode & SBC_CHANNEL_MODE_STEREO) + config->channel_mode = SBC_CHANNEL_MODE_STEREO; + else if (capabilities->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL) + config->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL; + else if (capabilities->channel_mode & SBC_CHANNEL_MODE_MONO) + config->channel_mode = SBC_CHANNEL_MODE_MONO; + else { + pa_log_error("No supported channel modes"); + return 0; + } + } + + if (capabilities->block_length & SBC_BLOCK_LENGTH_16) + config->block_length = SBC_BLOCK_LENGTH_16; + else if (capabilities->block_length & SBC_BLOCK_LENGTH_12) + config->block_length = SBC_BLOCK_LENGTH_12; + else if (capabilities->block_length & SBC_BLOCK_LENGTH_8) + config->block_length = SBC_BLOCK_LENGTH_8; + else if (capabilities->block_length & SBC_BLOCK_LENGTH_4) + config->block_length = SBC_BLOCK_LENGTH_4; + else { + pa_log_error("No supported block lengths"); + return 0; + } + + if (capabilities->subbands & SBC_SUBBANDS_8) + config->subbands = SBC_SUBBANDS_8; + else if (capabilities->subbands & SBC_SUBBANDS_4) + config->subbands = SBC_SUBBANDS_4; + else { + pa_log_error("No supported subbands"); + return 0; + } + + if (capabilities->allocation_method & SBC_ALLOCATION_LOUDNESS) + config->allocation_method = SBC_ALLOCATION_LOUDNESS; + else if (capabilities->allocation_method & SBC_ALLOCATION_SNR) + config->allocation_method = SBC_ALLOCATION_SNR; + else { + pa_log_error("No supported allocation method"); + return 0; + } + + config->min_bitpool = (uint8_t) PA_MAX(SBC_MIN_BITPOOL, capabilities->min_bitpool); + config->max_bitpool = (uint8_t) PA_MIN(default_bitpool(config->frequency, config->channel_mode), capabilities->max_bitpool); + + if (config->min_bitpool > config->max_bitpool) { + pa_log_error("No supported bitpool"); + return 0; + } + + return sizeof(*config); +} + +static void set_params(struct sbc_info *sbc_info) { + sbc_info->sbc.frequency = sbc_info->frequency; + sbc_info->sbc.blocks = sbc_info->blocks; + sbc_info->sbc.subbands = sbc_info->subbands; + sbc_info->sbc.mode = sbc_info->mode; + sbc_info->sbc.allocation = sbc_info->allocation; + sbc_info->sbc.bitpool = sbc_info->initial_bitpool; + sbc_info->sbc.endian = SBC_LE; + + sbc_info->codesize = sbc_get_codesize(&sbc_info->sbc); + sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc); +} + +static void *init(bool for_encoding, bool for_backchannel, const uint8_t *config_buffer, uint8_t config_size, pa_sample_spec *sample_spec) { + struct sbc_info *sbc_info; + const a2dp_sbc_t *config = (const a2dp_sbc_t *) config_buffer; + int ret; + + pa_assert(config_size == sizeof(*config)); + pa_assert(!for_backchannel); + + sbc_info = pa_xnew0(struct sbc_info, 1); + + ret = sbc_init(&sbc_info->sbc, 0); + if (ret != 0) { + pa_xfree(sbc_info); + pa_log_error("SBC initialization failed: %d", ret); + return NULL; + } + + sample_spec->format = PA_SAMPLE_S16LE; + + switch (config->frequency) { + case SBC_SAMPLING_FREQ_16000: + sbc_info->frequency = SBC_FREQ_16000; + sample_spec->rate = 16000U; + break; + case SBC_SAMPLING_FREQ_32000: + sbc_info->frequency = SBC_FREQ_32000; + sample_spec->rate = 32000U; + break; + case SBC_SAMPLING_FREQ_44100: + sbc_info->frequency = SBC_FREQ_44100; + sample_spec->rate = 44100U; + break; + case SBC_SAMPLING_FREQ_48000: + sbc_info->frequency = SBC_FREQ_48000; + sample_spec->rate = 48000U; + break; + default: + pa_assert_not_reached(); + } + + switch (config->channel_mode) { + case SBC_CHANNEL_MODE_MONO: + sbc_info->mode = SBC_MODE_MONO; + sample_spec->channels = 1; + break; + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + sbc_info->mode = SBC_MODE_DUAL_CHANNEL; + sample_spec->channels = 2; + break; + case SBC_CHANNEL_MODE_STEREO: + sbc_info->mode = SBC_MODE_STEREO; + sample_spec->channels = 2; + break; + case SBC_CHANNEL_MODE_JOINT_STEREO: + sbc_info->mode = SBC_MODE_JOINT_STEREO; + sample_spec->channels = 2; + break; + default: + pa_assert_not_reached(); + } + + switch (config->allocation_method) { + case SBC_ALLOCATION_SNR: + sbc_info->allocation = SBC_AM_SNR; + break; + case SBC_ALLOCATION_LOUDNESS: + sbc_info->allocation = SBC_AM_LOUDNESS; + break; + default: + pa_assert_not_reached(); + } + + switch (config->subbands) { + case SBC_SUBBANDS_4: + sbc_info->subbands = SBC_SB_4; + break; + case SBC_SUBBANDS_8: + sbc_info->subbands = SBC_SB_8; + break; + default: + pa_assert_not_reached(); + } + + switch (config->block_length) { + case SBC_BLOCK_LENGTH_4: + sbc_info->blocks = SBC_BLK_4; + break; + case SBC_BLOCK_LENGTH_8: + sbc_info->blocks = SBC_BLK_8; + break; + case SBC_BLOCK_LENGTH_12: + sbc_info->blocks = SBC_BLK_12; + break; + case SBC_BLOCK_LENGTH_16: + sbc_info->blocks = SBC_BLK_16; + break; + default: + pa_assert_not_reached(); + } + + sbc_info->min_bitpool = config->min_bitpool; + sbc_info->max_bitpool = config->max_bitpool; + + /* Set minimum bitpool for source to get the maximum possible block_size + * in get_block_size() function. This block_size is length of buffer used + * for decoded audio data and so is inversely proportional to frame length + * which depends on bitpool value. Bitpool is controlled by other side from + * range [min_bitpool, max_bitpool]. */ + sbc_info->initial_bitpool = for_encoding ? sbc_info->max_bitpool : sbc_info->min_bitpool; + + set_params(sbc_info); + + pa_log_info("SBC parameters: allocation=%s, subbands=%u, blocks=%u, mode=%s bitpool=%u codesize=%u frame_length=%u", + sbc_info->sbc.allocation ? "SNR" : "Loudness", sbc_info->sbc.subbands ? 8 : 4, + (sbc_info->sbc.blocks+1)*4, sbc_info->sbc.mode == SBC_MODE_MONO ? "Mono" : + sbc_info->sbc.mode == SBC_MODE_DUAL_CHANNEL ? "DualChannel" : + sbc_info->sbc.mode == SBC_MODE_STEREO ? "Stereo" : "JointStereo", + sbc_info->sbc.bitpool, (unsigned)sbc_info->codesize, (unsigned)sbc_info->frame_length); + + return sbc_info; +} + +static void deinit(void *codec_info) { + struct sbc_info *sbc_info = (struct sbc_info *) codec_info; + + sbc_finish(&sbc_info->sbc); + pa_xfree(sbc_info); +} + +static void set_bitpool(struct sbc_info *sbc_info, uint8_t bitpool) { + if (bitpool > sbc_info->max_bitpool) + bitpool = sbc_info->max_bitpool; + else if (bitpool < sbc_info->min_bitpool) + bitpool = sbc_info->min_bitpool; + + sbc_info->sbc.bitpool = bitpool; + + sbc_info->codesize = sbc_get_codesize(&sbc_info->sbc); + sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc); + + pa_log_debug("Bitpool has changed to %u", sbc_info->sbc.bitpool); +} + +static int reset(void *codec_info) { + struct sbc_info *sbc_info = (struct sbc_info *) codec_info; + int ret; + + ret = sbc_reinit(&sbc_info->sbc, 0); + if (ret != 0) { + pa_log_error("SBC reinitialization failed: %d", ret); + return -1; + } + + /* sbc_reinit() sets also default parameters, so reset them back */ + set_params(sbc_info); + + sbc_info->seq_num = 0; + return 0; +} + +static size_t get_block_size(void *codec_info, size_t link_mtu) { + struct sbc_info *sbc_info = (struct sbc_info *) codec_info; + size_t rtp_size = sizeof(struct rtp_header) + sizeof(struct rtp_sbc_payload); + size_t frame_count = (link_mtu - rtp_size) / sbc_info->frame_length; + + /* frame_count is only 4 bit number */ + if (frame_count > 15) + frame_count = 15; + + return frame_count * sbc_info->codesize; +} + +static size_t reduce_encoder_bitrate(void *codec_info, size_t write_link_mtu) { + struct sbc_info *sbc_info = (struct sbc_info *) codec_info; + uint8_t bitpool; + + /* Check if bitpool is already at its limit */ + if (sbc_info->sbc.bitpool <= SBC_BITPOOL_DEC_LIMIT) + return 0; + + bitpool = sbc_info->sbc.bitpool - SBC_BITPOOL_DEC_STEP; + + if (bitpool < SBC_BITPOOL_DEC_LIMIT) + bitpool = SBC_BITPOOL_DEC_LIMIT; + + if (sbc_info->sbc.bitpool == bitpool) + return 0; + + set_bitpool(sbc_info, bitpool); + return get_block_size(codec_info, write_link_mtu); +} + +static size_t encode_buffer(void *codec_info, uint32_t timestamp, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed) { + struct sbc_info *sbc_info = (struct sbc_info *) codec_info; + struct rtp_header *header; + struct rtp_sbc_payload *payload; + uint8_t *d; + const uint8_t *p; + size_t to_write, to_encode; + uint8_t frame_count; + + header = (struct rtp_header*) output_buffer; + payload = (struct rtp_sbc_payload*) (output_buffer + sizeof(*header)); + + frame_count = 0; + + p = input_buffer; + to_encode = input_size; + + d = output_buffer + sizeof(*header) + sizeof(*payload); + to_write = output_size - sizeof(*header) - sizeof(*payload); + + /* frame_count is only 4 bit number */ + while (PA_LIKELY(to_encode > 0 && to_write > 0 && frame_count < 15)) { + ssize_t written; + ssize_t encoded; + + encoded = sbc_encode(&sbc_info->sbc, + p, to_encode, + d, to_write, + &written); + + if (PA_UNLIKELY(encoded <= 0)) { + pa_log_error("SBC encoding error (%li)", (long) encoded); + break; + } + + if (PA_UNLIKELY(written < 0)) { + pa_log_error("SBC encoding error (%li)", (long) written); + break; + } + + pa_assert_fp((size_t) encoded <= to_encode); + pa_assert_fp((size_t) encoded == sbc_info->codesize); + + pa_assert_fp((size_t) written <= to_write); + pa_assert_fp((size_t) written == sbc_info->frame_length); + + p += encoded; + to_encode -= encoded; + + d += written; + to_write -= written; + + frame_count++; + } + + PA_ONCE_BEGIN { + pa_log_debug("Using SBC codec implementation: %s", pa_strnull(sbc_get_implementation_info(&sbc_info->sbc))); + } PA_ONCE_END; + + if (PA_UNLIKELY(frame_count == 0)) { + *processed = 0; + return 0; + } + + /* write it to the fifo */ + pa_memzero(output_buffer, sizeof(*header) + sizeof(*payload)); + header->v = 2; + + /* A2DP spec: "A payload type in the RTP dynamic range shall be chosen". + * RFC3551 defines the dynamic range to span from 96 to 127, and 96 appears + * to be the most common choice in A2DP implementations. */ + header->pt = 96; + + header->sequence_number = htons(sbc_info->seq_num++); + header->timestamp = htonl(timestamp); + header->ssrc = htonl(1); + payload->frame_count = frame_count; + + *processed = p - input_buffer; + return d - output_buffer; +} + +static size_t decode_buffer(void *codec_info, const uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size, size_t *processed) { + struct sbc_info *sbc_info = (struct sbc_info *) codec_info; + + struct rtp_header *header; + struct rtp_sbc_payload *payload; + const uint8_t *p; + uint8_t *d; + size_t to_write, to_decode; + uint8_t frame_count; + + header = (struct rtp_header *) input_buffer; + payload = (struct rtp_sbc_payload*) (input_buffer + sizeof(*header)); + + frame_count = payload->frame_count; + + /* TODO: Add support for decoding fragmented SBC frames */ + if (payload->is_fragmented) { + pa_log_error("Unsupported fragmented SBC frame"); + *processed = 0; + return 0; + } + + p = input_buffer + sizeof(*header) + sizeof(*payload); + to_decode = input_size - sizeof(*header) - sizeof(*payload); + + d = output_buffer; + to_write = output_size; + + while (PA_LIKELY(to_decode > 0 && to_write > 0 && frame_count > 0)) { + size_t written; + ssize_t decoded; + + decoded = sbc_decode(&sbc_info->sbc, + p, to_decode, + d, to_write, + &written); + + if (PA_UNLIKELY(decoded <= 0)) { + pa_log_error("SBC decoding error (%li)", (long) decoded); + break; + } + + /* Reset frame length, it can be changed due to bitpool change */ + sbc_info->frame_length = sbc_get_frame_length(&sbc_info->sbc); + + pa_assert_fp((size_t) decoded <= to_decode); + pa_assert_fp((size_t) decoded == sbc_info->frame_length); + + pa_assert_fp((size_t) written <= to_write); + pa_assert_fp((size_t) written == sbc_info->codesize); + + p += decoded; + to_decode -= decoded; + + d += written; + to_write -= written; + + frame_count--; + } + + *processed = p - input_buffer; + return d - output_buffer; +} + +const pa_a2dp_codec pa_a2dp_codec_sbc = { + .name = "sbc", + .description = "SBC", + .id = { A2DP_CODEC_SBC, 0, 0 }, + .support_backchannel = false, + .can_accept_capabilities = can_accept_capabilities, + .choose_remote_endpoint = choose_remote_endpoint, + .fill_capabilities = fill_capabilities, + .is_configuration_valid = is_configuration_valid, + .fill_preferred_configuration = fill_preferred_configuration, + .init = init, + .deinit = deinit, + .reset = reset, + .get_read_block_size = get_block_size, + .get_write_block_size = get_block_size, + .reduce_encoder_bitrate = reduce_encoder_bitrate, + .encode_buffer = encode_buffer, + .decode_buffer = decode_buffer, +}; diff --git a/src/modules/bluetooth/a2dp-codec-util.c b/src/modules/bluetooth/a2dp-codec-util.c new file mode 100644 index 0000000..94d01e7 --- /dev/null +++ b/src/modules/bluetooth/a2dp-codec-util.c @@ -0,0 +1,56 @@ +/*** + This file is part of PulseAudio. + + Copyright 2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulsecore/core.h> +#include <pulsecore/core-util.h> + +#include "a2dp-codec-util.h" + +extern const pa_a2dp_codec pa_a2dp_codec_sbc; + +/* This is list of supported codecs. Their order is important. + * Codec with higher index has higher priority. */ +const pa_a2dp_codec *pa_a2dp_codecs[] = { + &pa_a2dp_codec_sbc, +}; + +unsigned int pa_bluetooth_a2dp_codec_count(void) { + return PA_ELEMENTSOF(pa_a2dp_codecs); +} + +const pa_a2dp_codec *pa_bluetooth_a2dp_codec_iter(unsigned int i) { + pa_assert(i < pa_bluetooth_a2dp_codec_count()); + return pa_a2dp_codecs[i]; +} + +const pa_a2dp_codec *pa_bluetooth_get_a2dp_codec(const char *name) { + unsigned int i; + unsigned int count = pa_bluetooth_a2dp_codec_count(); + + for (i = 0; i < count; i++) { + if (pa_streq(pa_a2dp_codecs[i]->name, name)) + return pa_a2dp_codecs[i]; + } + + return NULL; +} diff --git a/src/modules/bluetooth/a2dp-codec-util.h b/src/modules/bluetooth/a2dp-codec-util.h new file mode 100644 index 0000000..86f233a --- /dev/null +++ b/src/modules/bluetooth/a2dp-codec-util.h @@ -0,0 +1,34 @@ +#ifndef fooa2dpcodecutilhfoo +#define fooa2dpcodecutilhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#include "a2dp-codec-api.h" + +/* Get number of supported A2DP codecs */ +unsigned int pa_bluetooth_a2dp_codec_count(void); + +/* Get i-th codec. Codec with higher number has higher priority */ +const pa_a2dp_codec *pa_bluetooth_a2dp_codec_iter(unsigned int i); + +/* Get codec by name */ +const pa_a2dp_codec *pa_bluetooth_get_a2dp_codec(const char *name); + +#endif diff --git a/src/modules/bluetooth/a2dp-codecs.h b/src/modules/bluetooth/a2dp-codecs.h new file mode 100644 index 0000000..93e9d35 --- /dev/null +++ b/src/modules/bluetooth/a2dp-codecs.h @@ -0,0 +1,435 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * Copyright (C) 2018 Pali Rohár <pali.rohar@gmail.com> + * + * + * 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 + * + */ + +#include <endian.h> +#include <stdint.h> + +#define A2DP_CODEC_SBC 0x00 +#define A2DP_CODEC_MPEG12 0x01 +#define A2DP_CODEC_MPEG24 0x02 +#define A2DP_CODEC_ATRAC 0x04 +#define A2DP_CODEC_VENDOR 0xFF + +#define SBC_SAMPLING_FREQ_16000 (1 << 3) +#define SBC_SAMPLING_FREQ_32000 (1 << 2) +#define SBC_SAMPLING_FREQ_44100 (1 << 1) +#define SBC_SAMPLING_FREQ_48000 1 + +#define SBC_CHANNEL_MODE_MONO (1 << 3) +#define SBC_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define SBC_CHANNEL_MODE_STEREO (1 << 1) +#define SBC_CHANNEL_MODE_JOINT_STEREO 1 + +#define SBC_BLOCK_LENGTH_4 (1 << 3) +#define SBC_BLOCK_LENGTH_8 (1 << 2) +#define SBC_BLOCK_LENGTH_12 (1 << 1) +#define SBC_BLOCK_LENGTH_16 1 + +#define SBC_SUBBANDS_4 (1 << 1) +#define SBC_SUBBANDS_8 1 + +#define SBC_ALLOCATION_SNR (1 << 1) +#define SBC_ALLOCATION_LOUDNESS 1 + +#define SBC_MIN_BITPOOL 2 +#define SBC_MAX_BITPOOL 250 + +/* Other settings: + * Block length = 16 + * Allocation method = Loudness + * Subbands = 8 + */ +#define SBC_BITPOOL_MQ_MONO_44100 19 +#define SBC_BITPOOL_MQ_MONO_48000 18 +#define SBC_BITPOOL_MQ_JOINT_STEREO_44100 35 +#define SBC_BITPOOL_MQ_JOINT_STEREO_48000 33 +#define SBC_BITPOOL_HQ_MONO_44100 31 +#define SBC_BITPOOL_HQ_MONO_48000 29 +#define SBC_BITPOOL_HQ_JOINT_STEREO_44100 53 +#define SBC_BITPOOL_HQ_JOINT_STEREO_48000 51 + +#define MPEG_CHANNEL_MODE_MONO (1 << 3) +#define MPEG_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define MPEG_CHANNEL_MODE_STEREO (1 << 1) +#define MPEG_CHANNEL_MODE_JOINT_STEREO 1 + +#define MPEG_LAYER_MP1 (1 << 2) +#define MPEG_LAYER_MP2 (1 << 1) +#define MPEG_LAYER_MP3 1 + +#define MPEG_SAMPLING_FREQ_16000 (1 << 5) +#define MPEG_SAMPLING_FREQ_22050 (1 << 4) +#define MPEG_SAMPLING_FREQ_24000 (1 << 3) +#define MPEG_SAMPLING_FREQ_32000 (1 << 2) +#define MPEG_SAMPLING_FREQ_44100 (1 << 1) +#define MPEG_SAMPLING_FREQ_48000 1 + +#define MPEG_BIT_RATE_INDEX_0 (1 << 0) +#define MPEG_BIT_RATE_INDEX_1 (1 << 1) +#define MPEG_BIT_RATE_INDEX_2 (1 << 2) +#define MPEG_BIT_RATE_INDEX_3 (1 << 3) +#define MPEG_BIT_RATE_INDEX_4 (1 << 4) +#define MPEG_BIT_RATE_INDEX_5 (1 << 5) +#define MPEG_BIT_RATE_INDEX_6 (1 << 6) +#define MPEG_BIT_RATE_INDEX_7 (1 << 7) +#define MPEG_BIT_RATE_INDEX_8 (1 << 8) +#define MPEG_BIT_RATE_INDEX_9 (1 << 9) +#define MPEG_BIT_RATE_INDEX_10 (1 << 10) +#define MPEG_BIT_RATE_INDEX_11 (1 << 11) +#define MPEG_BIT_RATE_INDEX_12 (1 << 12) +#define MPEG_BIT_RATE_INDEX_13 (1 << 13) +#define MPEG_BIT_RATE_INDEX_14 (1 << 14) + +#define MPEG_MP1_BIT_RATE_32000 MPEG_BIT_RATE_INDEX_1 +#define MPEG_MP1_BIT_RATE_64000 MPEG_BIT_RATE_INDEX_2 +#define MPEG_MP1_BIT_RATE_96000 MPEG_BIT_RATE_INDEX_3 +#define MPEG_MP1_BIT_RATE_128000 MPEG_BIT_RATE_INDEX_4 +#define MPEG_MP1_BIT_RATE_160000 MPEG_BIT_RATE_INDEX_5 +#define MPEG_MP1_BIT_RATE_192000 MPEG_BIT_RATE_INDEX_6 +#define MPEG_MP1_BIT_RATE_224000 MPEG_BIT_RATE_INDEX_7 +#define MPEG_MP1_BIT_RATE_256000 MPEG_BIT_RATE_INDEX_8 +#define MPEG_MP1_BIT_RATE_288000 MPEG_BIT_RATE_INDEX_9 +#define MPEG_MP1_BIT_RATE_320000 MPEG_BIT_RATE_INDEX_10 +#define MPEG_MP1_BIT_RATE_352000 MPEG_BIT_RATE_INDEX_11 +#define MPEG_MP1_BIT_RATE_384000 MPEG_BIT_RATE_INDEX_12 +#define MPEG_MP1_BIT_RATE_416000 MPEG_BIT_RATE_INDEX_13 +#define MPEG_MP1_BIT_RATE_448000 MPEG_BIT_RATE_INDEX_14 + +#define MPEG_MP2_BIT_RATE_32000 MPEG_BIT_RATE_INDEX_1 +#define MPEG_MP2_BIT_RATE_48000 MPEG_BIT_RATE_INDEX_2 +#define MPEG_MP2_BIT_RATE_56000 MPEG_BIT_RATE_INDEX_3 +#define MPEG_MP2_BIT_RATE_64000 MPEG_BIT_RATE_INDEX_4 +#define MPEG_MP2_BIT_RATE_80000 MPEG_BIT_RATE_INDEX_5 +#define MPEG_MP2_BIT_RATE_96000 MPEG_BIT_RATE_INDEX_6 +#define MPEG_MP2_BIT_RATE_112000 MPEG_BIT_RATE_INDEX_7 +#define MPEG_MP2_BIT_RATE_128000 MPEG_BIT_RATE_INDEX_8 +#define MPEG_MP2_BIT_RATE_160000 MPEG_BIT_RATE_INDEX_9 +#define MPEG_MP2_BIT_RATE_192000 MPEG_BIT_RATE_INDEX_10 +#define MPEG_MP2_BIT_RATE_224000 MPEG_BIT_RATE_INDEX_11 +#define MPEG_MP2_BIT_RATE_256000 MPEG_BIT_RATE_INDEX_12 +#define MPEG_MP2_BIT_RATE_320000 MPEG_BIT_RATE_INDEX_13 +#define MPEG_MP2_BIT_RATE_384000 MPEG_BIT_RATE_INDEX_14 + +#define MPEG_MP3_BIT_RATE_32000 MPEG_BIT_RATE_INDEX_1 +#define MPEG_MP3_BIT_RATE_40000 MPEG_BIT_RATE_INDEX_2 +#define MPEG_MP3_BIT_RATE_48000 MPEG_BIT_RATE_INDEX_3 +#define MPEG_MP3_BIT_RATE_56000 MPEG_BIT_RATE_INDEX_4 +#define MPEG_MP3_BIT_RATE_64000 MPEG_BIT_RATE_INDEX_5 +#define MPEG_MP3_BIT_RATE_80000 MPEG_BIT_RATE_INDEX_6 +#define MPEG_MP3_BIT_RATE_96000 MPEG_BIT_RATE_INDEX_7 +#define MPEG_MP3_BIT_RATE_112000 MPEG_BIT_RATE_INDEX_8 +#define MPEG_MP3_BIT_RATE_128000 MPEG_BIT_RATE_INDEX_9 +#define MPEG_MP3_BIT_RATE_160000 MPEG_BIT_RATE_INDEX_10 +#define MPEG_MP3_BIT_RATE_192000 MPEG_BIT_RATE_INDEX_11 +#define MPEG_MP3_BIT_RATE_224000 MPEG_BIT_RATE_INDEX_12 +#define MPEG_MP3_BIT_RATE_256000 MPEG_BIT_RATE_INDEX_13 +#define MPEG_MP3_BIT_RATE_320000 MPEG_BIT_RATE_INDEX_14 + +#define MPEG_BIT_RATE_FREE MPEG_BIT_RATE_INDEX_0 + +#define MPEG_GET_BITRATE(a) ((uint16_t)(a).bitrate1 << 8 | (a).bitrate2) +#define MPEG_SET_BITRATE(a, b) \ + do { \ + (a).bitrate1 = ((b) >> 8) & 0x7f; \ + (a).bitrate2 = (b) & 0xff; \ + } while (0) + +#define AAC_OBJECT_TYPE_MPEG2_AAC_LC 0x80 +#define AAC_OBJECT_TYPE_MPEG4_AAC_LC 0x40 +#define AAC_OBJECT_TYPE_MPEG4_AAC_LTP 0x20 +#define AAC_OBJECT_TYPE_MPEG4_AAC_SCA 0x10 + +#define AAC_SAMPLING_FREQ_8000 0x0800 +#define AAC_SAMPLING_FREQ_11025 0x0400 +#define AAC_SAMPLING_FREQ_12000 0x0200 +#define AAC_SAMPLING_FREQ_16000 0x0100 +#define AAC_SAMPLING_FREQ_22050 0x0080 +#define AAC_SAMPLING_FREQ_24000 0x0040 +#define AAC_SAMPLING_FREQ_32000 0x0020 +#define AAC_SAMPLING_FREQ_44100 0x0010 +#define AAC_SAMPLING_FREQ_48000 0x0008 +#define AAC_SAMPLING_FREQ_64000 0x0004 +#define AAC_SAMPLING_FREQ_88200 0x0002 +#define AAC_SAMPLING_FREQ_96000 0x0001 + +#define AAC_CHANNELS_1 0x02 +#define AAC_CHANNELS_2 0x01 + +#define AAC_GET_BITRATE(a) ((a).bitrate1 << 16 | \ + (a).bitrate2 << 8 | (a).bitrate3) +#define AAC_GET_FREQUENCY(a) ((a).frequency1 << 4 | (a).frequency2) + +#define AAC_SET_BITRATE(a, b) \ + do { \ + (a).bitrate1 = (b >> 16) & 0x7f; \ + (a).bitrate2 = (b >> 8) & 0xff; \ + (a).bitrate3 = b & 0xff; \ + } while (0) +#define AAC_SET_FREQUENCY(a, f) \ + do { \ + (a).frequency1 = (f >> 4) & 0xff; \ + (a).frequency2 = f & 0x0f; \ + } while (0) + +#define AAC_INIT_BITRATE(b) \ + .bitrate1 = (b >> 16) & 0x7f, \ + .bitrate2 = (b >> 8) & 0xff, \ + .bitrate3 = b & 0xff, +#define AAC_INIT_FREQUENCY(f) \ + .frequency1 = (f >> 4) & 0xff, \ + .frequency2 = f & 0x0f, + +#define APTX_VENDOR_ID 0x0000004f +#define APTX_CODEC_ID 0x0001 + +#define APTX_CHANNEL_MODE_MONO 0x01 +#define APTX_CHANNEL_MODE_STEREO 0x02 + +#define APTX_SAMPLING_FREQ_16000 0x08 +#define APTX_SAMPLING_FREQ_32000 0x04 +#define APTX_SAMPLING_FREQ_44100 0x02 +#define APTX_SAMPLING_FREQ_48000 0x01 + +#define FASTSTREAM_VENDOR_ID 0x0000000a +#define FASTSTREAM_CODEC_ID 0x0001 + +#define FASTSTREAM_DIRECTION_SINK 0x1 +#define FASTSTREAM_DIRECTION_SOURCE 0x2 + +#define FASTSTREAM_SINK_SAMPLING_FREQ_44100 0x2 +#define FASTSTREAM_SINK_SAMPLING_FREQ_48000 0x1 + +#define FASTSTREAM_SOURCE_SAMPLING_FREQ_16000 0x2 + +#define APTX_LL_VENDOR_ID 0x0000000a +#define APTX_LL_CODEC_ID 0x0002 + +/* Default parameters for aptX Low Latency encoder */ + +/* Target codec buffer level = 180 */ +#define APTX_LL_TARGET_LEVEL2 0xb4 +#define APTX_LL_TARGET_LEVEL1 0x00 + +/* Initial codec buffer level = 360 */ +#define APTX_LL_INITIAL_LEVEL2 0x68 +#define APTX_LL_INITIAL_LEVEL1 0x01 + +/* SRA max rate 0.005 * 10000 = 50 */ +#define APTX_LL_SRA_MAX_RATE 0x32 + +/* SRA averaging time = 1s */ +#define APTX_LL_SRA_AVG_TIME 0x01 + +/* Good working codec buffer level = 180 */ +#define APTX_LL_GOOD_WORKING_LEVEL2 0xB4 +#define APTX_LL_GOOD_WORKING_LEVEL1 0x00 + +#define APTX_HD_VENDOR_ID 0x000000D7 +#define APTX_HD_CODEC_ID 0x0024 + +#define LDAC_VENDOR_ID 0x0000012d +#define LDAC_CODEC_ID 0x00aa + +#define LDAC_SAMPLING_FREQ_44100 0x20 +#define LDAC_SAMPLING_FREQ_48000 0x10 +#define LDAC_SAMPLING_FREQ_88200 0x08 +#define LDAC_SAMPLING_FREQ_96000 0x04 +#define LDAC_SAMPLING_FREQ_176400 0x02 +#define LDAC_SAMPLING_FREQ_192000 0x01 + +#define LDAC_CHANNEL_MODE_MONO 0x04 +#define LDAC_CHANNEL_MODE_DUAL 0x02 +#define LDAC_CHANNEL_MODE_STEREO 0x01 + +typedef struct { + uint8_t vendor_id4; + uint8_t vendor_id3; + uint8_t vendor_id2; + uint8_t vendor_id1; + uint8_t codec_id2; + uint8_t codec_id1; +} __attribute__ ((packed)) a2dp_vendor_codec_t; + +#define A2DP_GET_VENDOR_ID(a) ( \ + (((uint32_t)(a).vendor_id4) << 0) | \ + (((uint32_t)(a).vendor_id3) << 8) | \ + (((uint32_t)(a).vendor_id2) << 16) | \ + (((uint32_t)(a).vendor_id1) << 24) \ + ) +#define A2DP_GET_CODEC_ID(a) ((a).codec_id2 | (((uint16_t)(a).codec_id1) << 8)) +#define A2DP_SET_VENDOR_ID_CODEC_ID(v, c) ((a2dp_vendor_codec_t){ \ + .vendor_id4 = (((v) >> 0) & 0xff), \ + .vendor_id3 = (((v) >> 8) & 0xff), \ + .vendor_id2 = (((v) >> 16) & 0xff), \ + .vendor_id1 = (((v) >> 24) & 0xff), \ + .codec_id2 = (((c) >> 0) & 0xff), \ + .codec_id1 = (((c) >> 8) & 0xff), \ + }) + +typedef struct { + uint8_t reserved; + uint8_t target_level2; + uint8_t target_level1; + uint8_t initial_level2; + uint8_t initial_level1; + uint8_t sra_max_rate; + uint8_t sra_avg_time; + uint8_t good_working_level2; + uint8_t good_working_level1; +} __attribute__ ((packed)) a2dp_aptx_ll_new_caps_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency; + uint8_t channel_mode; +} __attribute__ ((packed)) a2dp_ldac_t; + +#if defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && \ + __BYTE_ORDER == __LITTLE_ENDIAN + +typedef struct { + uint8_t channel_mode:4; + uint8_t frequency:4; + uint8_t allocation_method:2; + uint8_t subbands:2; + uint8_t block_length:4; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) a2dp_sbc_t; + +typedef struct { + uint8_t channel_mode:4; + uint8_t crc:1; + uint8_t layer:3; + uint8_t frequency:6; + uint8_t mpf:1; + uint8_t rfa:1; + uint8_t bitrate1:7; + uint8_t vbr:1; + uint8_t bitrate2; +} __attribute__ ((packed)) a2dp_mpeg_t; + +typedef struct { + uint8_t object_type; + uint8_t frequency1; + uint8_t rfa:2; + uint8_t channels:2; + uint8_t frequency2:4; + uint8_t bitrate1:7; + uint8_t vbr:1; + uint8_t bitrate2; + uint8_t bitrate3; +} __attribute__ ((packed)) a2dp_aac_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t channel_mode:4; + uint8_t frequency:4; +} __attribute__ ((packed)) a2dp_aptx_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t direction; + uint8_t sink_frequency:4; + uint8_t source_frequency:4; +} __attribute__ ((packed)) a2dp_faststream_t; + +typedef struct { + a2dp_aptx_t aptx; + uint8_t bidirect_link:1; + uint8_t has_new_caps:1; + uint8_t reserved:6; + a2dp_aptx_ll_new_caps_t new_caps[0]; +} __attribute__ ((packed)) a2dp_aptx_ll_t; + +#elif defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && \ + __BYTE_ORDER == __BIG_ENDIAN + +typedef struct { + uint8_t frequency:4; + uint8_t channel_mode:4; + uint8_t block_length:4; + uint8_t subbands:2; + uint8_t allocation_method:2; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) a2dp_sbc_t; + +typedef struct { + uint8_t layer:3; + uint8_t crc:1; + uint8_t channel_mode:4; + uint8_t rfa:1; + uint8_t mpf:1; + uint8_t frequency:6; + uint8_t vbr:1; + uint8_t bitrate1:7; + uint8_t bitrate2; +} __attribute__ ((packed)) a2dp_mpeg_t; + +typedef struct { + uint8_t object_type; + uint8_t frequency1; + uint8_t frequency2:4; + uint8_t channels:2; + uint8_t rfa:2; + uint8_t vbr:1; + uint8_t bitrate1:7; + uint8_t bitrate2; + uint8_t bitrate3; +} __attribute__ ((packed)) a2dp_aac_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency:4; + uint8_t channel_mode:4; +} __attribute__ ((packed)) a2dp_aptx_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t direction; + uint8_t source_frequency:4; + uint8_t sink_frequency:4; +} __attribute__ ((packed)) a2dp_faststream_t; + +typedef struct { + a2dp_aptx_t aptx; + uint8_t reserved:6; + uint8_t has_new_caps:1; + uint8_t bidirect_link:1; + a2dp_aptx_ll_new_caps_t new_caps[0]; +} __attribute__ ((packed)) a2dp_aptx_ll_t; + +#else +#error "Unknown byte order" +#endif + +typedef struct { + a2dp_aptx_t aptx; + uint8_t reserved0; + uint8_t reserved1; + uint8_t reserved2; + uint8_t reserved3; +} __attribute__ ((packed)) a2dp_aptx_hd_t; diff --git a/src/modules/bluetooth/backend-native.c b/src/modules/bluetooth/backend-native.c new file mode 100644 index 0000000..5ba7439 --- /dev/null +++ b/src/modules/bluetooth/backend-native.c @@ -0,0 +1,714 @@ +/*** + This file is part of PulseAudio. + + Copyright 2014 Wim Taymans <wim.taymans at gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulsecore/shared.h> +#include <pulsecore/core-error.h> +#include <pulsecore/core-util.h> +#include <pulsecore/dbus-shared.h> +#include <pulsecore/log.h> + +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/sco.h> + +#include "bluez5-util.h" + +struct pa_bluetooth_backend { + pa_core *core; + pa_dbus_connection *connection; + pa_bluetooth_discovery *discovery; + bool enable_hs_role; + + PA_LLIST_HEAD(pa_dbus_pending, pending); +}; + +struct transport_data { + int rfcomm_fd; + pa_io_event *rfcomm_io; + int sco_fd; + pa_io_event *sco_io; + pa_mainloop_api *mainloop; +}; + +#define BLUEZ_SERVICE "org.bluez" +#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1" + +#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported" + +#define BLUEZ_PROFILE_MANAGER_INTERFACE BLUEZ_SERVICE ".ProfileManager1" +#define BLUEZ_PROFILE_INTERFACE BLUEZ_SERVICE ".Profile1" + +#define HSP_AG_PROFILE "/Profile/HSPAGProfile" +#define HSP_HS_PROFILE "/Profile/HSPHSProfile" + +/* RFCOMM channel for HSP headset role + * The choice seems to be a bit arbitrary -- it looks like at least channels 2, 4 and 5 also work*/ +#define HSP_HS_DEFAULT_CHANNEL 3 + +#define PROFILE_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "<node>" \ + " <interface name=\"" BLUEZ_PROFILE_INTERFACE "\">" \ + " <method name=\"Release\">" \ + " </method>" \ + " <method name=\"RequestDisconnection\">" \ + " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \ + " </method>" \ + " <method name=\"NewConnection\">" \ + " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \ + " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \ + " <arg name=\"opts\" direction=\"in\" type=\"a{sv}\"/>" \ + " </method>" \ + " </interface>" \ + " <interface name=\"org.freedesktop.DBus.Introspectable\">" \ + " <method name=\"Introspect\">" \ + " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \ + " </method>" \ + " </interface>" \ + "</node>" + +static pa_dbus_pending* send_and_add_to_pending(pa_bluetooth_backend *backend, DBusMessage *m, + DBusPendingCallNotifyFunction func, void *call_data) { + + pa_dbus_pending *p; + DBusPendingCall *call; + + pa_assert(backend); + pa_assert(m); + + pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(backend->connection), m, &call, -1)); + + p = pa_dbus_pending_new(pa_dbus_connection_get(backend->connection), m, call, backend, call_data); + PA_LLIST_PREPEND(pa_dbus_pending, backend->pending, p); + dbus_pending_call_set_notify(call, func, p, NULL); + + return p; +} + +static int sco_do_connect(pa_bluetooth_transport *t) { + pa_bluetooth_device *d = t->device; + struct sockaddr_sco addr; + socklen_t len; + int err, i; + int sock; + bdaddr_t src; + bdaddr_t dst; + const char *src_addr, *dst_addr; + + src_addr = d->adapter->address; + dst_addr = d->address; + + /* don't use ba2str to avoid -lbluetooth */ + for (i = 5; i >= 0; i--, src_addr += 3) + src.b[i] = strtol(src_addr, NULL, 16); + for (i = 5; i >= 0; i--, dst_addr += 3) + dst.b[i] = strtol(dst_addr, NULL, 16); + + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO); + if (sock < 0) { + pa_log_error("socket(SEQPACKET, SCO) %s", pa_cstrerror(errno)); + return -1; + } + + len = sizeof(addr); + memset(&addr, 0, len); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &src); + + if (bind(sock, (struct sockaddr *) &addr, len) < 0) { + pa_log_error("bind(): %s", pa_cstrerror(errno)); + goto fail_close; + } + + memset(&addr, 0, len); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &dst); + + pa_log_info("doing connect"); + err = connect(sock, (struct sockaddr *) &addr, len); + if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) { + pa_log_error("connect(): %s", pa_cstrerror(errno)); + goto fail_close; + } + return sock; + +fail_close: + close(sock); + return -1; +} + +static int sco_do_accept(pa_bluetooth_transport *t) { + struct transport_data *trd = t->userdata; + struct sockaddr_sco addr; + socklen_t optlen; + int sock; + + memset(&addr, 0, sizeof(addr)); + optlen = sizeof(addr); + + pa_log_info ("doing accept"); + sock = accept(trd->sco_fd, (struct sockaddr *) &addr, &optlen); + if (sock < 0) { + if (errno != EAGAIN) + pa_log_error("accept(): %s", pa_cstrerror(errno)); + goto fail; + } + return sock; + +fail: + return -1; +} + +static int sco_acquire_cb(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) { + int sock; + socklen_t len; + + if (optional) + sock = sco_do_accept(t); + else + sock = sco_do_connect(t); + + if (sock < 0) + goto fail; + + if (imtu) *imtu = 48; + if (omtu) *omtu = 48; + + if (t->device->autodetect_mtu) { + struct sco_options sco_opt; + + len = sizeof(sco_opt); + memset(&sco_opt, 0, len); + + if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0) + pa_log_warn("getsockopt(SCO_OPTIONS) failed, loading defaults"); + else { + pa_log_debug("autodetected imtu = omtu = %u", sco_opt.mtu); + if (imtu) *imtu = sco_opt.mtu; + if (omtu) *omtu = sco_opt.mtu; + } + } + + return sock; + +fail: + return -1; +} + +static void sco_release_cb(pa_bluetooth_transport *t) { + pa_log_info("Transport %s released", t->path); + /* device will close the SCO socket for us */ +} + +static void sco_io_callback(pa_mainloop_api *io, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) { + pa_bluetooth_transport *t = userdata; + + pa_assert(io); + pa_assert(t); + + if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) { + pa_log_error("error listening SCO connection: %s", pa_cstrerror(errno)); + goto fail; + } + + if (t->state != PA_BLUETOOTH_TRANSPORT_STATE_PLAYING) { + pa_log_info("SCO incoming connection: changing state to PLAYING"); + pa_bluetooth_transport_set_state (t, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING); + } + +fail: + return; +} + +static int sco_listen(pa_bluetooth_transport *t) { + struct transport_data *trd = t->userdata; + struct sockaddr_sco addr; + int sock, i; + bdaddr_t src; + const char *src_addr; + + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, BTPROTO_SCO); + if (sock < 0) { + pa_log_error("socket(SEQPACKET, SCO) %s", pa_cstrerror(errno)); + return -1; + } + + src_addr = t->device->adapter->address; + + /* don't use ba2str to avoid -lbluetooth */ + for (i = 5; i >= 0; i--, src_addr += 3) + src.b[i] = strtol(src_addr, NULL, 16); + + /* Bind to local address */ + memset(&addr, 0, sizeof(addr)); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &src); + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + pa_log_error("bind(): %s", pa_cstrerror(errno)); + goto fail_close; + } + + pa_log_info ("doing listen"); + if (listen(sock, 1) < 0) { + pa_log_error("listen(): %s", pa_cstrerror(errno)); + goto fail_close; + } + + trd->sco_fd = sock; + trd->sco_io = trd->mainloop->io_new(trd->mainloop, sock, PA_IO_EVENT_INPUT, + sco_io_callback, t); + + return sock; + +fail_close: + close(sock); + return -1; +} + +static void register_profile_reply(DBusPendingCall *pending, void *userdata) { + DBusMessage *r; + pa_dbus_pending *p; + pa_bluetooth_backend *b; + char *profile; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(b = p->context_data); + pa_assert_se(profile = p->call_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + pa_log_info("Couldn't register profile %s because it is disabled in BlueZ", profile); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error(BLUEZ_PROFILE_MANAGER_INTERFACE ".RegisterProfile() failed: %s: %s", dbus_message_get_error_name(r), + pa_dbus_get_error_message(r)); + goto finish; + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); + pa_dbus_pending_free(p); + + pa_xfree(profile); +} + +static void register_profile(pa_bluetooth_backend *b, const char *profile, const char *uuid) { + DBusMessage *m; + DBusMessageIter i, d; + dbus_bool_t autoconnect; + dbus_uint16_t version, chan; + + pa_log_debug("Registering Profile %s %s", profile, uuid); + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile")); + + dbus_message_iter_init_append(m, &i); + pa_assert_se(dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &profile)); + pa_assert_se(dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &uuid)); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING + DBUS_TYPE_VARIANT_AS_STRING DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d); + if (pa_bluetooth_uuid_is_hsp_hs(uuid)) { + /* In the headset role, the connection will only be initiated from the remote side */ + autoconnect = 0; + pa_dbus_append_basic_variant_dict_entry(&d, "AutoConnect", DBUS_TYPE_BOOLEAN, &autoconnect); + chan = HSP_HS_DEFAULT_CHANNEL; + pa_dbus_append_basic_variant_dict_entry(&d, "Channel", DBUS_TYPE_UINT16, &chan); + /* HSP version 1.2 */ + version = 0x0102; + pa_dbus_append_basic_variant_dict_entry(&d, "Version", DBUS_TYPE_UINT16, &version); + } + dbus_message_iter_close_container(&i, &d); + + send_and_add_to_pending(b, m, register_profile_reply, pa_xstrdup(profile)); +} + +static void rfcomm_io_callback(pa_mainloop_api *io, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) { + pa_bluetooth_transport *t = userdata; + + pa_assert(io); + pa_assert(t); + + if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) { + pa_log_info("Lost RFCOMM connection."); + goto fail; + } + + if (events & PA_IO_EVENT_INPUT) { + char buf[512]; + ssize_t len; + int gain, dummy; + bool do_reply = false; + + len = pa_read(fd, buf, 511, NULL); + if (len < 0) { + pa_log_error("RFCOMM read error: %s", pa_cstrerror(errno)); + goto fail; + } + buf[len] = 0; + pa_log_debug("RFCOMM << %s", buf); + + /* There are only four HSP AT commands: + * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain. + * +VGS=value is sent by AG to HS as a response to an AT+VGS command or when the gain + * is changed on the AG side. + * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain. + * +VGM=value is sent by AG to HS as a response to an AT+VGM command or when the gain + * is changed on the AG side. + * AT+CKPD=200: Sent by HS when headset button is pressed. + * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because + * it does not expect a reply. */ + if (sscanf(buf, "AT+VGS=%d", &gain) == 1 || sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) { + t->speaker_gain = gain; + pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED), t); + do_reply = true; + + } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1 || sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) { + t->microphone_gain = gain; + pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED), t); + do_reply = true; + } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) { + do_reply = true; + } else { + do_reply = false; + } + + if (do_reply) { + pa_log_debug("RFCOMM >> OK"); + + len = write(fd, "\r\nOK\r\n", 6); + + /* we ignore any errors, it's not critical and real errors should + * be caught with the HANGUP and ERROR events handled above */ + if (len < 0) + pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno)); + } + } + + return; + +fail: + pa_bluetooth_transport_unlink(t); + pa_bluetooth_transport_free(t); +} + +static void transport_destroy(pa_bluetooth_transport *t) { + struct transport_data *trd = t->userdata; + + if (trd->sco_io) { + trd->mainloop->io_free(trd->sco_io); + shutdown(trd->sco_fd, SHUT_RDWR); + close (trd->sco_fd); + } + + trd->mainloop->io_free(trd->rfcomm_io); + shutdown(trd->rfcomm_fd, SHUT_RDWR); + close (trd->rfcomm_fd); + + pa_xfree(trd); +} + +static void set_speaker_gain(pa_bluetooth_transport *t, uint16_t gain) { + struct transport_data *trd = t->userdata; + char buf[512]; + ssize_t len, written; + + if (t->speaker_gain == gain) + return; + + t->speaker_gain = gain; + + /* If we are in the AG role, we send a command to the head set to change + * the speaker gain. In the HS role, source and sink are swapped, so + * in this case we notify the AG that the microphone gain has changed */ + if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) { + len = sprintf(buf, "\r\n+VGS=%d\r\n", gain); + pa_log_debug("RFCOMM >> +VGS=%d", gain); + } else { + len = sprintf(buf, "\r\nAT+VGM=%d\r\n", gain); + pa_log_debug("RFCOMM >> AT+VGM=%d", gain); + } + + written = write(trd->rfcomm_fd, buf, len); + + if (written != len) + pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno)); +} + +static void set_microphone_gain(pa_bluetooth_transport *t, uint16_t gain) { + struct transport_data *trd = t->userdata; + char buf[512]; + ssize_t len, written; + + if (t->microphone_gain == gain) + return; + + t->microphone_gain = gain; + + /* If we are in the AG role, we send a command to the head set to change + * the microphone gain. In the HS role, source and sink are swapped, so + * in this case we notify the AG that the speaker gain has changed */ + if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) { + len = sprintf(buf, "\r\n+VGM=%d\r\n", gain); + pa_log_debug("RFCOMM >> +VGM=%d", gain); + } else { + len = sprintf(buf, "\r\nAT+VGS=%d\r\n", gain); + pa_log_debug("RFCOMM >> AT+VGS=%d", gain); + } + + written = write (trd->rfcomm_fd, buf, len); + + if (written != len) + pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno)); +} + +static DBusMessage *profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_backend *b = userdata; + pa_bluetooth_device *d; + pa_bluetooth_transport *t; + pa_bluetooth_profile_t p; + DBusMessage *r; + int fd; + const char *sender, *path, PA_UNUSED *handler; + DBusMessageIter arg_i; + char *pathfd; + struct transport_data *trd; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oha{sv}")) { + pa_log_error("Invalid signature found in NewConnection"); + goto fail; + } + + handler = dbus_message_get_path(m); + if (pa_streq(handler, HSP_AG_PROFILE)) { + p = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT; + } else if (pa_streq(handler, HSP_HS_PROFILE)) { + p = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY; + } else { + pa_log_error("Invalid handler"); + goto fail; + } + + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_OBJECT_PATH); + dbus_message_iter_get_basic(&arg_i, &path); + + d = pa_bluetooth_discovery_get_device_by_path(b->discovery, path); + if (d == NULL) { + pa_log_error("Device doesnt exist for %s", path); + goto fail; + } + + pa_assert_se(dbus_message_iter_next(&arg_i)); + + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_UNIX_FD); + dbus_message_iter_get_basic(&arg_i, &fd); + + pa_log_debug("dbus: NewConnection path=%s, fd=%d, profile %s", path, fd, + pa_bluetooth_profile_to_string(p)); + + sender = dbus_message_get_sender(m); + + pathfd = pa_sprintf_malloc ("%s/fd%d", path, fd); + t = pa_bluetooth_transport_new(d, sender, pathfd, p, NULL, 0); + pa_xfree(pathfd); + + t->acquire = sco_acquire_cb; + t->release = sco_release_cb; + t->destroy = transport_destroy; + t->set_speaker_gain = set_speaker_gain; + t->set_microphone_gain = set_microphone_gain; + + trd = pa_xnew0(struct transport_data, 1); + trd->rfcomm_fd = fd; + trd->mainloop = b->core->mainloop; + trd->rfcomm_io = trd->mainloop->io_new(b->core->mainloop, fd, PA_IO_EVENT_INPUT, + rfcomm_io_callback, t); + t->userdata = trd; + + sco_listen(t); + + pa_bluetooth_transport_put(t); + + pa_log_debug("Transport %s available for profile %s", t->path, pa_bluetooth_profile_to_string(t->profile)); + + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; + +fail: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to handle new connection")); + return r; +} + +static DBusMessage *profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata) { + DBusMessage *r; + + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; +} + +static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata) { + pa_bluetooth_backend *b = userdata; + DBusMessage *r = NULL; + const char *path, *interface, *member; + + pa_assert(b); + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (!pa_streq(path, HSP_AG_PROFILE) && !pa_streq(path, HSP_HS_PROFILE)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = PROFILE_INTROSPECT_XML; + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)); + + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Release")) { + pa_log_debug("Release not handled"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection")) { + r = profile_request_disconnection(c, m, userdata); + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection")) + r = profile_new_connection(c, m, userdata); + else + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (r) { + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(b->connection), r, NULL)); + dbus_message_unref(r); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void profile_init(pa_bluetooth_backend *b, pa_bluetooth_profile_t profile) { + static const DBusObjectPathVTable vtable_profile = { + .message_function = profile_handler, + }; + const char *object_name; + const char *uuid; + + pa_assert(b); + + switch (profile) { + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + object_name = HSP_AG_PROFILE; + uuid = PA_BLUETOOTH_UUID_HSP_AG; + break; + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + object_name = HSP_HS_PROFILE; + uuid = PA_BLUETOOTH_UUID_HSP_HS; + break; + default: + pa_assert_not_reached(); + break; + } + + pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(b->connection), object_name, &vtable_profile, b)); + register_profile(b, object_name, uuid); +} + +static void profile_done(pa_bluetooth_backend *b, pa_bluetooth_profile_t profile) { + pa_assert(b); + + switch (profile) { + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + dbus_connection_unregister_object_path(pa_dbus_connection_get(b->connection), HSP_AG_PROFILE); + break; + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + dbus_connection_unregister_object_path(pa_dbus_connection_get(b->connection), HSP_HS_PROFILE); + break; + default: + pa_assert_not_reached(); + break; + } +} + +void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *native_backend, bool enable_hs_role) { + + if (enable_hs_role == native_backend->enable_hs_role) + return; + + if (enable_hs_role) + profile_init(native_backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY); + else + profile_done(native_backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY); + + native_backend->enable_hs_role = enable_hs_role; +} + +pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role) { + pa_bluetooth_backend *backend; + DBusError err; + + pa_log_debug("Bluetooth Headset Backend API support using the native backend"); + + backend = pa_xnew0(pa_bluetooth_backend, 1); + backend->core = c; + + dbus_error_init(&err); + if (!(backend->connection = pa_dbus_bus_get(c, DBUS_BUS_SYSTEM, &err))) { + pa_log("Failed to get D-Bus connection: %s", err.message); + dbus_error_free(&err); + pa_xfree(backend); + return NULL; + } + + backend->discovery = y; + backend->enable_hs_role = enable_hs_role; + + if (enable_hs_role) + profile_init(backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY); + profile_init(backend, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT); + + return backend; +} + +void pa_bluetooth_native_backend_free(pa_bluetooth_backend *backend) { + pa_assert(backend); + + pa_dbus_free_pending_list(&backend->pending); + + if (backend->enable_hs_role) + profile_done(backend, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY); + profile_done(backend, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT); + + pa_dbus_connection_unref(backend->connection); + + pa_xfree(backend); +} diff --git a/src/modules/bluetooth/backend-ofono.c b/src/modules/bluetooth/backend-ofono.c new file mode 100644 index 0000000..d7a13ef --- /dev/null +++ b/src/modules/bluetooth/backend-ofono.c @@ -0,0 +1,764 @@ +/*** + This file is part of PulseAudio. + + Copyright 2013 João Paulo Rechi Vita + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <errno.h> +#include <poll.h> + +#include <pulsecore/core-util.h> +#include <pulsecore/dbus-shared.h> +#include <pulsecore/shared.h> +#include <pulsecore/core-error.h> + +#include "bluez5-util.h" + +#define HFP_AUDIO_CODEC_CVSD 0x01 +#define HFP_AUDIO_CODEC_MSBC 0x02 + +#define OFONO_SERVICE "org.ofono" +#define HF_AUDIO_AGENT_INTERFACE OFONO_SERVICE ".HandsfreeAudioAgent" +#define HF_AUDIO_MANAGER_INTERFACE OFONO_SERVICE ".HandsfreeAudioManager" + +#define HF_AUDIO_AGENT_PATH "/HandsfreeAudioAgent" + +#define HF_AUDIO_AGENT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "<node>" \ + " <interface name=\"org.freedesktop.DBus.Introspectable\">" \ + " <method name=\"Introspect\">" \ + " <arg direction=\"out\" type=\"s\" />" \ + " </method>" \ + " </interface>" \ + " <interface name=\"org.ofono.HandsfreeAudioAgent\">" \ + " <method name=\"Release\">" \ + " </method>" \ + " <method name=\"NewConnection\">" \ + " <arg direction=\"in\" type=\"o\" name=\"card_path\" />" \ + " <arg direction=\"in\" type=\"h\" name=\"sco_fd\" />" \ + " <arg direction=\"in\" type=\"y\" name=\"codec\" />" \ + " </method>" \ + " </interface>" \ + "</node>" + +struct hf_audio_card { + pa_bluetooth_backend *backend; + char *path; + char *remote_address; + char *local_address; + + bool connecting; + int fd; + int (*acquire)(struct hf_audio_card *card); + + pa_bluetooth_transport *transport; + pa_hook_slot *device_unlink_slot; +}; + +struct pa_bluetooth_backend { + pa_core *core; + pa_bluetooth_discovery *discovery; + pa_dbus_connection *connection; + pa_hashmap *cards; + char *ofono_bus_id; + + PA_LLIST_HEAD(pa_dbus_pending, pending); +}; + +static pa_dbus_pending* hf_dbus_send_and_add_to_pending(pa_bluetooth_backend *backend, DBusMessage *m, + DBusPendingCallNotifyFunction func, void *call_data) { + pa_dbus_pending *p; + DBusPendingCall *call; + + pa_assert(backend); + pa_assert(m); + + pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(backend->connection), m, &call, -1)); + + p = pa_dbus_pending_new(pa_dbus_connection_get(backend->connection), m, call, backend, call_data); + PA_LLIST_PREPEND(pa_dbus_pending, backend->pending, p); + dbus_pending_call_set_notify(call, func, p, NULL); + + return p; +} + +static DBusMessage *card_send(struct hf_audio_card *card, const char *method, DBusError *err) +{ + pa_bluetooth_transport *t = card->transport; + DBusMessage *m, *r; + + pa_assert_se(m = dbus_message_new_method_call(t->owner, t->path, "org.ofono.HandsfreeAudioCard", method)); + r = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(card->backend->connection), m, -1, err); + dbus_message_unref(m); + + return r; +} + +static int card_connect(struct hf_audio_card *card) { + DBusMessage *r; + DBusError err; + + if (card->connecting) + return -EAGAIN; + + card->connecting = true; + + dbus_error_init(&err); + r = card_send(card, "Connect", &err); + + if (!r) { + pa_log_error("Failed to connect %s: %s", err.name, err.message); + card->connecting = false; + dbus_error_free(&err); + return -1; + } + + dbus_message_unref(r); + + if (card->connecting) + return -EAGAIN; + + return 0; +} + +static int card_acquire(struct hf_audio_card *card) { + int fd; + uint8_t codec; + DBusMessage *r; + DBusError err; + + /* Try acquiring the stream first which was introduced in 1.21 */ + dbus_error_init(&err); + r = card_send(card, "Acquire", &err); + + if (!r) { + if (!pa_streq(err.name, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_error("Failed to acquire %s: %s", err.name, err.message); + dbus_error_free(&err); + return -1; + } + dbus_error_free(&err); + /* Fallback to Connect as this might be an old version of ofono */ + card->acquire = card_connect; + return card_connect(card); + } + + if ((dbus_message_get_args(r, NULL, DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_BYTE, &codec, + DBUS_TYPE_INVALID) == true)) { + dbus_message_unref(r); + if (codec != HFP_AUDIO_CODEC_CVSD) { + pa_log_error("Invalid codec: %u", codec); + /* shutdown to make sure connection is dropped immediately */ + shutdown(fd, SHUT_RDWR); + close(fd); + return -1; + } + card->transport->codec = codec; + card->fd = fd; + return 0; + } + + pa_log_error("Unable to acquire"); + dbus_message_unref(r); + return -1; +} + +static void hf_audio_agent_card_removed(pa_bluetooth_backend *backend, const char *path); + +static pa_hook_result_t device_unlink_cb(pa_bluetooth_discovery *y, const pa_bluetooth_device *d, struct hf_audio_card *card) { + pa_assert(d); + pa_assert(card); + + hf_audio_agent_card_removed(card->backend, card->path); + + return PA_HOOK_OK; +} + +static struct hf_audio_card *hf_audio_card_new(pa_bluetooth_backend *backend, const char *path) { + struct hf_audio_card *card = pa_xnew0(struct hf_audio_card, 1); + + card->path = pa_xstrdup(path); + card->backend = backend; + card->fd = -1; + card->acquire = card_acquire; + + card->device_unlink_slot = pa_hook_connect(pa_bluetooth_discovery_hook(backend->discovery, PA_BLUETOOTH_HOOK_DEVICE_UNLINK), + PA_HOOK_NORMAL, (pa_hook_cb_t) device_unlink_cb, card); + + return card; +} + +static void hf_audio_card_free(struct hf_audio_card *card) { + pa_assert(card); + + if (card->device_unlink_slot) + pa_hook_slot_free(card->device_unlink_slot); + + if (card->transport) + pa_bluetooth_transport_free(card->transport); + + pa_xfree(card->path); + pa_xfree(card->remote_address); + pa_xfree(card->local_address); + pa_xfree(card); +} + +static int socket_accept(int sock) +{ + char c; + struct pollfd pfd; + + if (sock < 0) + return -ENOTCONN; + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = sock; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, 0) < 0) + return -errno; + + /* + * If socket already writable then it is not in defer setup state, + * otherwise it needs to be read to authorize the connection. + */ + if ((pfd.revents & POLLOUT)) + return 0; + + /* Enable socket by reading 1 byte */ + if (read(sock, &c, 1) < 0) + return -errno; + + return 0; +} + +static int hf_audio_agent_transport_acquire(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) { + struct hf_audio_card *card = t->userdata; + int err; + + pa_assert(card); + + if (!optional && card->fd < 0) { + err = card->acquire(card); + if (err < 0) + return err; + } + + /* The correct block size should take into account the SCO MTU from + * the Bluetooth adapter and (for adapters in the USB bus) the MxPS + * value from the Isoc USB endpoint in use by btusb and should be + * made available to userspace by the Bluetooth kernel subsystem. + * Meanwhile the empiric value 48 will be used. */ + if (imtu) + *imtu = 48; + if (omtu) + *omtu = 48; + + err = socket_accept(card->fd); + if (err < 0) { + pa_log_error("Deferred setup failed on fd %d: %s", card->fd, pa_cstrerror(-err)); + return -1; + } + + return card->fd; +} + +static void hf_audio_agent_transport_release(pa_bluetooth_transport *t) { + struct hf_audio_card *card = t->userdata; + + pa_assert(card); + + if (card->fd < 0) { + pa_log_info("Transport %s already released", t->path); + return; + } + + /* shutdown to make sure connection is dropped immediately */ + shutdown(card->fd, SHUT_RDWR); + close(card->fd); + card->fd = -1; +} + +static void hf_audio_agent_card_found(pa_bluetooth_backend *backend, const char *path, DBusMessageIter *props_i) { + DBusMessageIter i, value_i; + const char *key, *value; + struct hf_audio_card *card; + pa_bluetooth_device *d; + pa_bluetooth_profile_t p = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY; + + pa_assert(backend); + pa_assert(path); + pa_assert(props_i); + + pa_log_debug("New HF card found: %s", path); + + card = hf_audio_card_new(backend, path); + + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + char c; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if ((c = dbus_message_iter_get_arg_type(&value_i)) != DBUS_TYPE_STRING) { + pa_log_error("Invalid properties for %s: expected 's', received '%c'", path, c); + goto fail; + } + + dbus_message_iter_get_basic(&value_i, &value); + + if (pa_streq(key, "RemoteAddress")) { + pa_xfree(card->remote_address); + card->remote_address = pa_xstrdup(value); + } else if (pa_streq(key, "LocalAddress")) { + pa_xfree(card->local_address); + card->local_address = pa_xstrdup(value); + } else if (pa_streq(key, "Type")) { + if (pa_streq(value, "gateway")) + p = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT; + } + + pa_log_debug("%s: %s", key, value); + + dbus_message_iter_next(props_i); + } + + d = pa_bluetooth_discovery_get_device_by_address(backend->discovery, card->remote_address, card->local_address); + if (!d) { + pa_log_error("Device doesnt exist for %s", path); + goto fail; + } + + card->transport = pa_bluetooth_transport_new(d, backend->ofono_bus_id, path, p, NULL, 0); + card->transport->acquire = hf_audio_agent_transport_acquire; + card->transport->release = hf_audio_agent_transport_release; + card->transport->userdata = card; + + pa_bluetooth_transport_put(card->transport); + pa_hashmap_put(backend->cards, card->path, card); + + return; + +fail: + hf_audio_card_free(card); +} + +static void hf_audio_agent_card_removed(pa_bluetooth_backend *backend, const char *path) { + struct hf_audio_card *card; + + pa_assert(backend); + pa_assert(path); + + pa_log_debug("HF card removed: %s", path); + + card = pa_hashmap_remove(backend->cards, path); + if (!card) + return; + + hf_audio_card_free(card); +} + +static void hf_audio_agent_get_cards_reply(DBusPendingCall *pending, void *userdata) { + DBusMessage *r; + pa_dbus_pending *p; + pa_bluetooth_backend *backend; + DBusMessageIter i, array_i, struct_i, props_i; + + pa_assert_se(p = userdata); + pa_assert_se(backend = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("Failed to get a list of handsfree audio cards from ofono: %s: %s", + dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &i) || !pa_streq(dbus_message_get_signature(r), "a(oa{sv})")) { + pa_log_error("Invalid arguments in GetCards() reply"); + goto finish; + } + + dbus_message_iter_recurse(&i, &array_i); + while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) { + const char *path; + + dbus_message_iter_recurse(&array_i, &struct_i); + dbus_message_iter_get_basic(&struct_i, &path); + dbus_message_iter_next(&struct_i); + + dbus_message_iter_recurse(&struct_i, &props_i); + + hf_audio_agent_card_found(backend, path, &props_i); + + dbus_message_iter_next(&array_i); + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, backend->pending, p); + pa_dbus_pending_free(p); +} + +static void hf_audio_agent_get_cards(pa_bluetooth_backend *hf) { + DBusMessage *m; + + pa_assert(hf); + + pa_assert_se(m = dbus_message_new_method_call(OFONO_SERVICE, "/", HF_AUDIO_MANAGER_INTERFACE, "GetCards")); + hf_dbus_send_and_add_to_pending(hf, m, hf_audio_agent_get_cards_reply, NULL); +} + +static void ofono_bus_id_destroy(pa_bluetooth_backend *backend) { + pa_hashmap_remove_all(backend->cards); + + if (backend->ofono_bus_id) { + pa_xfree(backend->ofono_bus_id); + backend->ofono_bus_id = NULL; + pa_bluetooth_discovery_set_ofono_running(backend->discovery, false); + } +} + +static void hf_audio_agent_register_reply(DBusPendingCall *pending, void *userdata) { + DBusMessage *r; + pa_dbus_pending *p; + pa_bluetooth_backend *backend; + + pa_assert_se(p = userdata); + pa_assert_se(backend = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_info("Failed to register as a handsfree audio agent with ofono: %s: %s", + dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + backend->ofono_bus_id = pa_xstrdup(dbus_message_get_sender(r)); + + hf_audio_agent_get_cards(backend); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, backend->pending, p); + pa_dbus_pending_free(p); + + pa_bluetooth_discovery_set_ofono_running(backend->discovery, backend->ofono_bus_id != NULL); +} + +static void hf_audio_agent_register(pa_bluetooth_backend *hf) { + DBusMessage *m; + uint8_t codecs[2]; + const uint8_t *pcodecs = codecs; + int ncodecs = 0; + const char *path = HF_AUDIO_AGENT_PATH; + + pa_assert(hf); + + pa_assert_se(m = dbus_message_new_method_call(OFONO_SERVICE, "/", HF_AUDIO_MANAGER_INTERFACE, "Register")); + + codecs[ncodecs++] = HFP_AUDIO_CODEC_CVSD; + + pa_assert_se(dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &pcodecs, ncodecs, + DBUS_TYPE_INVALID)); + + hf_dbus_send_and_add_to_pending(hf, m, hf_audio_agent_register_reply, NULL); +} + +static void hf_audio_agent_unregister(pa_bluetooth_backend *backend) { + DBusMessage *m; + const char *path = HF_AUDIO_AGENT_PATH; + + pa_assert(backend); + pa_assert(backend->connection); + + if (backend->ofono_bus_id) { + pa_assert_se(m = dbus_message_new_method_call(backend->ofono_bus_id, "/", HF_AUDIO_MANAGER_INTERFACE, "Unregister")); + pa_assert_se(dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)); + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(backend->connection), m, NULL)); + + ofono_bus_id_destroy(backend); + } +} + +static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *data) { + const char *sender; + DBusError err; + pa_bluetooth_backend *backend = data; + + pa_assert(bus); + pa_assert(m); + pa_assert(backend); + + sender = dbus_message_get_sender(m); + if (!pa_safe_streq(backend->ofono_bus_id, sender) && !pa_streq("org.freedesktop.DBus", sender)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + pa_log_error("Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto fail; + } + + if (pa_streq(name, OFONO_SERVICE)) { + + if (old_owner && *old_owner) { + pa_log_debug("oFono disappeared"); + ofono_bus_id_destroy(backend); + } + + if (new_owner && *new_owner) { + pa_log_debug("oFono appeared"); + hf_audio_agent_register(backend); + } + } + + } else if (dbus_message_is_signal(m, "org.ofono.HandsfreeAudioManager", "CardAdded")) { + const char *p; + DBusMessageIter arg_i, props_i; + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oa{sv}")) { + pa_log_error("Failed to parse org.ofono.HandsfreeAudioManager.CardAdded"); + goto fail; + } + + dbus_message_iter_get_basic(&arg_i, &p); + + pa_assert_se(dbus_message_iter_next(&arg_i)); + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&arg_i, &props_i); + + hf_audio_agent_card_found(backend, p, &props_i); + } else if (dbus_message_is_signal(m, "org.ofono.HandsfreeAudioManager", "CardRemoved")) { + const char *p; + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) { + pa_log_error("Failed to parse org.ofono.HandsfreeAudioManager.CardRemoved: %s", err.message); + goto fail; + } + + hf_audio_agent_card_removed(backend, p); + } + +fail: + dbus_error_free(&err); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static DBusMessage *hf_audio_agent_release(DBusConnection *c, DBusMessage *m, void *data) { + DBusMessage *r; + const char *sender; + pa_bluetooth_backend *backend = data; + + pa_assert(backend); + + sender = dbus_message_get_sender(m); + if (!pa_safe_streq(backend->ofono_bus_id, sender)) { + pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.NotAllowed", "Operation is not allowed by this sender")); + return r; + } + + pa_log_debug("HF audio agent has been unregistered by oFono (%s)", backend->ofono_bus_id); + + ofono_bus_id_destroy(backend); + + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; +} + +static DBusMessage *hf_audio_agent_new_connection(DBusConnection *c, DBusMessage *m, void *data) { + DBusMessage *r; + const char *sender, *path; + int fd; + uint8_t codec; + struct hf_audio_card *card; + pa_bluetooth_backend *backend = data; + + pa_assert(backend); + + sender = dbus_message_get_sender(m); + if (!pa_safe_streq(backend->ofono_bus_id, sender)) { + pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.NotAllowed", "Operation is not allowed by this sender")); + return r; + } + + if (dbus_message_get_args(m, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_BYTE, &codec, + DBUS_TYPE_INVALID) == FALSE) { + pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.InvalidArguments", "Invalid arguments in method call")); + return r; + } + + card = pa_hashmap_get(backend->cards, path); + + if (!card || codec != HFP_AUDIO_CODEC_CVSD || card->fd >= 0) { + pa_log_warn("New audio connection invalid arguments (path=%s fd=%d, codec=%d)", path, fd, codec); + pa_assert_se(r = dbus_message_new_error(m, "org.ofono.Error.InvalidArguments", "Invalid arguments in method call")); + shutdown(fd, SHUT_RDWR); + close(fd); + return r; + } + + pa_log_debug("New audio connection on card %s (fd=%d, codec=%d)", path, fd, codec); + + card->connecting = false; + card->fd = fd; + card->transport->codec = codec; + + pa_bluetooth_transport_set_state(card->transport, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING); + + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; +} + +static DBusHandlerResult hf_audio_agent_handler(DBusConnection *c, DBusMessage *m, void *data) { + pa_bluetooth_backend *backend = data; + DBusMessage *r = NULL; + const char *path, *interface, *member; + + pa_assert(backend); + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + if (!pa_streq(path, HF_AUDIO_AGENT_PATH)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = HF_AUDIO_AGENT_XML; + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)); + + } else if (dbus_message_is_method_call(m, HF_AUDIO_AGENT_INTERFACE, "NewConnection")) + r = hf_audio_agent_new_connection(c, m, data); + else if (dbus_message_is_method_call(m, HF_AUDIO_AGENT_INTERFACE, "Release")) + r = hf_audio_agent_release(c, m, data); + else + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (r) { + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(backend->connection), r, NULL)); + dbus_message_unref(r); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +pa_bluetooth_backend *pa_bluetooth_ofono_backend_new(pa_core *c, pa_bluetooth_discovery *y) { + pa_bluetooth_backend *backend; + DBusError err; + static const DBusObjectPathVTable vtable_hf_audio_agent = { + .message_function = hf_audio_agent_handler, + }; + + pa_assert(c); + + backend = pa_xnew0(pa_bluetooth_backend, 1); + backend->core = c; + backend->discovery = y; + backend->cards = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) hf_audio_card_free); + + dbus_error_init(&err); + + if (!(backend->connection = pa_dbus_bus_get(c, DBUS_BUS_SYSTEM, &err))) { + pa_log("Failed to get D-Bus connection: %s", err.message); + dbus_error_free(&err); + pa_xfree(backend); + return NULL; + } + + /* dynamic detection of handsfree audio cards */ + if (!dbus_connection_add_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend, NULL)) { + pa_log_error("Failed to add filter function"); + pa_dbus_connection_unref(backend->connection); + pa_xfree(backend); + return NULL; + } + + if (pa_dbus_add_matches(pa_dbus_connection_get(backend->connection), &err, + "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" OFONO_SERVICE "'", + "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'", + "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'", + NULL) < 0) { + pa_log("Failed to add oFono D-Bus matches: %s", err.message); + dbus_connection_remove_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend); + pa_dbus_connection_unref(backend->connection); + pa_xfree(backend); + return NULL; + } + + pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(backend->connection), HF_AUDIO_AGENT_PATH, + &vtable_hf_audio_agent, backend)); + + hf_audio_agent_register(backend); + + return backend; +} + +void pa_bluetooth_ofono_backend_free(pa_bluetooth_backend *backend) { + pa_assert(backend); + + pa_dbus_free_pending_list(&backend->pending); + + hf_audio_agent_unregister(backend); + + dbus_connection_unregister_object_path(pa_dbus_connection_get(backend->connection), HF_AUDIO_AGENT_PATH); + + pa_dbus_remove_matches(pa_dbus_connection_get(backend->connection), + "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" OFONO_SERVICE "'", + "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'", + "type='signal',sender='" OFONO_SERVICE "',interface='" HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'", + NULL); + + dbus_connection_remove_filter(pa_dbus_connection_get(backend->connection), filter_cb, backend); + + pa_dbus_connection_unref(backend->connection); + + pa_hashmap_free(backend->cards); + + pa_xfree(backend); +} diff --git a/src/modules/bluetooth/bluez5-util.c b/src/modules/bluetooth/bluez5-util.c new file mode 100644 index 0000000..a21896e --- /dev/null +++ b/src/modules/bluetooth/bluez5-util.c @@ -0,0 +1,1744 @@ +/*** + This file is part of PulseAudio. + + Copyright 2008-2013 João Paulo Rechi Vita + Copyrigth 2018-2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulse/rtclock.h> +#include <pulse/timeval.h> +#include <pulse/xmalloc.h> + +#include <pulsecore/core.h> +#include <pulsecore/core-util.h> +#include <pulsecore/dbus-shared.h> +#include <pulsecore/log.h> +#include <pulsecore/macro.h> +#include <pulsecore/refcnt.h> +#include <pulsecore/shared.h> + +#include "a2dp-codec-util.h" +#include "a2dp-codecs.h" + +#include "bluez5-util.h" + +#define WAIT_FOR_PROFILES_TIMEOUT_USEC (3 * PA_USEC_PER_SEC) + +#define BLUEZ_SERVICE "org.bluez" +#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1" +#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1" +#define BLUEZ_MEDIA_INTERFACE BLUEZ_SERVICE ".Media1" +#define BLUEZ_MEDIA_ENDPOINT_INTERFACE BLUEZ_SERVICE ".MediaEndpoint1" +#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1" + +#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported" + +#define A2DP_SOURCE_ENDPOINT "/MediaEndpoint/A2DPSource" +#define A2DP_SINK_ENDPOINT "/MediaEndpoint/A2DPSink" + +#define ENDPOINT_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "<node>" \ + " <interface name=\"" BLUEZ_MEDIA_ENDPOINT_INTERFACE "\">" \ + " <method name=\"SetConfiguration\">" \ + " <arg name=\"transport\" direction=\"in\" type=\"o\"/>" \ + " <arg name=\"properties\" direction=\"in\" type=\"ay\"/>" \ + " </method>" \ + " <method name=\"SelectConfiguration\">" \ + " <arg name=\"capabilities\" direction=\"in\" type=\"ay\"/>" \ + " <arg name=\"configuration\" direction=\"out\" type=\"ay\"/>" \ + " </method>" \ + " <method name=\"ClearConfiguration\">" \ + " <arg name=\"transport\" direction=\"in\" type=\"o\"/>" \ + " </method>" \ + " <method name=\"Release\">" \ + " </method>" \ + " </interface>" \ + " <interface name=\"org.freedesktop.DBus.Introspectable\">" \ + " <method name=\"Introspect\">" \ + " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \ + " </method>" \ + " </interface>" \ + "</node>" + +struct pa_bluetooth_discovery { + PA_REFCNT_DECLARE; + + pa_core *core; + pa_dbus_connection *connection; + bool filter_added; + bool matches_added; + bool objects_listed; + pa_hook hooks[PA_BLUETOOTH_HOOK_MAX]; + pa_hashmap *adapters; + pa_hashmap *devices; + pa_hashmap *transports; + + int headset_backend; + pa_bluetooth_backend *ofono_backend, *native_backend; + PA_LLIST_HEAD(pa_dbus_pending, pending); +}; + +static pa_dbus_pending* send_and_add_to_pending(pa_bluetooth_discovery *y, DBusMessage *m, + DBusPendingCallNotifyFunction func, void *call_data) { + pa_dbus_pending *p; + DBusPendingCall *call; + + pa_assert(y); + pa_assert(m); + + pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(y->connection), m, &call, -1)); + + p = pa_dbus_pending_new(pa_dbus_connection_get(y->connection), m, call, y, call_data); + PA_LLIST_PREPEND(pa_dbus_pending, y->pending, p); + dbus_pending_call_set_notify(call, func, p, NULL); + + return p; +} + +static const char *check_variant_property(DBusMessageIter *i) { + const char *key; + + pa_assert(i); + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_STRING) { + pa_log_error("Property name not a string."); + return NULL; + } + + dbus_message_iter_get_basic(i, &key); + + if (!dbus_message_iter_next(i)) { + pa_log_error("Property value missing"); + return NULL; + } + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_VARIANT) { + pa_log_error("Property value not a variant."); + return NULL; + } + + return key; +} + +pa_bluetooth_transport *pa_bluetooth_transport_new(pa_bluetooth_device *d, const char *owner, const char *path, + pa_bluetooth_profile_t p, const uint8_t *config, size_t size) { + pa_bluetooth_transport *t; + + t = pa_xnew0(pa_bluetooth_transport, 1); + t->device = d; + t->owner = pa_xstrdup(owner); + t->path = pa_xstrdup(path); + t->profile = p; + t->config_size = size; + + if (size > 0) { + t->config = pa_xnew(uint8_t, size); + memcpy(t->config, config, size); + } + + return t; +} + +static const char *transport_state_to_string(pa_bluetooth_transport_state_t state) { + switch(state) { + case PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED: + return "disconnected"; + case PA_BLUETOOTH_TRANSPORT_STATE_IDLE: + return "idle"; + case PA_BLUETOOTH_TRANSPORT_STATE_PLAYING: + return "playing"; + } + + return "invalid"; +} + +static bool device_supports_profile(pa_bluetooth_device *device, pa_bluetooth_profile_t profile) { + switch (profile) { + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SINK); + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_A2DP_SOURCE); + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS) + || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_HS_ALT) + || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HFP_HF); + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + return !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HSP_AG) + || !!pa_hashmap_get(device->uuids, PA_BLUETOOTH_UUID_HFP_AG); + case PA_BLUETOOTH_PROFILE_OFF: + pa_assert_not_reached(); + } + + pa_assert_not_reached(); +} + +static bool device_is_profile_connected(pa_bluetooth_device *device, pa_bluetooth_profile_t profile) { + if (device->transports[profile] && device->transports[profile]->state != PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) + return true; + else + return false; +} + +static unsigned device_count_disconnected_profiles(pa_bluetooth_device *device) { + pa_bluetooth_profile_t profile; + unsigned count = 0; + + for (profile = 0; profile < PA_BLUETOOTH_PROFILE_COUNT; profile++) { + if (!device_supports_profile(device, profile)) + continue; + + if (!device_is_profile_connected(device, profile)) + count++; + } + + return count; +} + +static void device_stop_waiting_for_profiles(pa_bluetooth_device *device) { + if (!device->wait_for_profiles_timer) + return; + + device->discovery->core->mainloop->time_free(device->wait_for_profiles_timer); + device->wait_for_profiles_timer = NULL; +} + +static void wait_for_profiles_cb(pa_mainloop_api *api, pa_time_event* event, const struct timeval *tv, void *userdata) { + pa_bluetooth_device *device = userdata; + pa_strbuf *buf; + pa_bluetooth_profile_t profile; + bool first = true; + char *profiles_str; + + device_stop_waiting_for_profiles(device); + + buf = pa_strbuf_new(); + + for (profile = 0; profile < PA_BLUETOOTH_PROFILE_COUNT; profile++) { + if (device_is_profile_connected(device, profile)) + continue; + + if (!device_supports_profile(device, profile)) + continue; + + if (first) + first = false; + else + pa_strbuf_puts(buf, ", "); + + pa_strbuf_puts(buf, pa_bluetooth_profile_to_string(profile)); + } + + profiles_str = pa_strbuf_to_string_free(buf); + pa_log_debug("Timeout expired, and device %s still has disconnected profiles: %s", + device->path, profiles_str); + pa_xfree(profiles_str); + pa_hook_fire(&device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], device); +} + +static void device_start_waiting_for_profiles(pa_bluetooth_device *device) { + pa_assert(!device->wait_for_profiles_timer); + device->wait_for_profiles_timer = pa_core_rttime_new(device->discovery->core, + pa_rtclock_now() + WAIT_FOR_PROFILES_TIMEOUT_USEC, + wait_for_profiles_cb, device); +} + +void pa_bluetooth_transport_set_state(pa_bluetooth_transport *t, pa_bluetooth_transport_state_t state) { + bool old_any_connected; + unsigned n_disconnected_profiles; + bool new_device_appeared; + bool device_disconnected; + + pa_assert(t); + + if (t->state == state) + return; + + old_any_connected = pa_bluetooth_device_any_transport_connected(t->device); + + pa_log_debug("Transport %s state: %s -> %s", + t->path, transport_state_to_string(t->state), transport_state_to_string(state)); + + t->state = state; + + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED], t); + + /* If there are profiles that are expected to get connected soon (based + * on the UUID list), we wait for a bit before announcing the new + * device, so that all profiles have time to get connected before the + * card object is created. If we didn't wait, the card would always + * have only one profile marked as available in the initial state, + * which would prevent module-card-restore from restoring the initial + * profile properly. */ + + n_disconnected_profiles = device_count_disconnected_profiles(t->device); + + new_device_appeared = !old_any_connected && pa_bluetooth_device_any_transport_connected(t->device); + device_disconnected = old_any_connected && !pa_bluetooth_device_any_transport_connected(t->device); + + if (new_device_appeared) { + if (n_disconnected_profiles > 0) + device_start_waiting_for_profiles(t->device); + else + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], t->device); + return; + } + + if (device_disconnected) { + if (t->device->wait_for_profiles_timer) { + /* If the timer is still running when the device disconnects, we + * never sent the notification of the device getting connected, so + * we don't need to send a notification about the disconnection + * either. Let's just stop the timer. */ + device_stop_waiting_for_profiles(t->device); + } else + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], t->device); + return; + } + + if (n_disconnected_profiles == 0 && t->device->wait_for_profiles_timer) { + /* All profiles are now connected, so we can stop the wait timer and + * send a notification of the new device. */ + device_stop_waiting_for_profiles(t->device); + pa_hook_fire(&t->device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], t->device); + } +} + +void pa_bluetooth_transport_put(pa_bluetooth_transport *t) { + pa_assert(t); + + t->device->transports[t->profile] = t; + pa_assert_se(pa_hashmap_put(t->device->discovery->transports, t->path, t) >= 0); + pa_bluetooth_transport_set_state(t, PA_BLUETOOTH_TRANSPORT_STATE_IDLE); +} + +void pa_bluetooth_transport_unlink(pa_bluetooth_transport *t) { + pa_assert(t); + + pa_bluetooth_transport_set_state(t, PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED); + pa_hashmap_remove(t->device->discovery->transports, t->path); + t->device->transports[t->profile] = NULL; +} + +void pa_bluetooth_transport_free(pa_bluetooth_transport *t) { + pa_assert(t); + + if (t->destroy) + t->destroy(t); + pa_bluetooth_transport_unlink(t); + + pa_xfree(t->owner); + pa_xfree(t->path); + pa_xfree(t->config); + pa_xfree(t); +} + +static int bluez5_transport_acquire_cb(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) { + DBusMessage *m, *r; + DBusError err; + int ret; + uint16_t i, o; + const char *method = optional ? "TryAcquire" : "Acquire"; + + pa_assert(t); + pa_assert(t->device); + pa_assert(t->device->discovery); + + pa_assert_se(m = dbus_message_new_method_call(t->owner, t->path, BLUEZ_MEDIA_TRANSPORT_INTERFACE, method)); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(t->device->discovery->connection), m, -1, &err); + dbus_message_unref(m); + m = NULL; + if (!r) { + if (optional && pa_streq(err.name, "org.bluez.Error.NotAvailable")) + pa_log_info("Failed optional acquire of unavailable transport %s", t->path); + else + pa_log_error("Transport %s() failed for transport %s (%s)", method, t->path, err.message); + + dbus_error_free(&err); + return -1; + } + + if (!dbus_message_get_args(r, &err, DBUS_TYPE_UNIX_FD, &ret, DBUS_TYPE_UINT16, &i, DBUS_TYPE_UINT16, &o, + DBUS_TYPE_INVALID)) { + pa_log_error("Failed to parse %s() reply: %s", method, err.message); + dbus_error_free(&err); + ret = -1; + goto finish; + } + + if (imtu) + *imtu = i; + + if (omtu) + *omtu = o; + +finish: + dbus_message_unref(r); + return ret; +} + +static void bluez5_transport_release_cb(pa_bluetooth_transport *t) { + DBusMessage *m, *r; + DBusError err; + + pa_assert(t); + pa_assert(t->device); + pa_assert(t->device->discovery); + + dbus_error_init(&err); + + if (t->state <= PA_BLUETOOTH_TRANSPORT_STATE_IDLE) { + pa_log_info("Transport %s auto-released by BlueZ or already released", t->path); + return; + } + + pa_assert_se(m = dbus_message_new_method_call(t->owner, t->path, BLUEZ_MEDIA_TRANSPORT_INTERFACE, "Release")); + r = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(t->device->discovery->connection), m, -1, &err); + dbus_message_unref(m); + m = NULL; + if (r) { + dbus_message_unref(r); + r = NULL; + } + + if (dbus_error_is_set(&err)) { + pa_log_error("Failed to release transport %s: %s", t->path, err.message); + dbus_error_free(&err); + } else + pa_log_info("Transport %s released", t->path); +} + +bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d) { + unsigned i; + + pa_assert(d); + + if (!d->valid) + return false; + + for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) + if (d->transports[i] && d->transports[i]->state != PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) + return true; + + return false; +} + +static int transport_state_from_string(const char* value, pa_bluetooth_transport_state_t *state) { + pa_assert(value); + pa_assert(state); + + if (pa_streq(value, "idle")) + *state = PA_BLUETOOTH_TRANSPORT_STATE_IDLE; + else if (pa_streq(value, "pending") || pa_streq(value, "active")) + *state = PA_BLUETOOTH_TRANSPORT_STATE_PLAYING; + else + return -1; + + return 0; +} + +static void parse_transport_property(pa_bluetooth_transport *t, DBusMessageIter *i) { + const char *key; + DBusMessageIter variant_i; + + key = check_variant_property(i); + if (key == NULL) + return; + + dbus_message_iter_recurse(i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + + case DBUS_TYPE_STRING: { + + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "State")) { + pa_bluetooth_transport_state_t state; + + if (transport_state_from_string(value, &state) < 0) { + pa_log_error("Invalid state received: %s", value); + return; + } + + pa_bluetooth_transport_set_state(t, state); + } + + break; + } + } + + return; +} + +static int parse_transport_properties(pa_bluetooth_transport *t, DBusMessageIter *i) { + DBusMessageIter element_i; + + dbus_message_iter_recurse(i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&element_i, &dict_i); + + parse_transport_property(t, &dict_i); + + dbus_message_iter_next(&element_i); + } + + return 0; +} + +static pa_bluetooth_device* device_create(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *d; + + pa_assert(y); + pa_assert(path); + + d = pa_xnew0(pa_bluetooth_device, 1); + d->discovery = y; + d->path = pa_xstrdup(path); + d->uuids = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, pa_xfree); + + pa_hashmap_put(y->devices, d->path, d); + + return d; +} + +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_path(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *d; + + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + pa_assert(path); + + if ((d = pa_hashmap_get(y->devices, path)) && d->valid) + return d; + + return NULL; +} + +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_address(pa_bluetooth_discovery *y, const char *remote, const char *local) { + pa_bluetooth_device *d; + void *state = NULL; + + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + pa_assert(remote); + pa_assert(local); + + while ((d = pa_hashmap_iterate(y->devices, &state, NULL))) + if (d->valid && pa_streq(d->address, remote) && pa_streq(d->adapter->address, local)) + return d; + + return NULL; +} + +static void device_free(pa_bluetooth_device *d) { + unsigned i; + + pa_assert(d); + + device_stop_waiting_for_profiles(d); + + pa_hook_fire(&d->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_UNLINK], d); + + for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) { + pa_bluetooth_transport *t; + + if (!(t = d->transports[i])) + continue; + + pa_bluetooth_transport_free(t); + } + + if (d->uuids) + pa_hashmap_free(d->uuids); + + pa_xfree(d->path); + pa_xfree(d->alias); + pa_xfree(d->address); + pa_xfree(d->adapter_path); + pa_xfree(d); +} + +static void device_remove(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_device *d; + + if (!(d = pa_hashmap_remove(y->devices, path))) + pa_log_warn("Unknown device removed %s", path); + else { + pa_log_debug("Device %s removed", path); + device_free(d); + } +} + +static void device_set_valid(pa_bluetooth_device *device, bool valid) { + bool old_any_connected; + + pa_assert(device); + + if (valid == device->valid) + return; + + old_any_connected = pa_bluetooth_device_any_transport_connected(device); + device->valid = valid; + + if (pa_bluetooth_device_any_transport_connected(device) != old_any_connected) + pa_hook_fire(&device->discovery->hooks[PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED], device); +} + +static void device_update_valid(pa_bluetooth_device *d) { + pa_assert(d); + + if (!d->properties_received) { + pa_assert(!d->valid); + return; + } + + /* Check if mandatory properties are set. */ + if (!d->address || !d->adapter_path || !d->alias) { + device_set_valid(d, false); + return; + } + + if (!d->adapter || !d->adapter->valid) { + device_set_valid(d, false); + return; + } + + device_set_valid(d, true); +} + +static void device_set_adapter(pa_bluetooth_device *device, pa_bluetooth_adapter *adapter) { + pa_assert(device); + + if (adapter == device->adapter) + return; + + device->adapter = adapter; + + device_update_valid(device); +} + +static pa_bluetooth_adapter* adapter_create(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_adapter *a; + + pa_assert(y); + pa_assert(path); + + a = pa_xnew0(pa_bluetooth_adapter, 1); + a->discovery = y; + a->path = pa_xstrdup(path); + + pa_hashmap_put(y->adapters, a->path, a); + + return a; +} + +static void adapter_free(pa_bluetooth_adapter *a) { + pa_bluetooth_device *d; + void *state; + + pa_assert(a); + pa_assert(a->discovery); + + PA_HASHMAP_FOREACH(d, a->discovery->devices, state) + if (d->adapter == a) + device_set_adapter(d, NULL); + + pa_xfree(a->path); + pa_xfree(a->address); + pa_xfree(a); +} + +static void adapter_remove(pa_bluetooth_discovery *y, const char *path) { + pa_bluetooth_adapter *a; + + if (!(a = pa_hashmap_remove(y->adapters, path))) + pa_log_warn("Unknown adapter removed %s", path); + else { + pa_log_debug("Adapter %s removed", path); + adapter_free(a); + } +} + +static void parse_device_property(pa_bluetooth_device *d, DBusMessageIter *i) { + const char *key; + DBusMessageIter variant_i; + + pa_assert(d); + + key = check_variant_property(i); + if (key == NULL) { + pa_log_error("Received invalid property for device %s", d->path); + return; + } + + dbus_message_iter_recurse(i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + + case DBUS_TYPE_STRING: { + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "Alias")) { + pa_xfree(d->alias); + d->alias = pa_xstrdup(value); + pa_log_debug("%s: %s", key, value); + } else if (pa_streq(key, "Address")) { + if (d->properties_received) { + pa_log_warn("Device property 'Address' expected to be constant but changed for %s, ignoring", d->path); + return; + } + + if (d->address) { + pa_log_warn("Device %s: Received a duplicate 'Address' property, ignoring", d->path); + return; + } + + d->address = pa_xstrdup(value); + pa_log_debug("%s: %s", key, value); + } + + break; + } + + case DBUS_TYPE_OBJECT_PATH: { + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "Adapter")) { + + if (d->properties_received) { + pa_log_warn("Device property 'Adapter' expected to be constant but changed for %s, ignoring", d->path); + return; + } + + if (d->adapter_path) { + pa_log_warn("Device %s: Received a duplicate 'Adapter' property, ignoring", d->path); + return; + } + + d->adapter_path = pa_xstrdup(value); + pa_log_debug("%s: %s", key, value); + } + + break; + } + + case DBUS_TYPE_UINT32: { + uint32_t value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (pa_streq(key, "Class")) { + d->class_of_device = value; + pa_log_debug("%s: %d", key, value); + } + + break; + } + + case DBUS_TYPE_ARRAY: { + DBusMessageIter ai; + dbus_message_iter_recurse(&variant_i, &ai); + + if (dbus_message_iter_get_arg_type(&ai) == DBUS_TYPE_STRING && pa_streq(key, "UUIDs")) { + /* bluetoothd never removes UUIDs from a device object so we + * don't need to check for disappeared UUIDs here. */ + while (dbus_message_iter_get_arg_type(&ai) != DBUS_TYPE_INVALID) { + const char *value; + char *uuid; + + dbus_message_iter_get_basic(&ai, &value); + + if (pa_hashmap_get(d->uuids, value)) { + dbus_message_iter_next(&ai); + continue; + } + + uuid = pa_xstrdup(value); + pa_hashmap_put(d->uuids, uuid, uuid); + + pa_log_debug("%s: %s", key, value); + dbus_message_iter_next(&ai); + } + } + + break; + } + } +} + +static void parse_device_properties(pa_bluetooth_device *d, DBusMessageIter *i) { + DBusMessageIter element_i; + + dbus_message_iter_recurse(i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&element_i, &dict_i); + parse_device_property(d, &dict_i); + dbus_message_iter_next(&element_i); + } + + if (!d->properties_received) { + d->properties_received = true; + device_update_valid(d); + + if (!d->address || !d->adapter_path || !d->alias) + pa_log_error("Non-optional information missing for device %s", d->path); + } +} + +static void parse_adapter_properties(pa_bluetooth_adapter *a, DBusMessageIter *i, bool is_property_change) { + DBusMessageIter element_i; + + pa_assert(a); + + dbus_message_iter_recurse(i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + key = check_variant_property(&dict_i); + if (key == NULL) { + pa_log_error("Received invalid property for adapter %s", a->path); + return; + } + + dbus_message_iter_recurse(&dict_i, &variant_i); + + if (dbus_message_iter_get_arg_type(&variant_i) == DBUS_TYPE_STRING && pa_streq(key, "Address")) { + const char *value; + + if (is_property_change) { + pa_log_warn("Adapter property 'Address' expected to be constant but changed for %s, ignoring", a->path); + return; + } + + if (a->address) { + pa_log_warn("Adapter %s received a duplicate 'Address' property, ignoring", a->path); + return; + } + + dbus_message_iter_get_basic(&variant_i, &value); + a->address = pa_xstrdup(value); + a->valid = true; + } + + dbus_message_iter_next(&element_i); + } +} + +static void register_endpoint_reply(DBusPendingCall *pending, void *userdata) { + DBusMessage *r; + pa_dbus_pending *p; + pa_bluetooth_discovery *y; + char *endpoint; + + pa_assert(pending); + pa_assert_se(p = userdata); + pa_assert_se(y = p->context_data); + pa_assert_se(endpoint = p->call_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + pa_log_info("Couldn't register endpoint %s because it is disabled in BlueZ", endpoint); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error(BLUEZ_MEDIA_INTERFACE ".RegisterEndpoint() failed: %s: %s", dbus_message_get_error_name(r), + pa_dbus_get_error_message(r)); + goto finish; + } + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, y->pending, p); + pa_dbus_pending_free(p); + + pa_xfree(endpoint); +} + +static void register_endpoint(pa_bluetooth_discovery *y, const pa_a2dp_codec *a2dp_codec, const char *path, const char *endpoint, const char *uuid) { + DBusMessage *m; + DBusMessageIter i, d; + uint8_t capabilities[MAX_A2DP_CAPS_SIZE]; + size_t capabilities_size; + uint8_t codec_id; + + pa_log_debug("Registering %s on adapter %s", endpoint, path); + + codec_id = a2dp_codec->id.codec_id; + capabilities_size = a2dp_codec->fill_capabilities(capabilities); + pa_assert(capabilities_size != 0); + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, path, BLUEZ_MEDIA_INTERFACE, "RegisterEndpoint")); + + dbus_message_iter_init_append(m, &i); + pa_assert_se(dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &endpoint)); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING + DBUS_TYPE_VARIANT_AS_STRING DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d); + pa_dbus_append_basic_variant_dict_entry(&d, "UUID", DBUS_TYPE_STRING, &uuid); + pa_dbus_append_basic_variant_dict_entry(&d, "Codec", DBUS_TYPE_BYTE, &codec_id); + pa_dbus_append_basic_array_variant_dict_entry(&d, "Capabilities", DBUS_TYPE_BYTE, &capabilities, capabilities_size); + + dbus_message_iter_close_container(&i, &d); + + send_and_add_to_pending(y, m, register_endpoint_reply, pa_xstrdup(endpoint)); +} + +static void parse_interfaces_and_properties(pa_bluetooth_discovery *y, DBusMessageIter *dict_i) { + DBusMessageIter element_i; + const char *path; + void *state; + pa_bluetooth_device *d; + + pa_assert(dbus_message_iter_get_arg_type(dict_i) == DBUS_TYPE_OBJECT_PATH); + dbus_message_iter_get_basic(dict_i, &path); + + pa_assert_se(dbus_message_iter_next(dict_i)); + pa_assert(dbus_message_iter_get_arg_type(dict_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(dict_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter iface_i; + const char *interface; + + dbus_message_iter_recurse(&element_i, &iface_i); + + pa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_STRING); + dbus_message_iter_get_basic(&iface_i, &interface); + + pa_assert_se(dbus_message_iter_next(&iface_i)); + pa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + if (pa_streq(interface, BLUEZ_ADAPTER_INTERFACE)) { + + const pa_a2dp_codec *a2dp_codec_sbc; + pa_bluetooth_adapter *a; + + if ((a = pa_hashmap_get(y->adapters, path))) { + pa_log_error("Found duplicated D-Bus path for adapter %s", path); + return; + } else + a = adapter_create(y, path); + + pa_log_debug("Adapter %s found", path); + + parse_adapter_properties(a, &iface_i, false); + + if (!a->valid) + return; + + /* Currently only one A2DP codec is supported, so register only SBC + * Support for multiple codecs needs to use a new Bluez API which + * pulseaudio does not implement yet, patches are waiting in queue */ + a2dp_codec_sbc = pa_bluetooth_get_a2dp_codec("sbc"); + pa_assert(a2dp_codec_sbc); + register_endpoint(y, a2dp_codec_sbc, path, A2DP_SINK_ENDPOINT "/sbc", PA_BLUETOOTH_UUID_A2DP_SINK); + register_endpoint(y, a2dp_codec_sbc, path, A2DP_SOURCE_ENDPOINT "/sbc", PA_BLUETOOTH_UUID_A2DP_SOURCE); + + } else if (pa_streq(interface, BLUEZ_DEVICE_INTERFACE)) { + + if ((d = pa_hashmap_get(y->devices, path))) { + if (d->properties_received) { + pa_log_error("Found duplicated D-Bus path for device %s", path); + return; + } + } else + d = device_create(y, path); + + pa_log_debug("Device %s found", d->path); + + parse_device_properties(d, &iface_i); + + } else + pa_log_debug("Unknown interface %s found, skipping", interface); + + dbus_message_iter_next(&element_i); + } + + PA_HASHMAP_FOREACH(d, y->devices, state) { + if (d->properties_received && !d->tried_to_link_with_adapter) { + if (d->adapter_path) { + device_set_adapter(d, pa_hashmap_get(d->discovery->adapters, d->adapter_path)); + + if (!d->adapter) + pa_log("Device %s points to a nonexistent adapter %s.", d->path, d->adapter_path); + else if (!d->adapter->valid) + pa_log("Device %s points to an invalid adapter %s.", d->path, d->adapter_path); + } + + d->tried_to_link_with_adapter = true; + } + } + + return; +} + +void pa_bluetooth_discovery_set_ofono_running(pa_bluetooth_discovery *y, bool is_running) { + pa_assert(y); + + pa_log_debug("oFono is running: %s", pa_yes_no(is_running)); + if (y->headset_backend != HEADSET_BACKEND_AUTO) + return; + + /* If ofono starts running, all devices that might be connected to the HS role + * need to be disconnected, so that the devices can be handled by ofono */ + if (is_running) { + void *state; + pa_bluetooth_device *d; + + PA_HASHMAP_FOREACH(d, y->devices, state) { + if (device_supports_profile(d, PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY)) { + DBusMessage *m; + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, d->path, "org.bluez.Device1", "Disconnect")); + dbus_message_set_no_reply(m, true); + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(y->connection), m, NULL)); + dbus_message_unref(m); + } + } + } + + pa_bluetooth_native_backend_enable_hs_role(y->native_backend, !is_running); +} + +static void get_managed_objects_reply(DBusPendingCall *pending, void *userdata) { + pa_dbus_pending *p; + pa_bluetooth_discovery *y; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + + pa_assert_se(p = userdata); + pa_assert_se(y = p->context_data); + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + pa_log_warn("BlueZ D-Bus ObjectManager not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + pa_log_error("GetManagedObjects() failed: %s: %s", dbus_message_get_error_name(r), pa_dbus_get_error_message(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !pa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) { + pa_log_error("Invalid reply signature for GetManagedObjects()"); + goto finish; + } + + dbus_message_iter_recurse(&arg_i, &element_i); + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&element_i, &dict_i); + + parse_interfaces_and_properties(y, &dict_i); + + dbus_message_iter_next(&element_i); + } + + y->objects_listed = true; + + if (!y->native_backend && y->headset_backend != HEADSET_BACKEND_OFONO) + y->native_backend = pa_bluetooth_native_backend_new(y->core, y, (y->headset_backend == HEADSET_BACKEND_NATIVE)); + if (!y->ofono_backend && y->headset_backend != HEADSET_BACKEND_NATIVE) + y->ofono_backend = pa_bluetooth_ofono_backend_new(y->core, y); + +finish: + dbus_message_unref(r); + + PA_LLIST_REMOVE(pa_dbus_pending, y->pending, p); + pa_dbus_pending_free(p); +} + +static void get_managed_objects(pa_bluetooth_discovery *y) { + DBusMessage *m; + + pa_assert(y); + + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, "/", "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects")); + send_and_add_to_pending(y, m, get_managed_objects_reply, NULL); +} + +pa_hook* pa_bluetooth_discovery_hook(pa_bluetooth_discovery *y, pa_bluetooth_hook_t hook) { + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + + return &y->hooks[hook]; +} + +static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y; + DBusError err; + + pa_assert(bus); + pa_assert(m); + pa_assert_se(y = userdata); + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + pa_log_error("Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto fail; + } + + if (pa_streq(name, BLUEZ_SERVICE)) { + if (old_owner && *old_owner) { + pa_log_debug("Bluetooth daemon disappeared"); + pa_hashmap_remove_all(y->devices); + pa_hashmap_remove_all(y->adapters); + y->objects_listed = false; + if (y->ofono_backend) { + pa_bluetooth_ofono_backend_free(y->ofono_backend); + y->ofono_backend = NULL; + } + if (y->native_backend) { + pa_bluetooth_native_backend_free(y->native_backend); + y->native_backend = NULL; + } + } + + if (new_owner && *new_owner) { + pa_log_debug("Bluetooth daemon appeared"); + get_managed_objects(y); + } + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded")) { + DBusMessageIter arg_i; + + if (!y->objects_listed) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; /* No reply received yet from GetManagedObjects */ + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) { + pa_log_error("Invalid signature found in InterfacesAdded"); + goto fail; + } + + parse_interfaces_and_properties(y, &arg_i); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesRemoved")) { + const char *p; + DBusMessageIter arg_i; + DBusMessageIter element_i; + + if (!y->objects_listed) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; /* No reply received yet from GetManagedObjects */ + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oas")) { + pa_log_error("Invalid signature found in InterfacesRemoved"); + goto fail; + } + + dbus_message_iter_get_basic(&arg_i, &p); + + pa_assert_se(dbus_message_iter_next(&arg_i)); + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) { + const char *iface; + + dbus_message_iter_get_basic(&element_i, &iface); + + if (pa_streq(iface, BLUEZ_DEVICE_INTERFACE)) + device_remove(y, p); + else if (pa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) + adapter_remove(y, p); + + dbus_message_iter_next(&element_i); + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.Properties", "PropertiesChanged")) { + DBusMessageIter arg_i; + const char *iface; + + if (!y->objects_listed) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; /* No reply received yet from GetManagedObjects */ + + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "sa{sv}as")) { + pa_log_error("Invalid signature found in PropertiesChanged"); + goto fail; + } + + dbus_message_iter_get_basic(&arg_i, &iface); + + pa_assert_se(dbus_message_iter_next(&arg_i)); + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + if (pa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) { + pa_bluetooth_adapter *a; + + pa_log_debug("Properties changed in adapter %s", dbus_message_get_path(m)); + + if (!(a = pa_hashmap_get(y->adapters, dbus_message_get_path(m)))) { + pa_log_warn("Properties changed in unknown adapter"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + parse_adapter_properties(a, &arg_i, true); + + } else if (pa_streq(iface, BLUEZ_DEVICE_INTERFACE)) { + pa_bluetooth_device *d; + + pa_log_debug("Properties changed in device %s", dbus_message_get_path(m)); + + if (!(d = pa_hashmap_get(y->devices, dbus_message_get_path(m)))) { + pa_log_warn("Properties changed in unknown device"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (!d->properties_received) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + parse_device_properties(d, &arg_i); + } else if (pa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) { + pa_bluetooth_transport *t; + + pa_log_debug("Properties changed in transport %s", dbus_message_get_path(m)); + + if (!(t = pa_hashmap_get(y->transports, dbus_message_get_path(m)))) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + parse_transport_properties(t, &arg_i); + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + +fail: + dbus_error_free(&err); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile) { + switch(profile) { + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + return "a2dp_sink"; + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + return "a2dp_source"; + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + return "headset_head_unit"; + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + return "headset_audio_gateway"; + case PA_BLUETOOTH_PROFILE_OFF: + return "off"; + } + + return NULL; +} + +static const pa_a2dp_codec *a2dp_endpoint_to_a2dp_codec(const char *endpoint) { + const char *codec_name; + + if (pa_startswith(endpoint, A2DP_SINK_ENDPOINT "/")) + codec_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/"); + else if (pa_startswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) + codec_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/"); + else + return NULL; + + return pa_bluetooth_get_a2dp_codec(codec_name); +} + +static DBusMessage *endpoint_set_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y = userdata; + pa_bluetooth_device *d; + pa_bluetooth_transport *t; + const pa_a2dp_codec *a2dp_codec = NULL; + const char *sender, *path, *endpoint_path, *dev_path = NULL, *uuid = NULL; + const uint8_t *config = NULL; + int size = 0; + pa_bluetooth_profile_t p = PA_BLUETOOTH_PROFILE_OFF; + DBusMessageIter args, props; + DBusMessage *r; + + if (!dbus_message_iter_init(m, &args) || !pa_streq(dbus_message_get_signature(m), "oa{sv}")) { + pa_log_error("Invalid signature for method SetConfiguration()"); + goto fail2; + } + + dbus_message_iter_get_basic(&args, &path); + + if (pa_hashmap_get(y->transports, path)) { + pa_log_error("Endpoint SetConfiguration(): Transport %s is already configured.", path); + goto fail2; + } + + pa_assert_se(dbus_message_iter_next(&args)); + + dbus_message_iter_recurse(&args, &props); + if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY) + goto fail; + + endpoint_path = dbus_message_get_path(m); + + /* Read transport properties */ + while (dbus_message_iter_get_arg_type(&props) == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter value, entry; + int var; + + dbus_message_iter_recurse(&props, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + var = dbus_message_iter_get_arg_type(&value); + + if (pa_streq(key, "UUID")) { + if (var != DBUS_TYPE_STRING) { + pa_log_error("Property %s of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_get_basic(&value, &uuid); + + if (pa_startswith(endpoint_path, A2DP_SINK_ENDPOINT "/")) + p = PA_BLUETOOTH_PROFILE_A2DP_SOURCE; + else if (pa_startswith(endpoint_path, A2DP_SOURCE_ENDPOINT "/")) + p = PA_BLUETOOTH_PROFILE_A2DP_SINK; + + if ((pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE) && p != PA_BLUETOOTH_PROFILE_A2DP_SINK) || + (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK) && p != PA_BLUETOOTH_PROFILE_A2DP_SOURCE)) { + pa_log_error("UUID %s of transport %s incompatible with endpoint %s", uuid, path, endpoint_path); + goto fail; + } + } else if (pa_streq(key, "Device")) { + if (var != DBUS_TYPE_OBJECT_PATH) { + pa_log_error("Property %s of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_get_basic(&value, &dev_path); + } else if (pa_streq(key, "Configuration")) { + DBusMessageIter array; + + if (var != DBUS_TYPE_ARRAY) { + pa_log_error("Property %s of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_recurse(&value, &array); + var = dbus_message_iter_get_arg_type(&array); + if (var != DBUS_TYPE_BYTE) { + pa_log_error("%s is an array of wrong type %c", key, (char)var); + goto fail; + } + + dbus_message_iter_get_fixed_array(&array, &config, &size); + + a2dp_codec = a2dp_endpoint_to_a2dp_codec(endpoint_path); + pa_assert(a2dp_codec); + + if (!a2dp_codec->is_configuration_valid(config, size)) + goto fail; + } + + dbus_message_iter_next(&props); + } + + if (!a2dp_codec) + goto fail2; + + if ((d = pa_hashmap_get(y->devices, dev_path))) { + if (!d->valid) { + pa_log_error("Information about device %s is invalid", dev_path); + goto fail2; + } + } else { + /* InterfacesAdded signal is probably on its way, device_info_valid is kept as 0. */ + pa_log_warn("SetConfiguration() received for unknown device %s", dev_path); + d = device_create(y, dev_path); + } + + if (d->transports[p] != NULL) { + pa_log_error("Cannot configure transport %s because profile %s is already used", path, pa_bluetooth_profile_to_string(p)); + goto fail2; + } + + sender = dbus_message_get_sender(m); + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(y->connection), r, NULL)); + dbus_message_unref(r); + + t = pa_bluetooth_transport_new(d, sender, path, p, config, size); + t->a2dp_codec = a2dp_codec; + t->acquire = bluez5_transport_acquire_cb; + t->release = bluez5_transport_release_cb; + pa_bluetooth_transport_put(t); + + pa_log_debug("Transport %s available for profile %s", t->path, pa_bluetooth_profile_to_string(t->profile)); + + return NULL; + +fail: + pa_log_error("Endpoint SetConfiguration(): invalid arguments"); + +fail2: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to set configuration")); + return r; +} + +static DBusMessage *endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y = userdata; + const char *endpoint_path; + uint8_t *cap; + int size; + const pa_a2dp_codec *a2dp_codec; + uint8_t config[MAX_A2DP_CAPS_SIZE]; + uint8_t *config_ptr = config; + size_t config_size; + DBusMessage *r; + DBusError err; + + endpoint_path = dbus_message_get_path(m); + + dbus_error_init(&err); + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &cap, &size, DBUS_TYPE_INVALID)) { + pa_log_error("Endpoint SelectConfiguration(): %s", err.message); + dbus_error_free(&err); + goto fail; + } + + a2dp_codec = a2dp_endpoint_to_a2dp_codec(endpoint_path); + pa_assert(a2dp_codec); + + config_size = a2dp_codec->fill_preferred_configuration(&y->core->default_sample_spec, cap, size, config); + if (config_size == 0) + goto fail; + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &config_ptr, config_size, DBUS_TYPE_INVALID)); + + return r; + +fail: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to select configuration")); + return r; +} + +static DBusMessage *endpoint_clear_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) { + pa_bluetooth_discovery *y = userdata; + pa_bluetooth_transport *t; + DBusMessage *r; + DBusError err; + const char *path; + + dbus_error_init(&err); + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) { + pa_log_error("Endpoint ClearConfiguration(): %s", err.message); + dbus_error_free(&err); + goto fail; + } + + if ((t = pa_hashmap_get(y->transports, path))) { + pa_log_debug("Clearing transport %s profile %s", t->path, pa_bluetooth_profile_to_string(t->profile)); + pa_bluetooth_transport_free(t); + } + + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; + +fail: + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to clear configuration")); + return r; +} + +static DBusMessage *endpoint_release(DBusConnection *conn, DBusMessage *m, void *userdata) { + DBusMessage *r = NULL; + + /* From doc/media-api.txt in bluez: + * + * This method gets called when the service daemon + * unregisters the endpoint. An endpoint can use it to do + * cleanup tasks. There is no need to unregister the + * endpoint, because when this method gets called it has + * already been unregistered. + * + * We don't have any cleanup to do. */ + + /* Reply only if requested. Generally bluetoothd doesn't request a reply + * to the Release() call. Sending replies when not requested on the system + * bus tends to cause errors in syslog from dbus-daemon, because it + * doesn't let unexpected replies through, so it's important to have this + * check here. */ + if (!dbus_message_get_no_reply(m)) + pa_assert_se(r = dbus_message_new_method_return(m)); + + return r; +} + +static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata) { + struct pa_bluetooth_discovery *y = userdata; + DBusMessage *r = NULL; + const char *path, *interface, *member; + + pa_assert(y); + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (!a2dp_endpoint_to_a2dp_codec(path)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = ENDPOINT_INTROSPECT_XML; + + pa_assert_se(r = dbus_message_new_method_return(m)); + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)); + + } else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration")) + r = endpoint_set_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectConfiguration")) + r = endpoint_select_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration")) + r = endpoint_clear_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Release")) + r = endpoint_release(c, m, userdata); + else + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (r) { + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(y->connection), r, NULL)); + dbus_message_unref(r); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void endpoint_init(pa_bluetooth_discovery *y, const char *endpoint) { + static const DBusObjectPathVTable vtable_endpoint = { + .message_function = endpoint_handler, + }; + + pa_assert(y); + pa_assert(endpoint); + + pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(y->connection), endpoint, + &vtable_endpoint, y)); +} + +static void endpoint_done(pa_bluetooth_discovery *y, const char *endpoint) { + pa_assert(y); + pa_assert(endpoint); + + dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), endpoint); +} + +pa_bluetooth_discovery* pa_bluetooth_discovery_get(pa_core *c, int headset_backend) { + pa_bluetooth_discovery *y; + DBusError err; + DBusConnection *conn; + unsigned i, count; + const pa_a2dp_codec *a2dp_codec; + char *endpoint; + + y = pa_xnew0(pa_bluetooth_discovery, 1); + PA_REFCNT_INIT(y); + y->core = c; + y->headset_backend = headset_backend; + y->adapters = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) adapter_free); + y->devices = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) device_free); + y->transports = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + PA_LLIST_HEAD_INIT(pa_dbus_pending, y->pending); + + for (i = 0; i < PA_BLUETOOTH_HOOK_MAX; i++) + pa_hook_init(&y->hooks[i], y); + + pa_shared_set(c, "bluetooth-discovery", y); + + dbus_error_init(&err); + + if (!(y->connection = pa_dbus_bus_get(y->core, DBUS_BUS_SYSTEM, &err))) { + pa_log_error("Failed to get D-Bus connection: %s", err.message); + goto fail; + } + + conn = pa_dbus_connection_get(y->connection); + + /* dynamic detection of bluetooth audio devices */ + if (!dbus_connection_add_filter(conn, filter_cb, y, NULL)) { + pa_log_error("Failed to add filter function"); + goto fail; + } + y->filter_added = true; + + if (pa_dbus_add_matches(conn, &err, + "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'" + ",arg0='" BLUEZ_SERVICE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager'," + "member='InterfacesRemoved'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_ADAPTER_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_DEVICE_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + ",arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", + NULL) < 0) { + pa_log_error("Failed to add D-Bus matches: %s", err.message); + goto fail; + } + y->matches_added = true; + + count = pa_bluetooth_a2dp_codec_count(); + for (i = 0; i < count; i++) { + a2dp_codec = pa_bluetooth_a2dp_codec_iter(i); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SINK_ENDPOINT, a2dp_codec->name); + endpoint_init(y, endpoint); + pa_xfree(endpoint); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SOURCE_ENDPOINT, a2dp_codec->name); + endpoint_init(y, endpoint); + pa_xfree(endpoint); + } + + get_managed_objects(y); + + return y; + +fail: + pa_bluetooth_discovery_unref(y); + dbus_error_free(&err); + + return NULL; +} + +pa_bluetooth_discovery* pa_bluetooth_discovery_ref(pa_bluetooth_discovery *y) { + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + + PA_REFCNT_INC(y); + + return y; +} + +void pa_bluetooth_discovery_unref(pa_bluetooth_discovery *y) { + unsigned i, count; + const pa_a2dp_codec *a2dp_codec; + char *endpoint; + + pa_assert(y); + pa_assert(PA_REFCNT_VALUE(y) > 0); + + if (PA_REFCNT_DEC(y) > 0) + return; + + pa_dbus_free_pending_list(&y->pending); + + if (y->ofono_backend) + pa_bluetooth_ofono_backend_free(y->ofono_backend); + if (y->native_backend) + pa_bluetooth_native_backend_free(y->native_backend); + + if (y->adapters) + pa_hashmap_free(y->adapters); + + if (y->devices) + pa_hashmap_free(y->devices); + + if (y->transports) { + pa_assert(pa_hashmap_isempty(y->transports)); + pa_hashmap_free(y->transports); + } + + if (y->connection) { + + if (y->matches_added) + pa_dbus_remove_matches(pa_dbus_connection_get(y->connection), + "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" BLUEZ_SERVICE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager'," + "member='InterfacesAdded'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.ObjectManager'," + "member='InterfacesRemoved'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_ADAPTER_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_DEVICE_INTERFACE "'", + "type='signal',sender='" BLUEZ_SERVICE "',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", + NULL); + + if (y->filter_added) + dbus_connection_remove_filter(pa_dbus_connection_get(y->connection), filter_cb, y); + + count = pa_bluetooth_a2dp_codec_count(); + for (i = 0; i < count; i++) { + a2dp_codec = pa_bluetooth_a2dp_codec_iter(i); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SINK_ENDPOINT, a2dp_codec->name); + endpoint_done(y, endpoint); + pa_xfree(endpoint); + + endpoint = pa_sprintf_malloc("%s/%s", A2DP_SOURCE_ENDPOINT, a2dp_codec->name); + endpoint_done(y, endpoint); + pa_xfree(endpoint); + } + + pa_dbus_connection_unref(y->connection); + } + + pa_shared_remove(y->core, "bluetooth-discovery"); + pa_xfree(y); +} diff --git a/src/modules/bluetooth/bluez5-util.h b/src/modules/bluetooth/bluez5-util.h new file mode 100644 index 0000000..ff172e0 --- /dev/null +++ b/src/modules/bluetooth/bluez5-util.h @@ -0,0 +1,185 @@ +#ifndef foobluez5utilhfoo +#define foobluez5utilhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2008-2013 João Paulo Rechi Vita + Copyrigth 2018-2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#include <pulsecore/core.h> + +#include "a2dp-codec-util.h" + +#define PA_BLUETOOTH_UUID_A2DP_SOURCE "0000110a-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_A2DP_SINK "0000110b-0000-1000-8000-00805f9b34fb" + +/* There are two HSP HS UUIDs. The first one (older?) is used both as the HSP + * profile identifier and as the HS role identifier, while the second one is + * only used to identify the role. As far as PulseAudio is concerned, the two + * UUIDs mean exactly the same thing. */ +#define PA_BLUETOOTH_UUID_HSP_HS "00001108-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_HSP_HS_ALT "00001131-0000-1000-8000-00805f9b34fb" + +#define PA_BLUETOOTH_UUID_HSP_AG "00001112-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_HFP_HF "0000111e-0000-1000-8000-00805f9b34fb" +#define PA_BLUETOOTH_UUID_HFP_AG "0000111f-0000-1000-8000-00805f9b34fb" + +typedef struct pa_bluetooth_transport pa_bluetooth_transport; +typedef struct pa_bluetooth_device pa_bluetooth_device; +typedef struct pa_bluetooth_adapter pa_bluetooth_adapter; +typedef struct pa_bluetooth_discovery pa_bluetooth_discovery; +typedef struct pa_bluetooth_backend pa_bluetooth_backend; + +typedef enum pa_bluetooth_hook { + PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED, /* Call data: pa_bluetooth_device */ + PA_BLUETOOTH_HOOK_DEVICE_UNLINK, /* Call data: pa_bluetooth_device */ + PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED, /* Call data: pa_bluetooth_transport */ + PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED, /* Call data: pa_bluetooth_transport */ + PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED, /* Call data: pa_bluetooth_transport */ + PA_BLUETOOTH_HOOK_MAX +} pa_bluetooth_hook_t; + +typedef enum profile { + PA_BLUETOOTH_PROFILE_A2DP_SINK, + PA_BLUETOOTH_PROFILE_A2DP_SOURCE, + PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT, + PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY, + PA_BLUETOOTH_PROFILE_OFF +} pa_bluetooth_profile_t; +#define PA_BLUETOOTH_PROFILE_COUNT PA_BLUETOOTH_PROFILE_OFF + +typedef enum pa_bluetooth_transport_state { + PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED, + PA_BLUETOOTH_TRANSPORT_STATE_IDLE, + PA_BLUETOOTH_TRANSPORT_STATE_PLAYING +} pa_bluetooth_transport_state_t; + +typedef int (*pa_bluetooth_transport_acquire_cb)(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu); +typedef void (*pa_bluetooth_transport_release_cb)(pa_bluetooth_transport *t); +typedef void (*pa_bluetooth_transport_destroy_cb)(pa_bluetooth_transport *t); +typedef void (*pa_bluetooth_transport_set_speaker_gain_cb)(pa_bluetooth_transport *t, uint16_t gain); +typedef void (*pa_bluetooth_transport_set_microphone_gain_cb)(pa_bluetooth_transport *t, uint16_t gain); + +struct pa_bluetooth_transport { + pa_bluetooth_device *device; + + char *owner; + char *path; + pa_bluetooth_profile_t profile; + + uint8_t codec; + uint8_t *config; + size_t config_size; + + const pa_a2dp_codec *a2dp_codec; + + uint16_t microphone_gain; + uint16_t speaker_gain; + + pa_bluetooth_transport_state_t state; + + pa_bluetooth_transport_acquire_cb acquire; + pa_bluetooth_transport_release_cb release; + pa_bluetooth_transport_destroy_cb destroy; + pa_bluetooth_transport_set_speaker_gain_cb set_speaker_gain; + pa_bluetooth_transport_set_microphone_gain_cb set_microphone_gain; + void *userdata; +}; + +struct pa_bluetooth_device { + pa_bluetooth_discovery *discovery; + pa_bluetooth_adapter *adapter; + + bool properties_received; + bool tried_to_link_with_adapter; + bool valid; + bool autodetect_mtu; + + /* Device information */ + char *path; + char *adapter_path; + char *alias; + char *address; + uint32_t class_of_device; + pa_hashmap *uuids; /* char* -> char* (hashmap-as-a-set) */ + + pa_bluetooth_transport *transports[PA_BLUETOOTH_PROFILE_COUNT]; + + pa_time_event *wait_for_profiles_timer; +}; + +struct pa_bluetooth_adapter { + pa_bluetooth_discovery *discovery; + char *path; + char *address; + + bool valid; +}; + +#ifdef HAVE_BLUEZ_5_OFONO_HEADSET +pa_bluetooth_backend *pa_bluetooth_ofono_backend_new(pa_core *c, pa_bluetooth_discovery *y); +void pa_bluetooth_ofono_backend_free(pa_bluetooth_backend *b); +#else +static inline pa_bluetooth_backend *pa_bluetooth_ofono_backend_new(pa_core *c, pa_bluetooth_discovery *y) { + return NULL; +} +static inline void pa_bluetooth_ofono_backend_free(pa_bluetooth_backend *b) {} +#endif + +#ifdef HAVE_BLUEZ_5_NATIVE_HEADSET +pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role); +void pa_bluetooth_native_backend_free(pa_bluetooth_backend *b); +void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *b, bool enable_hs_role); +#else +static inline pa_bluetooth_backend *pa_bluetooth_native_backend_new(pa_core *c, pa_bluetooth_discovery *y, bool enable_hs_role) { + return NULL; +} +static inline void pa_bluetooth_native_backend_free(pa_bluetooth_backend *b) {} +static inline void pa_bluetooth_native_backend_enable_hs_role(pa_bluetooth_backend *b, bool enable_hs_role) {} +#endif + +pa_bluetooth_transport *pa_bluetooth_transport_new(pa_bluetooth_device *d, const char *owner, const char *path, + pa_bluetooth_profile_t p, const uint8_t *config, size_t size); + +void pa_bluetooth_transport_set_state(pa_bluetooth_transport *t, pa_bluetooth_transport_state_t state); +void pa_bluetooth_transport_put(pa_bluetooth_transport *t); +void pa_bluetooth_transport_unlink(pa_bluetooth_transport *t); +void pa_bluetooth_transport_free(pa_bluetooth_transport *t); + +bool pa_bluetooth_device_any_transport_connected(const pa_bluetooth_device *d); + +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_path(pa_bluetooth_discovery *y, const char *path); +pa_bluetooth_device* pa_bluetooth_discovery_get_device_by_address(pa_bluetooth_discovery *y, const char *remote, const char *local); + +pa_hook* pa_bluetooth_discovery_hook(pa_bluetooth_discovery *y, pa_bluetooth_hook_t hook); + +const char *pa_bluetooth_profile_to_string(pa_bluetooth_profile_t profile); + +static inline bool pa_bluetooth_uuid_is_hsp_hs(const char *uuid) { + return pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_HS) || pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_HS_ALT); +} + +#define HEADSET_BACKEND_OFONO 0 +#define HEADSET_BACKEND_NATIVE 1 +#define HEADSET_BACKEND_AUTO 2 + +pa_bluetooth_discovery* pa_bluetooth_discovery_get(pa_core *core, int headset_backend); +pa_bluetooth_discovery* pa_bluetooth_discovery_ref(pa_bluetooth_discovery *y); +void pa_bluetooth_discovery_unref(pa_bluetooth_discovery *y); +void pa_bluetooth_discovery_set_ofono_running(pa_bluetooth_discovery *y, bool is_running); +#endif diff --git a/src/modules/bluetooth/meson.build b/src/modules/bluetooth/meson.build new file mode 100644 index 0000000..9982cba --- /dev/null +++ b/src/modules/bluetooth/meson.build @@ -0,0 +1,33 @@ +libbluez5_util_sources = [ + 'a2dp-codec-sbc.c', + 'a2dp-codec-util.c', + 'bluez5-util.c', +] + +libbluez5_util_headers = [ + 'a2dp-codec-api.h', + 'a2dp-codecs.h', + 'a2dp-codec-util.h', + 'bluez5-util.h', + 'rtp.h', +] + +if get_option('bluez5-native-headset') + libbluez5_util_sources += [ 'backend-native.c' ] +endif + +if get_option('bluez5-ofono-headset') + libbluez5_util_sources += [ 'backend-ofono.c' ] +endif + +libbluez5_util = shared_library('bluez5-util', + libbluez5_util_sources, + libbluez5_util_headers, + c_args : [pa_c_args, server_c_args], + link_args : [nodelete_link_args], + include_directories : [configinc, topinc], + dependencies : [libpulse_dep, libpulsecommon_dep, libpulsecore_dep, dbus_dep, sbc_dep, libintl_dep], + install : true, + install_rpath : privlibdir, + install_dir : modlibexecdir, +) diff --git a/src/modules/bluetooth/module-bluetooth-discover.c b/src/modules/bluetooth/module-bluetooth-discover.c new file mode 100644 index 0000000..cf8c7ee --- /dev/null +++ b/src/modules/bluetooth/module-bluetooth-discover.c @@ -0,0 +1,76 @@ +/*** + This file is part of PulseAudio. + + Copyright 2013 João Paulo Rechi Vita + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulsecore/core-util.h> +#include <pulsecore/macro.h> +#include <pulsecore/module.h> + +PA_MODULE_AUTHOR("João Paulo Rechi Vita"); +PA_MODULE_DESCRIPTION("Detect available Bluetooth daemon and load the corresponding discovery module"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(true); +PA_MODULE_USAGE( + "headset=ofono|native|auto" + "autodetect_mtu=<boolean>" +); + +struct userdata { + uint32_t bluez5_module_idx; +}; + +int pa__init(pa_module* m) { + struct userdata *u; + pa_module *mm; + + pa_assert(m); + + m->userdata = u = pa_xnew0(struct userdata, 1); + u->bluez5_module_idx = PA_INVALID_INDEX; + + if (pa_module_exists("module-bluez5-discover")) { + pa_module_load(&mm, m->core, "module-bluez5-discover", m->argument); + if (mm) + u->bluez5_module_idx = mm->index; + } + + if (u->bluez5_module_idx == PA_INVALID_INDEX) { + pa_xfree(u); + return -1; + } + + return 0; +} + +void pa__done(pa_module* m) { + struct userdata *u; + + pa_assert(m); + + if (!(u = m->userdata)) + return; + + if (u->bluez5_module_idx != PA_INVALID_INDEX) + pa_module_unload_by_index(m->core, u->bluez5_module_idx, true); + + pa_xfree(u); +} diff --git a/src/modules/bluetooth/module-bluetooth-policy.c b/src/modules/bluetooth/module-bluetooth-policy.c new file mode 100644 index 0000000..ffaa140 --- /dev/null +++ b/src/modules/bluetooth/module-bluetooth-policy.c @@ -0,0 +1,517 @@ +/*** + This file is part of PulseAudio. + + Copyright 2006 Lennart Poettering + Copyright 2009 Canonical Ltd + Copyright (C) 2012 Intel Corporation + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulse/xmalloc.h> + +#include <pulsecore/core.h> +#include <pulsecore/modargs.h> +#include <pulsecore/source-output.h> +#include <pulsecore/source.h> +#include <pulsecore/core-util.h> + +PA_MODULE_AUTHOR("Frédéric Dalleau, Pali Rohár"); +PA_MODULE_DESCRIPTION("Policy module to make using bluetooth devices out-of-the-box easier"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(true); +PA_MODULE_USAGE( + "auto_switch=<Switch between hsp and a2dp profile? (0 - never, 1 - media.role=phone, 2 - heuristic> " + "a2dp_source=<Handle a2dp_source card profile (sink role)?> " + "ag=<Handle headset_audio_gateway card profile (headset role)?> "); + +static const char* const valid_modargs[] = { + "auto_switch", + "a2dp_source", + "ag", + NULL +}; + +struct userdata { + uint32_t auto_switch; + bool enable_a2dp_source; + bool enable_ag; + pa_hook_slot *source_put_slot; + pa_hook_slot *sink_put_slot; + pa_hook_slot *source_output_put_slot; + pa_hook_slot *source_output_unlink_slot; + pa_hook_slot *card_init_profile_slot; + pa_hook_slot *card_unlink_slot; + pa_hook_slot *profile_available_changed_slot; + pa_hashmap *will_need_revert_card_map; +}; + +/* When a source is created, loopback it to default sink */ +static pa_hook_result_t source_put_hook_callback(pa_core *c, pa_source *source, void *userdata) { + struct userdata *u = userdata; + const char *s; + const char *role; + char *args; + pa_module *m = NULL; + + pa_assert(c); + pa_assert(source); + + /* Only consider bluetooth sinks and sources */ + s = pa_proplist_gets(source->proplist, PA_PROP_DEVICE_BUS); + if (!s) + return PA_HOOK_OK; + + if (!pa_streq(s, "bluetooth")) + return PA_HOOK_OK; + + s = pa_proplist_gets(source->proplist, "bluetooth.protocol"); + if (!s) + return PA_HOOK_OK; + + if (u->enable_a2dp_source && pa_streq(s, "a2dp_source")) + role = "music"; + else if (u->enable_ag && pa_streq(s, "headset_audio_gateway")) + role = "phone"; + else { + pa_log_debug("Profile %s cannot be selected for loopback", s); + return PA_HOOK_OK; + } + + /* Load module-loopback */ + args = pa_sprintf_malloc("source=\"%s\" source_dont_move=\"true\" sink_input_properties=\"media.role=%s\"", source->name, + role); + (void) pa_module_load(&m, c, "module-loopback", args); + pa_xfree(args); + + return PA_HOOK_OK; +} + +/* When a sink is created, loopback it to default source */ +static pa_hook_result_t sink_put_hook_callback(pa_core *c, pa_sink *sink, void *userdata) { + struct userdata *u = userdata; + const char *s; + const char *role; + char *args; + pa_module *m = NULL; + + pa_assert(c); + pa_assert(sink); + + /* Only consider bluetooth sinks and sources */ + s = pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_BUS); + if (!s) + return PA_HOOK_OK; + + if (!pa_streq(s, "bluetooth")) + return PA_HOOK_OK; + + s = pa_proplist_gets(sink->proplist, "bluetooth.protocol"); + if (!s) + return PA_HOOK_OK; + + if (u->enable_ag && pa_streq(s, "headset_audio_gateway")) + role = "phone"; + else { + pa_log_debug("Profile %s cannot be selected for loopback", s); + return PA_HOOK_OK; + } + + /* Load module-loopback */ + args = pa_sprintf_malloc("sink=\"%s\" sink_dont_move=\"true\" source_output_properties=\"media.role=%s\"", sink->name, + role); + (void) pa_module_load(&m, c, "module-loopback", args); + pa_xfree(args); + + return PA_HOOK_OK; +} + +static void card_set_profile(struct userdata *u, pa_card *card, bool revert_to_a2dp) +{ + pa_card_profile *profile; + void *state; + + /* Find available profile and activate it */ + PA_HASHMAP_FOREACH(profile, card->profiles, state) { + if (profile->available == PA_AVAILABLE_NO) + continue; + + /* Check for correct profile based on revert_to_a2dp */ + if (revert_to_a2dp) { + if (!pa_streq(profile->name, "a2dp_sink")) + continue; + } else { + if (!pa_streq(profile->name, "headset_head_unit")) + continue; + } + + pa_log_debug("Setting card '%s' to profile '%s'", card->name, profile->name); + + if (pa_card_set_profile(card, profile, false) != 0) { + pa_log_warn("Could not set profile '%s'", profile->name); + continue; + } + + /* When we are not in revert_to_a2dp phase flag this card for will_need_revert */ + if (!revert_to_a2dp) + pa_hashmap_put(u->will_need_revert_card_map, card, PA_INT_TO_PTR(1)); + + break; + } +} + +/* Switch profile for one card */ +static void switch_profile(pa_card *card, bool revert_to_a2dp, void *userdata) { + struct userdata *u = userdata; + const char *s; + + /* Only consider bluetooth cards */ + s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS); + if (!s || !pa_streq(s, "bluetooth")) + return; + + if (revert_to_a2dp) { + /* In revert_to_a2dp phase only consider cards with will_need_revert flag and remove it */ + if (!pa_hashmap_remove(u->will_need_revert_card_map, card)) + return; + + /* Skip card if does not have active hsp profile */ + if (!pa_streq(card->active_profile->name, "headset_head_unit")) + return; + + /* Skip card if already has active a2dp profile */ + if (pa_streq(card->active_profile->name, "a2dp_sink")) + return; + } else { + /* Skip card if does not have active a2dp profile */ + if (!pa_streq(card->active_profile->name, "a2dp_sink")) + return; + + /* Skip card if already has active hsp profile */ + if (pa_streq(card->active_profile->name, "headset_head_unit")) + return; + } + + card_set_profile(u, card, revert_to_a2dp); +} + +/* Return true if we should ignore this source output */ +static bool ignore_output(pa_source_output *source_output, void *userdata) { + struct userdata *u = userdata; + const char *s; + + /* New applications could set media.role for identifying streams */ + /* We are interested only in media.role=phone */ + s = pa_proplist_gets(source_output->proplist, PA_PROP_MEDIA_ROLE); + if (s) + return !pa_streq(s, "phone"); + + /* If media.role is not set use some heuristic (if enabled) */ + if (u->auto_switch != 2) + return true; + + /* Ignore if resample method is peaks (used by desktop volume programs) */ + if (pa_source_output_get_resample_method(source_output) == PA_RESAMPLER_PEAKS) + return true; + + /* Ignore if there is no client/application assigned (used by virtual stream) */ + if (!source_output->client) + return true; + + /* Ignore if recording from monitor of sink */ + if (source_output->direct_on_input) + return true; + + return false; +} + +static unsigned source_output_count(pa_core *c, void *userdata) { + pa_source_output *source_output; + uint32_t idx; + unsigned count = 0; + + PA_IDXSET_FOREACH(source_output, c->source_outputs, idx) + if (!ignore_output(source_output, userdata)) + ++count; + + return count; +} + +/* Switch profile for all cards */ +static void switch_profile_all(pa_idxset *cards, bool revert_to_a2dp, void *userdata) { + pa_card *card; + uint32_t idx; + + PA_IDXSET_FOREACH(card, cards, idx) + switch_profile(card, revert_to_a2dp, userdata); +} + +/* When a source output is created, switch profile a2dp to profile hsp */ +static pa_hook_result_t source_output_put_hook_callback(pa_core *c, pa_source_output *source_output, void *userdata) { + pa_assert(c); + pa_assert(source_output); + + if (ignore_output(source_output, userdata)) + return PA_HOOK_OK; + + switch_profile_all(c->cards, false, userdata); + return PA_HOOK_OK; +} + +/* When all source outputs are unlinked, switch profile hsp back back to profile a2dp */ +static pa_hook_result_t source_output_unlink_hook_callback(pa_core *c, pa_source_output *source_output, void *userdata) { + pa_assert(c); + pa_assert(source_output); + + if (ignore_output(source_output, userdata)) + return PA_HOOK_OK; + + /* If there are still some source outputs do nothing. */ + if (source_output_count(c, userdata) > 0) + return PA_HOOK_OK; + + switch_profile_all(c->cards, true, userdata); + return PA_HOOK_OK; +} + +static pa_hook_result_t card_init_profile_hook_callback(pa_core *c, pa_card *card, void *userdata) { + struct userdata *u = userdata; + const char *s; + + pa_assert(c); + pa_assert(card); + + if (source_output_count(c, userdata) == 0) + return PA_HOOK_OK; + + /* Only consider bluetooth cards */ + s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS); + if (!s || !pa_streq(s, "bluetooth")) + return PA_HOOK_OK; + + /* Ignore card if has already set other initial profile than a2dp */ + if (card->active_profile && + !pa_streq(card->active_profile->name, "a2dp_sink")) + return PA_HOOK_OK; + + /* Set initial profile to hsp */ + card_set_profile(u, card, false); + + /* Flag this card for will_need_revert */ + pa_hashmap_put(u->will_need_revert_card_map, card, PA_INT_TO_PTR(1)); + return PA_HOOK_OK; +} + +static pa_hook_result_t card_unlink_hook_callback(pa_core *c, pa_card *card, void *userdata) { + pa_assert(c); + pa_assert(card); + switch_profile(card, true, userdata); + return PA_HOOK_OK; +} + +static pa_card_profile *find_best_profile(pa_card *card) { + void *state; + pa_card_profile *profile; + pa_card_profile *result = card->active_profile; + + PA_HASHMAP_FOREACH(profile, card->profiles, state) { + if (profile->available == PA_AVAILABLE_NO) + continue; + + if (result == NULL || + (profile->available == PA_AVAILABLE_YES && result->available == PA_AVAILABLE_UNKNOWN) || + (profile->available == result->available && profile->priority > result->priority)) + result = profile; + } + + return result; +} + +static pa_hook_result_t profile_available_hook_callback(pa_core *c, pa_card_profile *profile, void *userdata) { + pa_card *card; + const char *s; + bool is_active_profile; + pa_card_profile *selected_profile; + + pa_assert(c); + pa_assert(profile); + pa_assert_se((card = profile->card)); + + /* Only consider bluetooth cards */ + s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_BUS); + if (!s || !pa_streq(s, "bluetooth")) + return PA_HOOK_OK; + + /* Do not automatically switch profiles for headsets, just in case */ + if (pa_streq(profile->name, "a2dp_sink") || pa_streq(profile->name, "headset_head_unit")) + return PA_HOOK_OK; + + is_active_profile = card->active_profile == profile; + + if (profile->available == PA_AVAILABLE_YES) { + if (is_active_profile) + return PA_HOOK_OK; + + if (card->active_profile->available == PA_AVAILABLE_YES && card->active_profile->priority >= profile->priority) + return PA_HOOK_OK; + + selected_profile = profile; + } else { + if (!is_active_profile) + return PA_HOOK_OK; + + pa_assert_se((selected_profile = find_best_profile(card))); + + if (selected_profile == card->active_profile) + return PA_HOOK_OK; + } + + pa_log_debug("Setting card '%s' to profile '%s'", card->name, selected_profile->name); + + if (pa_card_set_profile(card, selected_profile, false) != 0) + pa_log_warn("Could not set profile '%s'", selected_profile->name); + + return PA_HOOK_OK; +} + +static void handle_all_profiles(pa_core *core) { + pa_card *card; + uint32_t state; + + PA_IDXSET_FOREACH(card, core->cards, state) { + pa_card_profile *profile; + void *state2; + + PA_HASHMAP_FOREACH(profile, card->profiles, state2) + profile_available_hook_callback(core, profile, NULL); + } +} + +int pa__init(pa_module *m) { + pa_modargs *ma; + struct userdata *u; + + pa_assert(m); + + if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { + pa_log_error("Failed to parse module arguments"); + goto fail; + } + + m->userdata = u = pa_xnew0(struct userdata, 1); + + u->auto_switch = 1; + + if (pa_modargs_get_value(ma, "auto_switch", NULL)) { + bool auto_switch_bool; + + /* auto_switch originally took a boolean value, let's keep + * compatibility with configuration files that still pass a boolean. */ + if (pa_modargs_get_value_boolean(ma, "auto_switch", &auto_switch_bool) >= 0) { + if (auto_switch_bool) + u->auto_switch = 1; + else + u->auto_switch = 0; + + } else if (pa_modargs_get_value_u32(ma, "auto_switch", &u->auto_switch) < 0) { + pa_log("Failed to parse auto_switch argument."); + goto fail; + } + } + + u->enable_a2dp_source = true; + if (pa_modargs_get_value_boolean(ma, "a2dp_source", &u->enable_a2dp_source) < 0) { + pa_log("Failed to parse a2dp_source argument."); + goto fail; + } + + u->enable_ag = true; + if (pa_modargs_get_value_boolean(ma, "ag", &u->enable_ag) < 0) { + pa_log("Failed to parse ag argument."); + goto fail; + } + + u->will_need_revert_card_map = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + u->source_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT], PA_HOOK_NORMAL, + (pa_hook_cb_t) source_put_hook_callback, u); + + u->sink_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT], PA_HOOK_NORMAL, + (pa_hook_cb_t) sink_put_hook_callback, u); + + if (u->auto_switch) { + u->source_output_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_PUT], PA_HOOK_NORMAL, + (pa_hook_cb_t) source_output_put_hook_callback, u); + + u->source_output_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_UNLINK_POST], PA_HOOK_NORMAL, + (pa_hook_cb_t) source_output_unlink_hook_callback, u); + + u->card_init_profile_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_CHOOSE_INITIAL_PROFILE], PA_HOOK_NORMAL, + (pa_hook_cb_t) card_init_profile_hook_callback, u); + + u->card_unlink_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_UNLINK], PA_HOOK_NORMAL, + (pa_hook_cb_t) card_unlink_hook_callback, u); + } + + u->profile_available_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CARD_PROFILE_AVAILABLE_CHANGED], + PA_HOOK_NORMAL, (pa_hook_cb_t) profile_available_hook_callback, u); + + handle_all_profiles(m->core); + + pa_modargs_free(ma); + return 0; + +fail: + if (ma) + pa_modargs_free(ma); + return -1; +} + +void pa__done(pa_module *m) { + struct userdata *u; + + pa_assert(m); + + if (!(u = m->userdata)) + return; + + if (u->source_put_slot) + pa_hook_slot_free(u->source_put_slot); + + if (u->sink_put_slot) + pa_hook_slot_free(u->sink_put_slot); + + if (u->source_output_put_slot) + pa_hook_slot_free(u->source_output_put_slot); + + if (u->source_output_unlink_slot) + pa_hook_slot_free(u->source_output_unlink_slot); + + if (u->card_init_profile_slot) + pa_hook_slot_free(u->card_init_profile_slot); + + if (u->card_unlink_slot) + pa_hook_slot_free(u->card_unlink_slot); + + if (u->profile_available_changed_slot) + pa_hook_slot_free(u->profile_available_changed_slot); + + pa_hashmap_free(u->will_need_revert_card_map); + + pa_xfree(u); +} diff --git a/src/modules/bluetooth/module-bluez5-device.c b/src/modules/bluetooth/module-bluez5-device.c new file mode 100644 index 0000000..402053a --- /dev/null +++ b/src/modules/bluetooth/module-bluez5-device.c @@ -0,0 +1,2417 @@ +/*** + This file is part of PulseAudio. + + Copyright 2008-2013 João Paulo Rechi Vita + Copyright 2011-2013 BMW Car IT GmbH. + Copyright 2018-2019 Pali Rohár <pali.rohar@gmail.com> + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <errno.h> + +#include <arpa/inet.h> + +#include <pulse/rtclock.h> +#include <pulse/timeval.h> +#include <pulse/utf8.h> +#include <pulse/util.h> + +#include <pulsecore/core-error.h> +#include <pulsecore/core-rtclock.h> +#include <pulsecore/core-util.h> +#include <pulsecore/i18n.h> +#include <pulsecore/module.h> +#include <pulsecore/modargs.h> +#include <pulsecore/poll.h> +#include <pulsecore/rtpoll.h> +#include <pulsecore/shared.h> +#include <pulsecore/socket-util.h> +#include <pulsecore/thread.h> +#include <pulsecore/thread-mq.h> +#include <pulsecore/time-smoother.h> + +#include "a2dp-codecs.h" +#include "a2dp-codec-util.h" +#include "bluez5-util.h" + +PA_MODULE_AUTHOR("João Paulo Rechi Vita"); +PA_MODULE_DESCRIPTION("BlueZ 5 Bluetooth audio sink and source"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(false); +PA_MODULE_USAGE("path=<device object path>" + "autodetect_mtu=<boolean>"); + +#define FIXED_LATENCY_PLAYBACK_A2DP (25 * PA_USEC_PER_MSEC) +#define FIXED_LATENCY_PLAYBACK_SCO (25 * PA_USEC_PER_MSEC) +#define FIXED_LATENCY_RECORD_A2DP (25 * PA_USEC_PER_MSEC) +#define FIXED_LATENCY_RECORD_SCO (25 * PA_USEC_PER_MSEC) + +#define HSP_MAX_GAIN 15 + +static const char* const valid_modargs[] = { + "path", + "autodetect_mtu", + NULL +}; + +enum { + BLUETOOTH_MESSAGE_IO_THREAD_FAILED, + BLUETOOTH_MESSAGE_STREAM_FD_HUP, + BLUETOOTH_MESSAGE_SET_TRANSPORT_PLAYING, + BLUETOOTH_MESSAGE_MAX +}; + +enum { + PA_SOURCE_MESSAGE_SETUP_STREAM = PA_SOURCE_MESSAGE_MAX, +}; + +enum { + PA_SINK_MESSAGE_SETUP_STREAM = PA_SINK_MESSAGE_MAX, +}; + +typedef struct bluetooth_msg { + pa_msgobject parent; + pa_card *card; +} bluetooth_msg; +PA_DEFINE_PRIVATE_CLASS(bluetooth_msg, pa_msgobject); +#define BLUETOOTH_MSG(o) (bluetooth_msg_cast(o)) + +struct userdata { + pa_module *module; + pa_core *core; + + pa_hook_slot *device_connection_changed_slot; + pa_hook_slot *transport_state_changed_slot; + pa_hook_slot *transport_speaker_gain_changed_slot; + pa_hook_slot *transport_microphone_gain_changed_slot; + + pa_bluetooth_discovery *discovery; + pa_bluetooth_device *device; + pa_bluetooth_transport *transport; + bool transport_acquired; + bool stream_setup_done; + + pa_card *card; + pa_sink *sink; + pa_source *source; + pa_bluetooth_profile_t profile; + char *output_port_name; + char *input_port_name; + + pa_thread *thread; + pa_thread_mq thread_mq; + pa_rtpoll *rtpoll; + pa_rtpoll_item *rtpoll_item; + bluetooth_msg *msg; + + int stream_fd; + int stream_write_type; + size_t read_link_mtu; + size_t write_link_mtu; + size_t read_block_size; + size_t write_block_size; + uint64_t read_index; + uint64_t write_index; + pa_usec_t started_at; + pa_smoother *read_smoother; + pa_memchunk write_memchunk; + + const pa_a2dp_codec *a2dp_codec; + + void *encoder_info; + pa_sample_spec encoder_sample_spec; + void *encoder_buffer; /* Codec transfer buffer */ + size_t encoder_buffer_size; /* Size of the buffer */ + + void *decoder_info; + pa_sample_spec decoder_sample_spec; + void *decoder_buffer; /* Codec transfer buffer */ + size_t decoder_buffer_size; /* Size of the buffer */ +}; + +typedef enum pa_bluetooth_form_factor { + PA_BLUETOOTH_FORM_FACTOR_UNKNOWN, + PA_BLUETOOTH_FORM_FACTOR_HEADSET, + PA_BLUETOOTH_FORM_FACTOR_HANDSFREE, + PA_BLUETOOTH_FORM_FACTOR_MICROPHONE, + PA_BLUETOOTH_FORM_FACTOR_SPEAKER, + PA_BLUETOOTH_FORM_FACTOR_HEADPHONE, + PA_BLUETOOTH_FORM_FACTOR_PORTABLE, + PA_BLUETOOTH_FORM_FACTOR_CAR, + PA_BLUETOOTH_FORM_FACTOR_HIFI, + PA_BLUETOOTH_FORM_FACTOR_PHONE, +} pa_bluetooth_form_factor_t; + +/* Run from main thread */ +static pa_bluetooth_form_factor_t form_factor_from_class(uint32_t class_of_device) { + unsigned major, minor; + pa_bluetooth_form_factor_t r; + + static const pa_bluetooth_form_factor_t table[] = { + [1] = PA_BLUETOOTH_FORM_FACTOR_HEADSET, + [2] = PA_BLUETOOTH_FORM_FACTOR_HANDSFREE, + [4] = PA_BLUETOOTH_FORM_FACTOR_MICROPHONE, + [5] = PA_BLUETOOTH_FORM_FACTOR_SPEAKER, + [6] = PA_BLUETOOTH_FORM_FACTOR_HEADPHONE, + [7] = PA_BLUETOOTH_FORM_FACTOR_PORTABLE, + [8] = PA_BLUETOOTH_FORM_FACTOR_CAR, + [10] = PA_BLUETOOTH_FORM_FACTOR_HIFI + }; + + /* + * See Bluetooth Assigned Numbers: + * https://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm + */ + major = (class_of_device >> 8) & 0x1F; + minor = (class_of_device >> 2) & 0x3F; + + switch (major) { + case 2: + return PA_BLUETOOTH_FORM_FACTOR_PHONE; + case 4: + break; + default: + pa_log_debug("Unknown Bluetooth major device class %u", major); + return PA_BLUETOOTH_FORM_FACTOR_UNKNOWN; + } + + r = minor < PA_ELEMENTSOF(table) ? table[minor] : PA_BLUETOOTH_FORM_FACTOR_UNKNOWN; + + if (!r) + pa_log_debug("Unknown Bluetooth minor device class %u", minor); + + return r; +} + +/* Run from main thread */ +static const char *form_factor_to_string(pa_bluetooth_form_factor_t ff) { + switch (ff) { + case PA_BLUETOOTH_FORM_FACTOR_UNKNOWN: + return "unknown"; + case PA_BLUETOOTH_FORM_FACTOR_HEADSET: + return "headset"; + case PA_BLUETOOTH_FORM_FACTOR_HANDSFREE: + return "hands-free"; + case PA_BLUETOOTH_FORM_FACTOR_MICROPHONE: + return "microphone"; + case PA_BLUETOOTH_FORM_FACTOR_SPEAKER: + return "speaker"; + case PA_BLUETOOTH_FORM_FACTOR_HEADPHONE: + return "headphone"; + case PA_BLUETOOTH_FORM_FACTOR_PORTABLE: + return "portable"; + case PA_BLUETOOTH_FORM_FACTOR_CAR: + return "car"; + case PA_BLUETOOTH_FORM_FACTOR_HIFI: + return "hifi"; + case PA_BLUETOOTH_FORM_FACTOR_PHONE: + return "phone"; + } + + pa_assert_not_reached(); +} + +/* Run from main thread */ +static void connect_ports(struct userdata *u, void *new_data, pa_direction_t direction) { + pa_device_port *port; + + if (direction == PA_DIRECTION_OUTPUT) { + pa_sink_new_data *sink_new_data = new_data; + + pa_assert_se(port = pa_hashmap_get(u->card->ports, u->output_port_name)); + pa_assert_se(pa_hashmap_put(sink_new_data->ports, port->name, port) >= 0); + pa_device_port_ref(port); + } else { + pa_source_new_data *source_new_data = new_data; + + pa_assert_se(port = pa_hashmap_get(u->card->ports, u->input_port_name)); + pa_assert_se(pa_hashmap_put(source_new_data->ports, port->name, port) >= 0); + pa_device_port_ref(port); + } +} + +/* Run from IO thread */ +static int sco_process_render(struct userdata *u) { + ssize_t l; + pa_memchunk memchunk; + int saved_errno; + + pa_assert(u); + pa_assert(u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || + u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY); + pa_assert(u->sink); + + pa_sink_render_full(u->sink, u->write_block_size, &memchunk); + + pa_assert(memchunk.length == u->write_block_size); + + for (;;) { + const void *p; + + /* Now write that data to the socket. The socket is of type + * SEQPACKET, and we generated the data of the MTU size, so this + * should just work. */ + + p = (const uint8_t *) pa_memblock_acquire_chunk(&memchunk); + l = pa_write(u->stream_fd, p, memchunk.length, &u->stream_write_type); + pa_memblock_release(memchunk.memblock); + + pa_assert(l != 0); + + if (l > 0) + break; + + saved_errno = errno; + + if (saved_errno == EINTR) + /* Retry right away if we got interrupted */ + continue; + + pa_memblock_unref(memchunk.memblock); + + if (saved_errno == EAGAIN) { + /* Hmm, apparently the socket was not writable, give up for now. + * Because the data was already rendered, let's discard the block. */ + pa_log_debug("Got EAGAIN on write() after POLLOUT, probably there is a temporary connection loss."); + return 1; + } + + pa_log_error("Failed to write data to SCO socket: %s", pa_cstrerror(saved_errno)); + return -1; + } + + pa_assert((size_t) l <= memchunk.length); + + if ((size_t) l != memchunk.length) { + pa_log_error("Wrote memory block to socket only partially! %llu written, wanted to write %llu.", + (unsigned long long) l, + (unsigned long long) memchunk.length); + + pa_memblock_unref(memchunk.memblock); + return -1; + } + + u->write_index += (uint64_t) memchunk.length; + pa_memblock_unref(memchunk.memblock); + + return 1; +} + +/* Run from IO thread */ +static int sco_process_push(struct userdata *u) { + ssize_t l; + pa_memchunk memchunk; + struct cmsghdr *cm; + struct msghdr m; + bool found_tstamp = false; + pa_usec_t tstamp = 0; + + pa_assert(u); + pa_assert(u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || + u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY); + pa_assert(u->source); + pa_assert(u->read_smoother); + + memchunk.memblock = pa_memblock_new(u->core->mempool, u->read_block_size); + memchunk.index = memchunk.length = 0; + + for (;;) { + void *p; + uint8_t aux[1024]; + struct iovec iov; + + pa_zero(m); + pa_zero(aux); + pa_zero(iov); + + m.msg_iov = &iov; + m.msg_iovlen = 1; + m.msg_control = aux; + m.msg_controllen = sizeof(aux); + + p = pa_memblock_acquire(memchunk.memblock); + iov.iov_base = p; + iov.iov_len = pa_memblock_get_length(memchunk.memblock); + l = recvmsg(u->stream_fd, &m, 0); + pa_memblock_release(memchunk.memblock); + + if (l > 0) + break; + + if (l < 0 && errno == EINTR) + /* Retry right away if we got interrupted */ + continue; + + pa_memblock_unref(memchunk.memblock); + + if (l < 0 && errno == EAGAIN) + /* Hmm, apparently the socket was not readable, give up for now. */ + return 0; + + pa_log_error("Failed to read data from SCO socket: %s", l < 0 ? pa_cstrerror(errno) : "EOF"); + return -1; + } + + pa_assert((size_t) l <= pa_memblock_get_length(memchunk.memblock)); + + /* In some rare occasions, we might receive packets of a very strange + * size. This could potentially be possible if the SCO packet was + * received partially over-the-air, or more probably due to hardware + * issues in our Bluetooth adapter. In these cases, in order to avoid + * an assertion failure due to unaligned data, just discard the whole + * packet */ + if (!pa_frame_aligned(l, &u->decoder_sample_spec)) { + pa_log_warn("SCO packet received of unaligned size: %zu", l); + pa_memblock_unref(memchunk.memblock); + return -1; + } + + memchunk.length = (size_t) l; + u->read_index += (uint64_t) l; + + for (cm = CMSG_FIRSTHDR(&m); cm; cm = CMSG_NXTHDR(&m, cm)) + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMP) { + struct timeval *tv = (struct timeval*) CMSG_DATA(cm); + pa_rtclock_from_wallclock(tv); + tstamp = pa_timeval_load(tv); + found_tstamp = true; + break; + } + + if (!found_tstamp) { + PA_ONCE_BEGIN { + pa_log_warn("Couldn't find SO_TIMESTAMP data in auxiliary recvmsg() data!"); + } PA_ONCE_END; + tstamp = pa_rtclock_now(); + } + + pa_smoother_put(u->read_smoother, tstamp, pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec)); + pa_smoother_resume(u->read_smoother, tstamp, true); + + pa_source_post(u->source, &memchunk); + pa_memblock_unref(memchunk.memblock); + + return l; +} + +/* Run from IO thread */ +static void a2dp_prepare_encoder_buffer(struct userdata *u) { + pa_assert(u); + + if (u->encoder_buffer_size < u->write_link_mtu) { + pa_xfree(u->encoder_buffer); + u->encoder_buffer = pa_xmalloc(u->write_link_mtu); + } + + /* Encoder buffer cannot be larger then link MTU, otherwise + * encode method would produce larger packets then link MTU */ + u->encoder_buffer_size = u->write_link_mtu; +} + +/* Run from IO thread */ +static void a2dp_prepare_decoder_buffer(struct userdata *u) { + pa_assert(u); + + if (u->decoder_buffer_size < u->read_link_mtu) { + pa_xfree(u->decoder_buffer); + u->decoder_buffer = pa_xmalloc(u->read_link_mtu); + } + + /* Decoder buffer cannot be larger then link MTU, otherwise + * decode method would produce larger output then read_block_size */ + u->decoder_buffer_size = u->read_link_mtu; +} + +/* Run from IO thread */ +static int a2dp_write_buffer(struct userdata *u, size_t nbytes) { + int ret = 0; + + /* Encoder function of A2DP codec may provide empty buffer, in this case do + * not post any empty buffer via A2DP socket. It may be because of codec + * internal state, e.g. encoder is waiting for more samples so it can + * provide encoded data. */ + if (PA_UNLIKELY(!nbytes)) { + u->write_index += (uint64_t) u->write_memchunk.length; + pa_memblock_unref(u->write_memchunk.memblock); + pa_memchunk_reset(&u->write_memchunk); + return 0; + } + + for (;;) { + ssize_t l; + + l = pa_write(u->stream_fd, u->encoder_buffer, nbytes, &u->stream_write_type); + + pa_assert(l != 0); + + if (l < 0) { + + if (errno == EINTR) + /* Retry right away if we got interrupted */ + continue; + + else if (errno == EAGAIN) { + /* Hmm, apparently the socket was not writable, give up for now */ + pa_log_debug("Got EAGAIN on write() after POLLOUT, probably there is a temporary connection loss."); + break; + } + + pa_log_error("Failed to write data to socket: %s", pa_cstrerror(errno)); + ret = -1; + break; + } + + pa_assert((size_t) l <= nbytes); + + if ((size_t) l != nbytes) { + pa_log_warn("Wrote memory block to socket only partially! %llu written, wanted to write %llu.", + (unsigned long long) l, + (unsigned long long) nbytes); + ret = -1; + break; + } + + u->write_index += (uint64_t) u->write_memchunk.length; + pa_memblock_unref(u->write_memchunk.memblock); + pa_memchunk_reset(&u->write_memchunk); + + ret = 1; + + break; + } + + return ret; +} + +/* Run from IO thread */ +static int a2dp_process_render(struct userdata *u) { + const uint8_t *ptr; + size_t processed; + size_t length; + + pa_assert(u); + pa_assert(u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK); + pa_assert(u->sink); + pa_assert(u->a2dp_codec); + + /* First, render some data */ + if (!u->write_memchunk.memblock) + pa_sink_render_full(u->sink, u->write_block_size, &u->write_memchunk); + + pa_assert(u->write_memchunk.length == u->write_block_size); + + a2dp_prepare_encoder_buffer(u); + + /* Try to create a packet of the full MTU */ + ptr = (const uint8_t *) pa_memblock_acquire_chunk(&u->write_memchunk); + + length = u->a2dp_codec->encode_buffer(u->encoder_info, u->write_index / pa_frame_size(&u->encoder_sample_spec), ptr, u->write_memchunk.length, u->encoder_buffer, u->encoder_buffer_size, &processed); + + pa_memblock_release(u->write_memchunk.memblock); + + if (processed != u->write_memchunk.length) { + pa_log_error("Encoding error"); + return -1; + } + + return a2dp_write_buffer(u, length); +} + +/* Run from IO thread */ +static int a2dp_process_push(struct userdata *u) { + int ret = 0; + pa_memchunk memchunk; + + pa_assert(u); + pa_assert(u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE); + pa_assert(u->source); + pa_assert(u->read_smoother); + pa_assert(u->a2dp_codec); + + memchunk.memblock = pa_memblock_new(u->core->mempool, u->read_block_size); + memchunk.index = memchunk.length = 0; + + a2dp_prepare_decoder_buffer(u); + + for (;;) { + uint8_t aux[1024]; + struct iovec iov; + struct cmsghdr *cm; + struct msghdr m; + bool found_tstamp = false; + pa_usec_t tstamp; + uint8_t *ptr; + ssize_t l; + size_t processed; + + pa_zero(m); + pa_zero(aux); + pa_zero(iov); + + m.msg_iov = &iov; + m.msg_iovlen = 1; + m.msg_control = aux; + m.msg_controllen = sizeof(aux); + + iov.iov_base = u->decoder_buffer; + iov.iov_len = u->decoder_buffer_size; + + l = recvmsg(u->stream_fd, &m, 0); + + if (l <= 0) { + + if (l < 0 && errno == EINTR) + /* Retry right away if we got interrupted */ + continue; + + else if (l < 0 && errno == EAGAIN) + /* Hmm, apparently the socket was not readable, give up for now. */ + break; + + pa_log_error("Failed to read data from socket: %s", l < 0 ? pa_cstrerror(errno) : "EOF"); + ret = -1; + break; + } + + pa_assert((size_t) l <= u->decoder_buffer_size); + + /* TODO: get timestamp from rtp */ + + for (cm = CMSG_FIRSTHDR(&m); cm; cm = CMSG_NXTHDR(&m, cm)) { + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMP) { + struct timeval *tv = (struct timeval*) CMSG_DATA(cm); + pa_rtclock_from_wallclock(tv); + tstamp = pa_timeval_load(tv); + found_tstamp = true; + break; + } + } + + if (!found_tstamp) { + PA_ONCE_BEGIN { + pa_log_warn("Couldn't find SO_TIMESTAMP data in auxiliary recvmsg() data!"); + } PA_ONCE_END; + tstamp = pa_rtclock_now(); + } + + ptr = pa_memblock_acquire(memchunk.memblock); + memchunk.length = pa_memblock_get_length(memchunk.memblock); + + memchunk.length = u->a2dp_codec->decode_buffer(u->decoder_info, u->decoder_buffer, l, ptr, memchunk.length, &processed); + + pa_memblock_release(memchunk.memblock); + + if (processed != (size_t) l) { + pa_log_error("Decoding error"); + ret = -1; + break; + } + + u->read_index += (uint64_t) memchunk.length; + pa_smoother_put(u->read_smoother, tstamp, pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec)); + pa_smoother_resume(u->read_smoother, tstamp, true); + + /* Decoding of A2DP codec data may result in empty buffer, in this case + * do not post empty audio samples. It may happen due to algorithmic + * delay of audio codec. */ + if (PA_LIKELY(memchunk.length)) + pa_source_post(u->source, &memchunk); + + ret = l; + break; + } + + pa_memblock_unref(memchunk.memblock); + + return ret; +} + +static void update_sink_buffer_size(struct userdata *u) { + int old_bufsize; + socklen_t len = sizeof(int); + int ret; + + ret = getsockopt(u->stream_fd, SOL_SOCKET, SO_SNDBUF, &old_bufsize, &len); + if (ret == -1) { + pa_log_warn("Changing bluetooth buffer size: Failed to getsockopt(SO_SNDBUF): %s", pa_cstrerror(errno)); + } else { + int new_bufsize; + + /* Set send buffer size as small as possible. The minimum value is 1024 according to the + * socket man page. The data is written to the socket in chunks of write_block_size, so + * there should at least be room for two chunks in the buffer. Generally, write_block_size + * is larger than 512. If not, use the next multiple of write_block_size which is larger + * than 1024. */ + new_bufsize = 2 * u->write_block_size; + if (new_bufsize < 1024) + new_bufsize = (1024 / u->write_block_size + 1) * u->write_block_size; + + /* The kernel internally doubles the buffer size that was set by setsockopt and getsockopt + * returns the doubled value. */ + if (new_bufsize != old_bufsize / 2) { + ret = setsockopt(u->stream_fd, SOL_SOCKET, SO_SNDBUF, &new_bufsize, len); + if (ret == -1) + pa_log_warn("Changing bluetooth buffer size: Failed to change from %d to %d: %s", old_bufsize / 2, new_bufsize, pa_cstrerror(errno)); + else + pa_log_info("Changing bluetooth buffer size: Changed from %d to %d", old_bufsize / 2, new_bufsize); + } + } +} + +static void teardown_stream(struct userdata *u) { + if (u->rtpoll_item) { + pa_rtpoll_item_free(u->rtpoll_item); + u->rtpoll_item = NULL; + } + + if (u->stream_fd >= 0) { + pa_close(u->stream_fd); + u->stream_fd = -1; + } + + if (u->read_smoother) { + pa_smoother_free(u->read_smoother); + u->read_smoother = NULL; + } + + if (u->write_memchunk.memblock) { + pa_memblock_unref(u->write_memchunk.memblock); + pa_memchunk_reset(&u->write_memchunk); + } + + pa_log_debug("Audio stream torn down"); + u->stream_setup_done = false; +} + +static int transport_acquire(struct userdata *u, bool optional) { + pa_assert(u->transport); + + if (u->transport_acquired) + return 0; + + pa_log_debug("Acquiring transport %s", u->transport->path); + + u->stream_fd = u->transport->acquire(u->transport, optional, &u->read_link_mtu, &u->write_link_mtu); + if (u->stream_fd < 0) + return u->stream_fd; + + /* transport_acquired must be set before calling + * pa_bluetooth_transport_set_state() */ + u->transport_acquired = true; + pa_log_info("Transport %s acquired: fd %d", u->transport->path, u->stream_fd); + + if (u->transport->state == PA_BLUETOOTH_TRANSPORT_STATE_IDLE) { + if (pa_thread_mq_get() != NULL) + pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_SET_TRANSPORT_PLAYING, NULL, 0, NULL, NULL); + else + pa_bluetooth_transport_set_state(u->transport, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING); + } + + return 0; +} + +static void transport_release(struct userdata *u) { + pa_assert(u->transport); + + /* Ignore if already released */ + if (!u->transport_acquired) + return; + + pa_log_debug("Releasing transport %s", u->transport->path); + + u->transport->release(u->transport); + + u->transport_acquired = false; + + teardown_stream(u); + + /* Set transport state to idle if this was not already done by the remote end closing + * the file descriptor. Only do this when called from the I/O thread */ + if (pa_thread_mq_get() != NULL && u->transport->state == PA_BLUETOOTH_TRANSPORT_STATE_PLAYING) + pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_STREAM_FD_HUP, NULL, 0, NULL, NULL); +} + +/* Run from I/O thread */ +static void handle_sink_block_size_change(struct userdata *u) { + pa_sink_set_max_request_within_thread(u->sink, u->write_block_size); + pa_sink_set_fixed_latency_within_thread(u->sink, + (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK ? + FIXED_LATENCY_PLAYBACK_A2DP : FIXED_LATENCY_PLAYBACK_SCO) + + pa_bytes_to_usec(u->write_block_size, &u->encoder_sample_spec)); + + /* If there is still data in the memchunk, we have to discard it + * because the write_block_size may have changed. */ + if (u->write_memchunk.memblock) { + pa_memblock_unref(u->write_memchunk.memblock); + pa_memchunk_reset(&u->write_memchunk); + } + + update_sink_buffer_size(u); +} + +/* Run from I/O thread */ +static void transport_config_mtu(struct userdata *u) { + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) { + u->read_block_size = u->read_link_mtu; + u->write_block_size = u->write_link_mtu; + + if (!pa_frame_aligned(u->read_block_size, &u->source->sample_spec)) { + pa_log_debug("Got invalid read MTU: %lu, rounding down", u->read_block_size); + u->read_block_size = pa_frame_align(u->read_block_size, &u->source->sample_spec); + } + + if (!pa_frame_aligned(u->write_block_size, &u->sink->sample_spec)) { + pa_log_debug("Got invalid write MTU: %lu, rounding down", u->write_block_size); + u->write_block_size = pa_frame_align(u->write_block_size, &u->sink->sample_spec); + } + } else { + pa_assert(u->a2dp_codec); + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { + u->write_block_size = u->a2dp_codec->get_write_block_size(u->encoder_info, u->write_link_mtu); + } else { + u->read_block_size = u->a2dp_codec->get_read_block_size(u->decoder_info, u->read_link_mtu); + } + } + + if (u->sink) + handle_sink_block_size_change(u); + + if (u->source) + pa_source_set_fixed_latency_within_thread(u->source, + (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE ? + FIXED_LATENCY_RECORD_A2DP : FIXED_LATENCY_RECORD_SCO) + + pa_bytes_to_usec(u->read_block_size, &u->decoder_sample_spec)); +} + +/* Run from I/O thread */ +static int setup_stream(struct userdata *u) { + struct pollfd *pollfd; + int one; + + pa_assert(u->stream_fd >= 0); + + /* return if stream is already set up */ + if (u->stream_setup_done) + return 0; + + pa_log_info("Transport %s resuming", u->transport->path); + + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { + pa_assert(u->a2dp_codec); + if (u->a2dp_codec->reset(u->encoder_info) < 0) + return -1; + } else if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) { + pa_assert(u->a2dp_codec); + if (u->a2dp_codec->reset(u->decoder_info) < 0) + return -1; + } + + transport_config_mtu(u); + + pa_make_fd_nonblock(u->stream_fd); + pa_make_socket_low_delay(u->stream_fd); + + one = 1; + if (setsockopt(u->stream_fd, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0) + pa_log_warn("Failed to enable SO_TIMESTAMP: %s", pa_cstrerror(errno)); + + pa_log_debug("Stream properly set up, we're ready to roll!"); + + u->rtpoll_item = pa_rtpoll_item_new(u->rtpoll, PA_RTPOLL_NEVER, 1); + pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL); + pollfd->fd = u->stream_fd; + pollfd->events = pollfd->revents = 0; + + u->read_index = u->write_index = 0; + u->started_at = 0; + u->stream_setup_done = true; + + if (u->source) + u->read_smoother = pa_smoother_new(PA_USEC_PER_SEC, 2*PA_USEC_PER_SEC, true, true, 10, pa_rtclock_now(), true); + + return 0; +} + +/* Called from I/O thread, returns true if the transport was acquired or + * a connection was requested successfully. */ +static bool setup_transport_and_stream(struct userdata *u) { + int transport_error; + + transport_error = transport_acquire(u, false); + if (transport_error < 0) { + if (transport_error != -EAGAIN) + return false; + } else { + if (setup_stream(u) < 0) + return false; + } + return true; +} + +/* Run from IO thread */ +static int source_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) { + struct userdata *u = PA_SOURCE(o)->userdata; + + pa_assert(u->source == PA_SOURCE(o)); + pa_assert(u->transport); + + switch (code) { + + case PA_SOURCE_MESSAGE_GET_LATENCY: { + int64_t wi, ri; + + if (u->read_smoother) { + wi = pa_smoother_get(u->read_smoother, pa_rtclock_now()); + ri = pa_bytes_to_usec(u->read_index, &u->decoder_sample_spec); + + *((int64_t*) data) = u->source->thread_info.fixed_latency + wi - ri; + } else + *((int64_t*) data) = 0; + + return 0; + } + + case PA_SOURCE_MESSAGE_SETUP_STREAM: + /* Skip stream setup if stream_fd has been invalidated. + This can occur if the stream has already been set up and + then immediately received POLLHUP. If the stream has + already been set up earlier, then this setup_stream() + call is redundant anyway, but currently the code + is such that this kind of unnecessary setup_stream() + calls can happen. */ + if (u->stream_fd < 0) + pa_log_debug("Skip source stream setup while closing"); + else + setup_stream(u); + return 0; + + } + + return pa_source_process_msg(o, code, data, offset, chunk); +} + +/* Called from the IO thread. */ +static int source_set_state_in_io_thread_cb(pa_source *s, pa_source_state_t new_state, pa_suspend_cause_t new_suspend_cause) { + struct userdata *u; + + pa_assert(s); + pa_assert_se(u = s->userdata); + + switch (new_state) { + + case PA_SOURCE_SUSPENDED: + /* Ignore if transition is PA_SOURCE_INIT->PA_SOURCE_SUSPENDED */ + if (!PA_SOURCE_IS_OPENED(s->thread_info.state)) + break; + + /* Stop the device if the sink is suspended as well */ + if (!u->sink || u->sink->state == PA_SINK_SUSPENDED) + transport_release(u); + + if (u->read_smoother) + pa_smoother_pause(u->read_smoother, pa_rtclock_now()); + + break; + + case PA_SOURCE_IDLE: + case PA_SOURCE_RUNNING: + if (s->thread_info.state != PA_SOURCE_SUSPENDED) + break; + + /* Resume the device if the sink was suspended as well */ + if (!u->sink || !PA_SINK_IS_OPENED(u->sink->thread_info.state)) + if (!setup_transport_and_stream(u)) + return -1; + + /* We don't resume the smoother here. Instead we + * wait until the first packet arrives */ + + break; + + case PA_SOURCE_UNLINKED: + case PA_SOURCE_INIT: + case PA_SOURCE_INVALID_STATE: + break; + } + + return 0; +} + +/* Run from main thread */ +static void source_set_volume_cb(pa_source *s) { + uint16_t gain; + pa_volume_t volume; + struct userdata *u; + + pa_assert(s); + pa_assert(s->core); + + u = s->userdata; + + pa_assert(u); + pa_assert(u->source == s); + + if (u->transport->set_microphone_gain == NULL) + return; + + gain = (pa_cvolume_max(&s->real_volume) * HSP_MAX_GAIN) / PA_VOLUME_NORM; + + if (gain > HSP_MAX_GAIN) + gain = HSP_MAX_GAIN; + + volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN); + + /* increment volume by one to correct rounding errors */ + if (volume < PA_VOLUME_NORM) + volume++; + + pa_cvolume_set(&s->real_volume, u->decoder_sample_spec.channels, volume); + + /* Set soft volume when in headset role */ + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + pa_cvolume_set(&s->soft_volume, u->decoder_sample_spec.channels, volume); + + /* If we are in the AG role, we send a command to the head set to change + * the microphone gain. In the HS role, source and sink are swapped, so + * in this case we notify the AG that the speaker gain has changed */ + u->transport->set_microphone_gain(u->transport, gain); +} + +/* Run from main thread */ +static int add_source(struct userdata *u) { + pa_source_new_data data; + + pa_assert(u->transport); + + pa_source_new_data_init(&data); + data.module = u->module; + data.card = u->card; + data.driver = __FILE__; + data.name = pa_sprintf_malloc("bluez_source.%s.%s", u->device->address, pa_bluetooth_profile_to_string(u->profile)); + data.namereg_fail = false; + pa_proplist_sets(data.proplist, "bluetooth.protocol", pa_bluetooth_profile_to_string(u->profile)); + pa_source_new_data_set_sample_spec(&data, &u->decoder_sample_spec); + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone"); + + connect_ports(u, &data, PA_DIRECTION_INPUT); + + if (!u->transport_acquired) + switch (u->profile) { + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + data.suspend_cause = PA_SUSPEND_USER; + break; + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + /* u->stream_fd contains the error returned by the last transport_acquire() + * EAGAIN means we are waiting for a NewConnection signal */ + if (u->stream_fd == -EAGAIN) + data.suspend_cause = PA_SUSPEND_USER; + else + pa_assert_not_reached(); + break; + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + case PA_BLUETOOTH_PROFILE_OFF: + pa_assert_not_reached(); + break; + } + + u->source = pa_source_new(u->core, &data, PA_SOURCE_HARDWARE|PA_SOURCE_LATENCY); + pa_source_new_data_done(&data); + if (!u->source) { + pa_log_error("Failed to create source"); + return -1; + } + + u->source->userdata = u; + u->source->parent.process_msg = source_process_msg; + u->source->set_state_in_io_thread = source_set_state_in_io_thread_cb; + + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) { + pa_source_set_set_volume_callback(u->source, source_set_volume_cb); + u->source->n_volume_steps = 16; + } + return 0; +} + +/* Run from IO thread */ +static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) { + struct userdata *u = PA_SINK(o)->userdata; + + pa_assert(u->sink == PA_SINK(o)); + pa_assert(u->transport); + + switch (code) { + + case PA_SINK_MESSAGE_GET_LATENCY: { + int64_t wi = 0, ri = 0; + + if (u->read_smoother) { + ri = pa_smoother_get(u->read_smoother, pa_rtclock_now()); + wi = pa_bytes_to_usec(u->write_index + u->write_block_size, &u->encoder_sample_spec); + } else if (u->started_at) { + ri = pa_rtclock_now() - u->started_at; + wi = pa_bytes_to_usec(u->write_index, &u->encoder_sample_spec); + } + + *((int64_t*) data) = u->sink->thread_info.fixed_latency + wi - ri; + + return 0; + } + + case PA_SINK_MESSAGE_SETUP_STREAM: + /* Skip stream setup if stream_fd has been invalidated. + This can occur if the stream has already been set up and + then immediately received POLLHUP. If the stream has + already been set up earlier, then this setup_stream() + call is redundant anyway, but currently the code + is such that this kind of unnecessary setup_stream() + calls can happen. */ + if (u->stream_fd < 0) + pa_log_debug("Skip sink stream setup while closing"); + else + setup_stream(u); + return 0; + } + + return pa_sink_process_msg(o, code, data, offset, chunk); +} + +/* Called from the IO thread. */ +static int sink_set_state_in_io_thread_cb(pa_sink *s, pa_sink_state_t new_state, pa_suspend_cause_t new_suspend_cause) { + struct userdata *u; + + pa_assert(s); + pa_assert_se(u = s->userdata); + + switch (new_state) { + + case PA_SINK_SUSPENDED: + /* Ignore if transition is PA_SINK_INIT->PA_SINK_SUSPENDED */ + if (!PA_SINK_IS_OPENED(s->thread_info.state)) + break; + + /* Stop the device if the source is suspended as well */ + if (!u->source || u->source->state == PA_SOURCE_SUSPENDED) + /* We deliberately ignore whether stopping + * actually worked. Since the stream_fd is + * closed it doesn't really matter */ + transport_release(u); + + break; + + case PA_SINK_IDLE: + case PA_SINK_RUNNING: + if (s->thread_info.state != PA_SINK_SUSPENDED) + break; + + /* Resume the device if the source was suspended as well */ + if (!u->source || !PA_SOURCE_IS_OPENED(u->source->thread_info.state)) + if (!setup_transport_and_stream(u)) + return -1; + + break; + + case PA_SINK_UNLINKED: + case PA_SINK_INIT: + case PA_SINK_INVALID_STATE: + break; + } + + return 0; +} + +/* Run from main thread */ +static void sink_set_volume_cb(pa_sink *s) { + uint16_t gain; + pa_volume_t volume; + struct userdata *u; + + pa_assert(s); + pa_assert(s->core); + + u = s->userdata; + + pa_assert(u); + pa_assert(u->sink == s); + + if (u->transport->set_speaker_gain == NULL) + return; + + gain = (pa_cvolume_max(&s->real_volume) * HSP_MAX_GAIN) / PA_VOLUME_NORM; + + if (gain > HSP_MAX_GAIN) + gain = HSP_MAX_GAIN; + + volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN); + + /* increment volume by one to correct rounding errors */ + if (volume < PA_VOLUME_NORM) + volume++; + + pa_cvolume_set(&s->real_volume, u->encoder_sample_spec.channels, volume); + + /* Set soft volume when in headset role */ + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + pa_cvolume_set(&s->soft_volume, u->encoder_sample_spec.channels, volume); + + /* If we are in the AG role, we send a command to the head set to change + * the speaker gain. In the HS role, source and sink are swapped, so + * in this case we notify the AG that the microphone gain has changed */ + u->transport->set_speaker_gain(u->transport, gain); +} + +/* Run from main thread */ +static int add_sink(struct userdata *u) { + pa_sink_new_data data; + + pa_assert(u->transport); + + pa_sink_new_data_init(&data); + data.module = u->module; + data.card = u->card; + data.driver = __FILE__; + data.name = pa_sprintf_malloc("bluez_sink.%s.%s", u->device->address, pa_bluetooth_profile_to_string(u->profile)); + data.namereg_fail = false; + pa_proplist_sets(data.proplist, "bluetooth.protocol", pa_bluetooth_profile_to_string(u->profile)); + pa_sink_new_data_set_sample_spec(&data, &u->encoder_sample_spec); + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone"); + + connect_ports(u, &data, PA_DIRECTION_OUTPUT); + + if (!u->transport_acquired) + switch (u->profile) { + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + data.suspend_cause = PA_SUSPEND_USER; + break; + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + /* u->stream_fd contains the error returned by the last transport_acquire() + * EAGAIN means we are waiting for a NewConnection signal */ + if (u->stream_fd == -EAGAIN) + data.suspend_cause = PA_SUSPEND_USER; + else + pa_assert_not_reached(); + break; + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + /* Profile switch should have failed */ + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + case PA_BLUETOOTH_PROFILE_OFF: + pa_assert_not_reached(); + break; + } + + u->sink = pa_sink_new(u->core, &data, PA_SINK_HARDWARE|PA_SINK_LATENCY); + pa_sink_new_data_done(&data); + if (!u->sink) { + pa_log_error("Failed to create sink"); + return -1; + } + + u->sink->userdata = u; + u->sink->parent.process_msg = sink_process_msg; + u->sink->set_state_in_io_thread = sink_set_state_in_io_thread_cb; + + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) { + pa_sink_set_set_volume_callback(u->sink, sink_set_volume_cb); + u->sink->n_volume_steps = 16; + } + return 0; +} + +/* Run from main thread */ +static int transport_config(struct userdata *u) { + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) { + u->encoder_sample_spec.format = PA_SAMPLE_S16LE; + u->encoder_sample_spec.channels = 1; + u->encoder_sample_spec.rate = 8000; + u->decoder_sample_spec.format = PA_SAMPLE_S16LE; + u->decoder_sample_spec.channels = 1; + u->decoder_sample_spec.rate = 8000; + return 0; + } else { + bool is_a2dp_sink = u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK; + void *info; + + pa_assert(u->transport); + + pa_assert(!u->a2dp_codec); + pa_assert(!u->encoder_info); + pa_assert(!u->decoder_info); + + u->a2dp_codec = u->transport->a2dp_codec; + pa_assert(u->a2dp_codec); + + info = u->a2dp_codec->init(is_a2dp_sink, false, u->transport->config, u->transport->config_size, is_a2dp_sink ? &u->encoder_sample_spec : &u->decoder_sample_spec); + if (is_a2dp_sink) + u->encoder_info = info; + else + u->decoder_info = info; + + if (!info) + return -1; + + return 0; + } +} + +/* Run from main thread */ +static int setup_transport(struct userdata *u) { + pa_bluetooth_transport *t; + + pa_assert(u); + pa_assert(!u->transport); + pa_assert(u->profile != PA_BLUETOOTH_PROFILE_OFF); + + /* check if profile has a transport */ + t = u->device->transports[u->profile]; + if (!t || t->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) { + pa_log_warn("Profile %s has no transport", pa_bluetooth_profile_to_string(u->profile)); + return -1; + } + + u->transport = t; + + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE || u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + transport_acquire(u, true); /* In case of error, the sink/sources will be created suspended */ + else { + int transport_error; + + transport_error = transport_acquire(u, false); + if (transport_error < 0 && transport_error != -EAGAIN) + return -1; /* We need to fail here until the interactions with module-suspend-on-idle and alike get improved */ + } + + return transport_config(u); +} + +/* Run from main thread */ +static pa_direction_t get_profile_direction(pa_bluetooth_profile_t p) { + static const pa_direction_t profile_direction[] = { + [PA_BLUETOOTH_PROFILE_A2DP_SINK] = PA_DIRECTION_OUTPUT, + [PA_BLUETOOTH_PROFILE_A2DP_SOURCE] = PA_DIRECTION_INPUT, + [PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT] = PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT, + [PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY] = PA_DIRECTION_INPUT | PA_DIRECTION_OUTPUT, + [PA_BLUETOOTH_PROFILE_OFF] = 0 + }; + + return profile_direction[p]; +} + +/* Run from main thread */ +static int init_profile(struct userdata *u) { + int r = 0; + pa_assert(u); + pa_assert(u->profile != PA_BLUETOOTH_PROFILE_OFF); + + if (setup_transport(u) < 0) + return -1; + + pa_assert(u->transport); + + if (get_profile_direction (u->profile) & PA_DIRECTION_OUTPUT) + if (add_sink(u) < 0) + r = -1; + + if (get_profile_direction (u->profile) & PA_DIRECTION_INPUT) + if (add_source(u) < 0) + r = -1; + + return r; +} + +static int write_block(struct userdata *u) { + int n_written; + + if (u->write_index <= 0) + u->started_at = pa_rtclock_now(); + + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { + if ((n_written = a2dp_process_render(u)) < 0) + return -1; + } else { + if ((n_written = sco_process_render(u)) < 0) + return -1; + } + + return n_written; +} + + +/* I/O thread function */ +static void thread_func(void *userdata) { + struct userdata *u = userdata; + unsigned blocks_to_write = 0; + unsigned bytes_to_write = 0; + + pa_assert(u); + pa_assert(u->transport); + + pa_log_debug("IO Thread starting up"); + + if (u->core->realtime_scheduling) + pa_thread_make_realtime(u->core->realtime_priority); + + pa_thread_mq_install(&u->thread_mq); + + /* Setup the stream only if the transport was already acquired */ + if (u->transport_acquired) + setup_stream(u); + + for (;;) { + struct pollfd *pollfd; + int ret; + bool disable_timer = true; + bool writable = false; + bool have_source = u->source ? PA_SOURCE_IS_LINKED(u->source->thread_info.state) : false; + bool have_sink = u->sink ? PA_SINK_IS_LINKED(u->sink->thread_info.state) : false; + + pollfd = u->rtpoll_item ? pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL) : NULL; + + /* Check for stream error or close */ + if (pollfd && (pollfd->revents & ~(POLLOUT|POLLIN))) { + pa_log_info("FD error: %s%s%s%s", + pollfd->revents & POLLERR ? "POLLERR " :"", + pollfd->revents & POLLHUP ? "POLLHUP " :"", + pollfd->revents & POLLPRI ? "POLLPRI " :"", + pollfd->revents & POLLNVAL ? "POLLNVAL " :""); + + if (pollfd->revents & POLLHUP) { + pollfd = NULL; + teardown_stream(u); + blocks_to_write = 0; + bytes_to_write = 0; + pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_STREAM_FD_HUP, NULL, 0, NULL, NULL); + } else + goto fail; + } + + /* If there is a pollfd, the stream is set up and we need to do something */ + if (pollfd) { + + /* Handle source if present */ + if (have_source) { + + /* We should send two blocks to the device before we expect a response. */ + if (have_sink && u->write_index == 0 && u->read_index <= 0) + blocks_to_write = 2; + + /* If we got woken up by POLLIN let's do some reading */ + if (pollfd->revents & POLLIN) { + int n_read; + + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) + n_read = a2dp_process_push(u); + else + n_read = sco_process_push(u); + + if (n_read < 0) + goto fail; + + if (have_sink && n_read > 0) { + /* We just read something, so we are supposed to write something, too */ + bytes_to_write += n_read; + blocks_to_write += bytes_to_write / u->write_block_size; + bytes_to_write = bytes_to_write % u->write_block_size; + } + } + } + + /* Handle sink if present */ + if (have_sink) { + + /* Process rewinds */ + if (PA_UNLIKELY(u->sink->thread_info.rewind_requested)) + pa_sink_process_rewind(u->sink, 0); + + /* Test if the stream is writable */ + if (pollfd->revents & POLLOUT) + writable = true; + + /* If we have a source, we let the source determine the timing + * for the sink */ + if (have_source) { + + if (writable && blocks_to_write > 0) { + int result; + + if ((result = write_block(u)) < 0) + goto fail; + + blocks_to_write -= result; + + /* writable controls whether we set POLLOUT when polling - we set it to + * false to enable POLLOUT. If there are more blocks to write, we want to + * be woken up immediately when the socket becomes writable. If there + * aren't currently any more blocks to write, then we'll have to wait + * until we've received more data, so in that case we only want to set + * POLLIN. Note that when we are woken up the next time, POLLOUT won't be + * set in revents even if the socket has meanwhile become writable, which + * may seem bad, but in that case we'll set POLLOUT in the subsequent + * poll, and the poll will return immediately, so our writes won't be + * delayed. */ + if (blocks_to_write > 0) + writable = false; + } + + /* There is no source, we have to use the system clock for timing */ + } else { + bool have_written = false; + pa_usec_t time_passed = 0; + pa_usec_t audio_sent = 0; + + if (u->started_at) { + time_passed = pa_rtclock_now() - u->started_at; + audio_sent = pa_bytes_to_usec(u->write_index, &u->encoder_sample_spec); + } + + /* A new block needs to be sent. */ + if (audio_sent <= time_passed) { + size_t bytes_to_send = pa_usec_to_bytes(time_passed - audio_sent, &u->encoder_sample_spec); + + /* There are more than two blocks that need to be written. It seems that + * the socket has not been accepting data fast enough (could be due to + * hiccups in the wireless transmission). We need to discard everything + * older than two block sizes to keep the latency from growing. */ + if (bytes_to_send > 2 * u->write_block_size) { + uint64_t skip_bytes; + pa_memchunk tmp; + size_t mempool_max_block_size = pa_mempool_block_size_max(u->core->mempool); + pa_usec_t skip_usec; + + skip_bytes = bytes_to_send - 2 * u->write_block_size; + skip_usec = pa_bytes_to_usec(skip_bytes, &u->encoder_sample_spec); + + pa_log_debug("Skipping %llu us (= %llu bytes) in audio stream", + (unsigned long long) skip_usec, + (unsigned long long) skip_bytes); + + while (skip_bytes > 0) { + size_t bytes_to_render; + + if (skip_bytes > mempool_max_block_size) + bytes_to_render = mempool_max_block_size; + else + bytes_to_render = skip_bytes; + + pa_sink_render_full(u->sink, bytes_to_render, &tmp); + pa_memblock_unref(tmp.memblock); + u->write_index += bytes_to_render; + skip_bytes -= bytes_to_render; + } + + if (u->write_index > 0 && u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK) { + size_t new_write_block_size = u->a2dp_codec->reduce_encoder_bitrate(u->encoder_info, u->write_link_mtu); + if (new_write_block_size) { + u->write_block_size = new_write_block_size; + handle_sink_block_size_change(u); + } + } + } + + blocks_to_write = 1; + } + + /* If the stream is writable, send some data if necessary */ + if (writable && blocks_to_write > 0) { + int result; + + if ((result = write_block(u)) < 0) + goto fail; + + blocks_to_write -= result; + writable = false; + if (result) + have_written = true; + } + + /* If nothing was written during this iteration, either the stream + * is not writable or there was no write pending. Set up a timer that + * will wake up the thread when the next data needs to be written. */ + if (!have_written) { + pa_usec_t sleep_for; + pa_usec_t next_write_at; + + if (writable) { + /* There was no write pending on this iteration of the loop. + * Let's estimate when we need to wake up next */ + next_write_at = pa_bytes_to_usec(u->write_index, &u->encoder_sample_spec); + sleep_for = time_passed < next_write_at ? next_write_at - time_passed : 0; + /* pa_log("Sleeping for %lu; time passed %lu, next write at %lu", (unsigned long) sleep_for, (unsigned long) time_passed, (unsigned long)next_write_at); */ + } else + /* We could not write because the stream was not ready. Let's try + * again in 500 ms and drop audio if we still can't write. The + * thread will also be woken up when we can write again. */ + sleep_for = PA_USEC_PER_MSEC * 500; + + pa_rtpoll_set_timer_relative(u->rtpoll, sleep_for); + disable_timer = false; + } + } + } + + /* Set events to wake up the thread */ + pollfd->events = (short) (((have_sink && !writable) ? POLLOUT : 0) | (have_source ? POLLIN : 0)); + + } + + if (disable_timer) + pa_rtpoll_set_timer_disabled(u->rtpoll); + + if ((ret = pa_rtpoll_run(u->rtpoll)) < 0) { + pa_log_debug("pa_rtpoll_run failed with: %d", ret); + goto fail; + } + + if (ret == 0) { + pa_log_debug("IO thread shutdown requested, stopping cleanly"); + transport_release(u); + goto finish; + } + } + +fail: + /* If this was no regular exit from the loop we have to continue processing messages until we receive PA_MESSAGE_SHUTDOWN */ + pa_log_debug("IO thread failed"); + pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), BLUETOOTH_MESSAGE_IO_THREAD_FAILED, NULL, 0, NULL, NULL); + pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN); + +finish: + pa_log_debug("IO thread shutting down"); +} + +/* Run from main thread */ +static int start_thread(struct userdata *u) { + pa_assert(u); + pa_assert(!u->thread); + pa_assert(!u->rtpoll); + pa_assert(!u->rtpoll_item); + + u->rtpoll = pa_rtpoll_new(); + + if (pa_thread_mq_init(&u->thread_mq, u->core->mainloop, u->rtpoll) < 0) { + pa_log("pa_thread_mq_init() failed."); + return -1; + } + + if (!(u->thread = pa_thread_new("bluetooth", thread_func, u))) { + pa_log_error("Failed to create IO thread"); + return -1; + } + + if (u->sink) { + pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq); + pa_sink_set_rtpoll(u->sink, u->rtpoll); + + /* If we are in the headset role, the sink should not become default + * unless there is no other sound device available. */ + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY) + u->sink->priority = 1500; + + pa_sink_put(u->sink); + + if (u->sink->set_volume) + u->sink->set_volume(u->sink); + } + + if (u->source) { + pa_source_set_asyncmsgq(u->source, u->thread_mq.inq); + pa_source_set_rtpoll(u->source, u->rtpoll); + + /* If we are in the headset role or the device is an a2dp source, + * the source should not become default unless there is no other + * sound device available. */ + if (u->profile == PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) + u->source->priority = 1500; + + pa_source_put(u->source); + + if (u->source->set_volume) + u->source->set_volume(u->source); + } + + return 0; +} + +/* Run from main thread */ +static void stop_thread(struct userdata *u) { + pa_assert(u); + + if (u->sink) + pa_sink_unlink(u->sink); + + if (u->source) + pa_source_unlink(u->source); + + if (u->thread) { + pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL); + pa_thread_free(u->thread); + u->thread = NULL; + } + + if (u->rtpoll_item) { + pa_rtpoll_item_free(u->rtpoll_item); + u->rtpoll_item = NULL; + } + + if (u->rtpoll) { + pa_rtpoll_free(u->rtpoll); + u->rtpoll = NULL; + pa_thread_mq_done(&u->thread_mq); + } + + if (u->transport) { + transport_release(u); + u->transport = NULL; + } + + if (u->sink) { + pa_sink_unref(u->sink); + u->sink = NULL; + } + + if (u->source) { + pa_source_unref(u->source); + u->source = NULL; + } + + if (u->read_smoother) { + pa_smoother_free(u->read_smoother); + u->read_smoother = NULL; + } + + if (u->profile == PA_BLUETOOTH_PROFILE_A2DP_SINK || u->profile == PA_BLUETOOTH_PROFILE_A2DP_SOURCE) { + if (u->encoder_info) { + u->a2dp_codec->deinit(u->encoder_info); + u->encoder_info = NULL; + } + + if (u->decoder_info) { + u->a2dp_codec->deinit(u->decoder_info); + u->decoder_info = NULL; + } + + u->a2dp_codec = NULL; + } +} + +/* Run from main thread */ +static pa_available_t get_port_availability(struct userdata *u, pa_direction_t direction) { + pa_available_t result = PA_AVAILABLE_NO; + unsigned i; + + pa_assert(u); + pa_assert(u->device); + + for (i = 0; i < PA_BLUETOOTH_PROFILE_COUNT; i++) { + pa_bluetooth_transport *transport; + + if (!(get_profile_direction(i) & direction)) + continue; + + if (!(transport = u->device->transports[i])) + continue; + + switch(transport->state) { + case PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED: + continue; + + case PA_BLUETOOTH_TRANSPORT_STATE_IDLE: + if (result == PA_AVAILABLE_NO) + result = PA_AVAILABLE_UNKNOWN; + + break; + + case PA_BLUETOOTH_TRANSPORT_STATE_PLAYING: + return PA_AVAILABLE_YES; + } + } + + return result; +} + +/* Run from main thread */ +static pa_available_t transport_state_to_availability(pa_bluetooth_transport_state_t state) { + switch (state) { + case PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED: + return PA_AVAILABLE_NO; + case PA_BLUETOOTH_TRANSPORT_STATE_PLAYING: + return PA_AVAILABLE_YES; + default: + return PA_AVAILABLE_UNKNOWN; + } +} + +/* Run from main thread */ +static void create_card_ports(struct userdata *u, pa_hashmap *ports) { + pa_device_port *port; + pa_device_port_new_data port_data; + pa_device_port_type_t input_type, output_type; + const char *name_prefix, *input_description, *output_description; + + pa_assert(u); + pa_assert(ports); + pa_assert(u->device); + + name_prefix = "unknown"; + input_description = _("Bluetooth Input"); + output_description = _("Bluetooth Output"); + input_type = output_type = PA_DEVICE_PORT_TYPE_BLUETOOTH; + + switch (form_factor_from_class(u->device->class_of_device)) { + case PA_BLUETOOTH_FORM_FACTOR_HEADSET: + name_prefix = "headset"; + input_description = output_description = _("Headset"); + input_type = output_type = PA_DEVICE_PORT_TYPE_HEADSET; + break; + + case PA_BLUETOOTH_FORM_FACTOR_HANDSFREE: + name_prefix = "handsfree"; + input_description = output_description = _("Handsfree"); + input_type = output_type = PA_DEVICE_PORT_TYPE_HANDSFREE; + break; + + case PA_BLUETOOTH_FORM_FACTOR_MICROPHONE: + name_prefix = "microphone"; + input_description = _("Microphone"); + output_description = _("Bluetooth Output"); + input_type = PA_DEVICE_PORT_TYPE_MIC; + break; + + case PA_BLUETOOTH_FORM_FACTOR_SPEAKER: + name_prefix = "speaker"; + input_description = _("Bluetooth Input"); + output_description = _("Speaker"); + output_type = PA_DEVICE_PORT_TYPE_SPEAKER; + break; + + case PA_BLUETOOTH_FORM_FACTOR_HEADPHONE: + name_prefix = "headphone"; + input_description = _("Bluetooth Input"); + output_description = _("Headphone"); + output_type = PA_DEVICE_PORT_TYPE_HEADPHONES; + break; + + case PA_BLUETOOTH_FORM_FACTOR_PORTABLE: + name_prefix = "portable"; + input_description = output_description = _("Portable"); + input_type = output_type = PA_DEVICE_PORT_TYPE_PORTABLE; + break; + + case PA_BLUETOOTH_FORM_FACTOR_CAR: + name_prefix = "car"; + input_description = output_description = _("Car"); + input_type = output_type = PA_DEVICE_PORT_TYPE_CAR; + break; + + case PA_BLUETOOTH_FORM_FACTOR_HIFI: + name_prefix = "hifi"; + input_description = output_description = _("HiFi"); + input_type = output_type = PA_DEVICE_PORT_TYPE_HIFI; + break; + + case PA_BLUETOOTH_FORM_FACTOR_PHONE: + name_prefix = "phone"; + input_description = output_description = _("Phone"); + input_type = output_type = PA_DEVICE_PORT_TYPE_PHONE; + break; + + case PA_BLUETOOTH_FORM_FACTOR_UNKNOWN: + break; + } + + u->output_port_name = pa_sprintf_malloc("%s-output", name_prefix); + pa_device_port_new_data_init(&port_data); + pa_device_port_new_data_set_name(&port_data, u->output_port_name); + pa_device_port_new_data_set_description(&port_data, output_description); + pa_device_port_new_data_set_direction(&port_data, PA_DIRECTION_OUTPUT); + pa_device_port_new_data_set_type(&port_data, output_type); + pa_device_port_new_data_set_available(&port_data, get_port_availability(u, PA_DIRECTION_OUTPUT)); + pa_assert_se(port = pa_device_port_new(u->core, &port_data, 0)); + pa_assert_se(pa_hashmap_put(ports, port->name, port) >= 0); + pa_device_port_new_data_done(&port_data); + + u->input_port_name = pa_sprintf_malloc("%s-input", name_prefix); + pa_device_port_new_data_init(&port_data); + pa_device_port_new_data_set_name(&port_data, u->input_port_name); + pa_device_port_new_data_set_description(&port_data, input_description); + pa_device_port_new_data_set_direction(&port_data, PA_DIRECTION_INPUT); + pa_device_port_new_data_set_type(&port_data, input_type); + pa_device_port_new_data_set_available(&port_data, get_port_availability(u, PA_DIRECTION_INPUT)); + pa_assert_se(port = pa_device_port_new(u->core, &port_data, 0)); + pa_assert_se(pa_hashmap_put(ports, port->name, port) >= 0); + pa_device_port_new_data_done(&port_data); +} + +/* Run from main thread */ +static pa_card_profile *create_card_profile(struct userdata *u, pa_bluetooth_profile_t profile, pa_hashmap *ports) { + pa_device_port *input_port, *output_port; + const char *name; + pa_card_profile *cp = NULL; + pa_bluetooth_profile_t *p; + + pa_assert(u->input_port_name); + pa_assert(u->output_port_name); + pa_assert_se(input_port = pa_hashmap_get(ports, u->input_port_name)); + pa_assert_se(output_port = pa_hashmap_get(ports, u->output_port_name)); + + name = pa_bluetooth_profile_to_string(profile); + + switch (profile) { + case PA_BLUETOOTH_PROFILE_A2DP_SINK: + cp = pa_card_profile_new(name, _("High Fidelity Playback (A2DP Sink)"), sizeof(pa_bluetooth_profile_t)); + cp->priority = 40; + cp->n_sinks = 1; + cp->n_sources = 0; + cp->max_sink_channels = 2; + cp->max_source_channels = 0; + pa_hashmap_put(output_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + break; + + case PA_BLUETOOTH_PROFILE_A2DP_SOURCE: + cp = pa_card_profile_new(name, _("High Fidelity Capture (A2DP Source)"), sizeof(pa_bluetooth_profile_t)); + cp->priority = 20; + cp->n_sinks = 0; + cp->n_sources = 1; + cp->max_sink_channels = 0; + cp->max_source_channels = 2; + pa_hashmap_put(input_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + break; + + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: + cp = pa_card_profile_new(name, _("Headset Head Unit (HSP/HFP)"), sizeof(pa_bluetooth_profile_t)); + cp->priority = 30; + cp->n_sinks = 1; + cp->n_sources = 1; + cp->max_sink_channels = 1; + cp->max_source_channels = 1; + pa_hashmap_put(input_port->profiles, cp->name, cp); + pa_hashmap_put(output_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + break; + + case PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY: + cp = pa_card_profile_new(name, _("Headset Audio Gateway (HSP/HFP)"), sizeof(pa_bluetooth_profile_t)); + cp->priority = 10; + cp->n_sinks = 1; + cp->n_sources = 1; + cp->max_sink_channels = 1; + cp->max_source_channels = 1; + pa_hashmap_put(input_port->profiles, cp->name, cp); + pa_hashmap_put(output_port->profiles, cp->name, cp); + + p = PA_CARD_PROFILE_DATA(cp); + break; + + case PA_BLUETOOTH_PROFILE_OFF: + pa_assert_not_reached(); + } + + *p = profile; + + if (u->device->transports[*p]) + cp->available = transport_state_to_availability(u->device->transports[*p]->state); + else + cp->available = PA_AVAILABLE_NO; + + return cp; +} + +/* Run from main thread */ +static int set_profile_cb(pa_card *c, pa_card_profile *new_profile) { + struct userdata *u; + pa_bluetooth_profile_t *p; + + pa_assert(c); + pa_assert(new_profile); + pa_assert_se(u = c->userdata); + + p = PA_CARD_PROFILE_DATA(new_profile); + + if (*p != PA_BLUETOOTH_PROFILE_OFF) { + const pa_bluetooth_device *d = u->device; + + if (!d->transports[*p] || d->transports[*p]->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) { + pa_log_warn("Refused to switch profile to %s: Not connected", new_profile->name); + return -PA_ERR_IO; + } + } + + stop_thread(u); + + u->profile = *p; + + if (u->profile != PA_BLUETOOTH_PROFILE_OFF) + if (init_profile(u) < 0) + goto off; + + if (u->sink || u->source) + if (start_thread(u) < 0) + goto off; + + return 0; + +off: + stop_thread(u); + + pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0); + + return -PA_ERR_IO; +} + +static int uuid_to_profile(const char *uuid, pa_bluetooth_profile_t *_r) { + if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK)) + *_r = PA_BLUETOOTH_PROFILE_A2DP_SINK; + else if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE)) + *_r = PA_BLUETOOTH_PROFILE_A2DP_SOURCE; + else if (pa_bluetooth_uuid_is_hsp_hs(uuid) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_HF)) + *_r = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT; + else if (pa_streq(uuid, PA_BLUETOOTH_UUID_HSP_AG) || pa_streq(uuid, PA_BLUETOOTH_UUID_HFP_AG)) + *_r = PA_BLUETOOTH_PROFILE_HEADSET_AUDIO_GATEWAY; + else + return -PA_ERR_INVALID; + + return 0; +} + +/* Run from main thread */ +static int add_card(struct userdata *u) { + const pa_bluetooth_device *d; + pa_card_new_data data; + char *alias; + pa_bluetooth_form_factor_t ff; + pa_card_profile *cp; + pa_bluetooth_profile_t *p; + const char *uuid; + void *state; + + pa_assert(u); + pa_assert(u->device); + + d = u->device; + + pa_card_new_data_init(&data); + data.driver = __FILE__; + data.module = u->module; + + alias = pa_utf8_filter(d->alias); + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_DESCRIPTION, alias); + pa_xfree(alias); + + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, d->address); + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_API, "bluez"); + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_CLASS, "sound"); + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_BUS, "bluetooth"); + + if ((ff = form_factor_from_class(d->class_of_device)) != PA_BLUETOOTH_FORM_FACTOR_UNKNOWN) + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_FORM_FACTOR, form_factor_to_string(ff)); + + pa_proplist_sets(data.proplist, "bluez.path", d->path); + pa_proplist_setf(data.proplist, "bluez.class", "0x%06x", d->class_of_device); + pa_proplist_sets(data.proplist, "bluez.alias", d->alias); + data.name = pa_sprintf_malloc("bluez_card.%s", d->address); + data.namereg_fail = false; + + create_card_ports(u, data.ports); + + PA_HASHMAP_FOREACH(uuid, d->uuids, state) { + pa_bluetooth_profile_t profile; + + if (uuid_to_profile(uuid, &profile) < 0) + continue; + + if (pa_hashmap_get(data.profiles, pa_bluetooth_profile_to_string(profile))) + continue; + + cp = create_card_profile(u, profile, data.ports); + pa_hashmap_put(data.profiles, cp->name, cp); + } + + pa_assert(!pa_hashmap_isempty(data.profiles)); + + cp = pa_card_profile_new("off", _("Off"), sizeof(pa_bluetooth_profile_t)); + cp->available = PA_AVAILABLE_YES; + p = PA_CARD_PROFILE_DATA(cp); + *p = PA_BLUETOOTH_PROFILE_OFF; + pa_hashmap_put(data.profiles, cp->name, cp); + + u->card = pa_card_new(u->core, &data); + pa_card_new_data_done(&data); + if (!u->card) { + pa_log("Failed to allocate card."); + return -1; + } + + u->card->userdata = u; + u->card->set_profile = set_profile_cb; + pa_card_choose_initial_profile(u->card); + pa_card_put(u->card); + + p = PA_CARD_PROFILE_DATA(u->card->active_profile); + u->profile = *p; + + return 0; +} + +/* Run from main thread */ +static void handle_transport_state_change(struct userdata *u, struct pa_bluetooth_transport *t) { + bool acquire = false; + bool release = false; + pa_card_profile *cp; + pa_device_port *port; + pa_available_t oldavail; + + pa_assert(u); + pa_assert(t); + pa_assert_se(cp = pa_hashmap_get(u->card->profiles, pa_bluetooth_profile_to_string(t->profile))); + + oldavail = cp->available; + pa_card_profile_set_available(cp, transport_state_to_availability(t->state)); + + /* Update port availability */ + pa_assert_se(port = pa_hashmap_get(u->card->ports, u->output_port_name)); + pa_device_port_set_available(port, get_port_availability(u, PA_DIRECTION_OUTPUT)); + pa_assert_se(port = pa_hashmap_get(u->card->ports, u->input_port_name)); + pa_device_port_set_available(port, get_port_availability(u, PA_DIRECTION_INPUT)); + + /* Acquire or release transport as needed */ + acquire = (t->state == PA_BLUETOOTH_TRANSPORT_STATE_PLAYING && u->profile == t->profile); + release = (oldavail != PA_AVAILABLE_NO && t->state != PA_BLUETOOTH_TRANSPORT_STATE_PLAYING && u->profile == t->profile); + + if (acquire && transport_acquire(u, true) >= 0) { + if (u->source) { + pa_log_debug("Resuming source %s because its transport state changed to playing", u->source->name); + + /* When the ofono backend resumes source or sink when in the audio gateway role, the + * state of source or sink may already be RUNNING before the transport is acquired via + * hf_audio_agent_new_connection(), so the pa_source_suspend() call will not lead to a + * state change message. In this case we explicitly need to signal the I/O thread to + * set up the stream. */ + if (PA_SOURCE_IS_OPENED(u->source->state)) + pa_asyncmsgq_send(u->source->asyncmsgq, PA_MSGOBJECT(u->source), PA_SOURCE_MESSAGE_SETUP_STREAM, NULL, 0, NULL); + + /* We remove the IDLE suspend cause, because otherwise + * module-loopback doesn't uncork its streams. FIXME: Messing with + * the IDLE suspend cause here is wrong, the correct way to handle + * this would probably be to uncork the loopback streams not only + * when the other end is unsuspended, but also when the other end's + * suspend cause changes to IDLE only (currently there's no + * notification mechanism for suspend cause changes, though). */ + pa_source_suspend(u->source, false, PA_SUSPEND_IDLE|PA_SUSPEND_USER); + } + + if (u->sink) { + pa_log_debug("Resuming sink %s because its transport state changed to playing", u->sink->name); + + /* Same comment as above */ + if (PA_SINK_IS_OPENED(u->sink->state)) + pa_asyncmsgq_send(u->sink->asyncmsgq, PA_MSGOBJECT(u->sink), PA_SINK_MESSAGE_SETUP_STREAM, NULL, 0, NULL); + + /* FIXME: See the previous comment. */ + pa_sink_suspend(u->sink, false, PA_SUSPEND_IDLE|PA_SUSPEND_USER); + } + } + + if (release && u->transport_acquired) { + /* FIXME: this release is racy, since the audio stream might have + * been set up again in the meantime (but not processed yet by PA). + * BlueZ should probably release the transport automatically, and in + * that case we would just mark the transport as released */ + + /* Remote side closed the stream so we consider it PA_SUSPEND_USER */ + if (u->source) { + pa_log_debug("Suspending source %s because the remote end closed the stream", u->source->name); + pa_source_suspend(u->source, true, PA_SUSPEND_USER); + } + + if (u->sink) { + pa_log_debug("Suspending sink %s because the remote end closed the stream", u->sink->name); + pa_sink_suspend(u->sink, true, PA_SUSPEND_USER); + } + } +} + +/* Run from main thread */ +static pa_hook_result_t device_connection_changed_cb(pa_bluetooth_discovery *y, const pa_bluetooth_device *d, struct userdata *u) { + pa_assert(d); + pa_assert(u); + + if (d != u->device || pa_bluetooth_device_any_transport_connected(d)) + return PA_HOOK_OK; + + pa_log_debug("Unloading module for device %s", d->path); + pa_module_unload(u->module, true); + + return PA_HOOK_OK; +} + +/* Run from main thread */ +static pa_hook_result_t transport_state_changed_cb(pa_bluetooth_discovery *y, pa_bluetooth_transport *t, struct userdata *u) { + pa_assert(t); + pa_assert(u); + + if (t == u->transport && t->state <= PA_BLUETOOTH_TRANSPORT_STATE_DISCONNECTED) + pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0); + + if (t->device == u->device) + handle_transport_state_change(u, t); + + return PA_HOOK_OK; +} + +static pa_hook_result_t transport_speaker_gain_changed_cb(pa_bluetooth_discovery *y, pa_bluetooth_transport *t, struct userdata *u) { + pa_volume_t volume; + pa_cvolume v; + uint16_t gain; + + pa_assert(t); + pa_assert(u); + + if (t != u->transport) + return PA_HOOK_OK; + + gain = t->speaker_gain; + volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN); + + /* increment volume by one to correct rounding errors */ + if (volume < PA_VOLUME_NORM) + volume++; + + pa_cvolume_set(&v, u->encoder_sample_spec.channels, volume); + if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) + pa_sink_volume_changed(u->sink, &v); + else + pa_sink_set_volume(u->sink, &v, true, true); + + return PA_HOOK_OK; +} + +static pa_hook_result_t transport_microphone_gain_changed_cb(pa_bluetooth_discovery *y, pa_bluetooth_transport *t, struct userdata *u) { + pa_volume_t volume; + pa_cvolume v; + uint16_t gain; + + pa_assert(t); + pa_assert(u); + + if (t != u->transport) + return PA_HOOK_OK; + + gain = t->microphone_gain; + volume = (pa_volume_t) (gain * PA_VOLUME_NORM / HSP_MAX_GAIN); + + /* increment volume by one to correct rounding errors */ + if (volume < PA_VOLUME_NORM) + volume++; + + pa_cvolume_set(&v, u->decoder_sample_spec.channels, volume); + + if (t->profile == PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT) + pa_source_volume_changed(u->source, &v); + else + pa_source_set_volume(u->source, &v, true, true); + + return PA_HOOK_OK; +} + +/* Run from main thread context */ +static int device_process_msg(pa_msgobject *obj, int code, void *data, int64_t offset, pa_memchunk *chunk) { + struct bluetooth_msg *m = BLUETOOTH_MSG(obj); + struct userdata *u = m->card->userdata; + + switch (code) { + case BLUETOOTH_MESSAGE_IO_THREAD_FAILED: + if (m->card->module->unload_requested) + break; + + pa_log_debug("Switching the profile to off due to IO thread failure."); + pa_assert_se(pa_card_set_profile(m->card, pa_hashmap_get(m->card->profiles, "off"), false) >= 0); + break; + case BLUETOOTH_MESSAGE_STREAM_FD_HUP: + if (u->transport->state > PA_BLUETOOTH_TRANSPORT_STATE_IDLE) + pa_bluetooth_transport_set_state(u->transport, PA_BLUETOOTH_TRANSPORT_STATE_IDLE); + break; + case BLUETOOTH_MESSAGE_SET_TRANSPORT_PLAYING: + /* transport_acquired needs to be checked here, because a message could have been + * pending when the profile was switched. If the new transport has been acquired + * correctly, the call below will have no effect because the transport state is + * already PLAYING. If transport_acquire() failed for the new profile, the transport + * state should not be changed. If the transport has been released for other reasons + * (I/O thread shutdown), transport_acquired will also be false. */ + if (u->transport_acquired) + pa_bluetooth_transport_set_state(u->transport, PA_BLUETOOTH_TRANSPORT_STATE_PLAYING); + break; + } + + return 0; +} + +int pa__init(pa_module* m) { + struct userdata *u; + const char *path; + pa_modargs *ma; + bool autodetect_mtu; + + pa_assert(m); + + m->userdata = u = pa_xnew0(struct userdata, 1); + u->module = m; + u->core = m->core; + + if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { + pa_log_error("Failed to parse module arguments"); + goto fail_free_modargs; + } + + if (!(path = pa_modargs_get_value(ma, "path", NULL))) { + pa_log_error("Failed to get device path from module arguments"); + goto fail_free_modargs; + } + + if ((u->discovery = pa_shared_get(u->core, "bluetooth-discovery"))) + pa_bluetooth_discovery_ref(u->discovery); + else { + pa_log_error("module-bluez5-discover doesn't seem to be loaded, refusing to load module-bluez5-device"); + goto fail_free_modargs; + } + + if (!(u->device = pa_bluetooth_discovery_get_device_by_path(u->discovery, path))) { + pa_log_error("%s is unknown", path); + goto fail_free_modargs; + } + + autodetect_mtu = false; + if (pa_modargs_get_value_boolean(ma, "autodetect_mtu", &autodetect_mtu) < 0) { + pa_log("Invalid boolean value for autodetect_mtu parameter"); + goto fail_free_modargs; + } + + u->device->autodetect_mtu = autodetect_mtu; + + pa_modargs_free(ma); + + u->device_connection_changed_slot = + pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED), + PA_HOOK_NORMAL, (pa_hook_cb_t) device_connection_changed_cb, u); + + u->transport_state_changed_slot = + pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_STATE_CHANGED), + PA_HOOK_NORMAL, (pa_hook_cb_t) transport_state_changed_cb, u); + + u->transport_speaker_gain_changed_slot = + pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SPEAKER_GAIN_CHANGED), PA_HOOK_NORMAL, (pa_hook_cb_t) transport_speaker_gain_changed_cb, u); + + u->transport_microphone_gain_changed_slot = + pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_MICROPHONE_GAIN_CHANGED), PA_HOOK_NORMAL, (pa_hook_cb_t) transport_microphone_gain_changed_cb, u); + + if (add_card(u) < 0) + goto fail; + + if (!(u->msg = pa_msgobject_new(bluetooth_msg))) + goto fail; + + u->msg->parent.process_msg = device_process_msg; + u->msg->card = u->card; + u->stream_setup_done = false; + + if (u->profile != PA_BLUETOOTH_PROFILE_OFF) + if (init_profile(u) < 0) + goto off; + + if (u->sink || u->source) + if (start_thread(u) < 0) + goto off; + + return 0; + +off: + stop_thread(u); + + pa_assert_se(pa_card_set_profile(u->card, pa_hashmap_get(u->card->profiles, "off"), false) >= 0); + + return 0; + +fail_free_modargs: + + if (ma) + pa_modargs_free(ma); + +fail: + + pa__done(m); + + return -1; +} + +void pa__done(pa_module *m) { + struct userdata *u; + + pa_assert(m); + + if (!(u = m->userdata)) + return; + + stop_thread(u); + + if (u->device_connection_changed_slot) + pa_hook_slot_free(u->device_connection_changed_slot); + + if (u->transport_state_changed_slot) + pa_hook_slot_free(u->transport_state_changed_slot); + + if (u->transport_speaker_gain_changed_slot) + pa_hook_slot_free(u->transport_speaker_gain_changed_slot); + + if (u->transport_microphone_gain_changed_slot) + pa_hook_slot_free(u->transport_microphone_gain_changed_slot); + + if (u->encoder_buffer) + pa_xfree(u->encoder_buffer); + + if (u->decoder_buffer) + pa_xfree(u->decoder_buffer); + + if (u->msg) + pa_xfree(u->msg); + + if (u->card) + pa_card_free(u->card); + + if (u->discovery) + pa_bluetooth_discovery_unref(u->discovery); + + pa_xfree(u->output_port_name); + pa_xfree(u->input_port_name); + + pa_xfree(u); +} + +int pa__get_n_used(pa_module *m) { + struct userdata *u; + + pa_assert(m); + pa_assert_se(u = m->userdata); + + return (u->sink ? pa_sink_linked_by(u->sink) : 0) + (u->source ? pa_source_linked_by(u->source) : 0); +} diff --git a/src/modules/bluetooth/module-bluez5-discover.c b/src/modules/bluetooth/module-bluez5-discover.c new file mode 100644 index 0000000..47b5761 --- /dev/null +++ b/src/modules/bluetooth/module-bluez5-discover.c @@ -0,0 +1,173 @@ +/*** + This file is part of PulseAudio. + + Copyright 2008-2013 João Paulo Rechi Vita + + PulseAudio 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. + + PulseAudio 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 + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <pulsecore/core.h> +#include <pulsecore/core-util.h> +#include <pulsecore/macro.h> +#include <pulsecore/module.h> +#include <pulsecore/modargs.h> +#include <pulsecore/shared.h> + +#include "bluez5-util.h" + +PA_MODULE_AUTHOR("João Paulo Rechi Vita"); +PA_MODULE_DESCRIPTION("Detect available BlueZ 5 Bluetooth audio devices and load BlueZ 5 Bluetooth audio drivers"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(true); +PA_MODULE_USAGE( + "headset=ofono|native|auto" + "autodetect_mtu=<boolean>" +); + +static const char* const valid_modargs[] = { + "headset", + "autodetect_mtu", + NULL +}; + +struct userdata { + pa_module *module; + pa_core *core; + pa_hashmap *loaded_device_paths; + pa_hook_slot *device_connection_changed_slot; + pa_bluetooth_discovery *discovery; + bool autodetect_mtu; +}; + +static pa_hook_result_t device_connection_changed_cb(pa_bluetooth_discovery *y, const pa_bluetooth_device *d, struct userdata *u) { + bool module_loaded; + + pa_assert(d); + pa_assert(u); + + module_loaded = pa_hashmap_get(u->loaded_device_paths, d->path) ? true : false; + + if (module_loaded && !pa_bluetooth_device_any_transport_connected(d)) { + /* disconnection, the module unloads itself */ + pa_log_debug("Unregistering module for %s", d->path); + pa_hashmap_remove(u->loaded_device_paths, d->path); + return PA_HOOK_OK; + } + + if (!module_loaded && pa_bluetooth_device_any_transport_connected(d)) { + /* a new device has been connected */ + pa_module *m; + char *args = pa_sprintf_malloc("path=%s autodetect_mtu=%i", d->path, (int)u->autodetect_mtu); + + pa_log_debug("Loading module-bluez5-device %s", args); + pa_module_load(&m, u->module->core, "module-bluez5-device", args); + pa_xfree(args); + + if (m) + /* No need to duplicate the path here since the device object will + * exist for the whole hashmap entry lifespan */ + pa_hashmap_put(u->loaded_device_paths, d->path, d->path); + else + pa_log_warn("Failed to load module for device %s", d->path); + + return PA_HOOK_OK; + } + + return PA_HOOK_OK; +} + +#ifdef HAVE_BLUEZ_5_NATIVE_HEADSET +const char *default_headset_backend = "auto"; +#else +const char *default_headset_backend = "ofono"; +#endif + +int pa__init(pa_module *m) { + struct userdata *u; + pa_modargs *ma; + const char *headset_str; + int headset_backend; + bool autodetect_mtu; + + pa_assert(m); + + if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { + pa_log("failed to parse module arguments."); + goto fail; + } + + pa_assert_se(headset_str = pa_modargs_get_value(ma, "headset", default_headset_backend)); + if (pa_streq(headset_str, "ofono")) + headset_backend = HEADSET_BACKEND_OFONO; + else if (pa_streq(headset_str, "native")) + headset_backend = HEADSET_BACKEND_NATIVE; + else if (pa_streq(headset_str, "auto")) + headset_backend = HEADSET_BACKEND_AUTO; + else { + pa_log("headset parameter must be either ofono, native or auto (found %s)", headset_str); + goto fail; + } + + autodetect_mtu = false; + if (pa_modargs_get_value_boolean(ma, "autodetect_mtu", &autodetect_mtu) < 0) { + pa_log("Invalid boolean value for autodetect_mtu parameter"); + goto fail; + } + + m->userdata = u = pa_xnew0(struct userdata, 1); + u->module = m; + u->core = m->core; + u->autodetect_mtu = autodetect_mtu; + u->loaded_device_paths = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + + if (!(u->discovery = pa_bluetooth_discovery_get(u->core, headset_backend))) + goto fail; + + u->device_connection_changed_slot = + pa_hook_connect(pa_bluetooth_discovery_hook(u->discovery, PA_BLUETOOTH_HOOK_DEVICE_CONNECTION_CHANGED), + PA_HOOK_NORMAL, (pa_hook_cb_t) device_connection_changed_cb, u); + + pa_modargs_free(ma); + return 0; + +fail: + if (ma) + pa_modargs_free(ma); + pa__done(m); + return -1; +} + +void pa__done(pa_module *m) { + struct userdata *u; + + pa_assert(m); + + if (!(u = m->userdata)) + return; + + if (u->device_connection_changed_slot) + pa_hook_slot_free(u->device_connection_changed_slot); + + if (u->loaded_device_paths) + pa_hashmap_free(u->loaded_device_paths); + + if (u->discovery) + pa_bluetooth_discovery_unref(u->discovery); + + pa_xfree(u); +} diff --git a/src/modules/bluetooth/rtp.h b/src/modules/bluetooth/rtp.h new file mode 100644 index 0000000..813d9e3 --- /dev/null +++ b/src/modules/bluetooth/rtp.h @@ -0,0 +1,80 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org> + * Copyright (C) 2019 Pali Rohár <pali.rohar@gmail.com> + * + * + * 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, see <http://www.gnu.org/licenses/>. + */ + +#include <endian.h> +#include <stdint.h> + +#if defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && \ + __BYTE_ORDER == __LITTLE_ENDIAN + +struct rtp_header { + uint8_t cc:4; + uint8_t x:1; + uint8_t p:1; + uint8_t v:2; + + uint8_t pt:7; + uint8_t m:1; + + uint16_t sequence_number; + uint32_t timestamp; + uint32_t ssrc; + uint32_t csrc[0]; +} __attribute__ ((packed)); + +struct rtp_sbc_payload { + uint8_t frame_count:4; + uint8_t rfa0:1; + uint8_t is_last_fragment:1; + uint8_t is_first_fragment:1; + uint8_t is_fragmented:1; +} __attribute__ ((packed)); + +#elif defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && \ + __BYTE_ORDER == __BIG_ENDIAN + +struct rtp_header { + uint8_t v:2; + uint8_t p:1; + uint8_t x:1; + uint8_t cc:4; + + uint8_t m:1; + uint8_t pt:7; + + uint16_t sequence_number; + uint32_t timestamp; + uint32_t ssrc; + uint32_t csrc[0]; +} __attribute__ ((packed)); + +struct rtp_sbc_payload { + uint8_t is_fragmented:1; + uint8_t is_first_fragment:1; + uint8_t is_last_fragment:1; + uint8_t rfa0:1; + uint8_t frame_count:4; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif |