2023-02-08 18:12:00 +01:00
|
|
|
/* Spa Bluez5 Device */
|
|
|
|
|
/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */
|
|
|
|
|
/* SPDX-License-Identifier: MIT */
|
2018-11-26 12:18:53 +01:00
|
|
|
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <poll.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
|
|
|
|
|
#include <spa/support/log.h>
|
|
|
|
|
#include <spa/utils/type.h>
|
2019-06-03 16:48:01 +02:00
|
|
|
#include <spa/utils/keys.h>
|
2019-06-21 13:31:34 +02:00
|
|
|
#include <spa/utils/names.h>
|
2021-05-18 11:36:13 +10:00
|
|
|
#include <spa/utils/string.h>
|
2019-12-19 13:15:10 +01:00
|
|
|
#include <spa/node/node.h>
|
2018-11-26 12:18:53 +01:00
|
|
|
#include <spa/support/loop.h>
|
|
|
|
|
#include <spa/support/plugin.h>
|
2021-04-15 17:42:02 +02:00
|
|
|
#include <spa/support/i18n.h>
|
2018-11-26 12:18:53 +01:00
|
|
|
#include <spa/monitor/device.h>
|
2019-05-20 10:14:00 +02:00
|
|
|
#include <spa/monitor/utils.h>
|
2021-01-07 18:10:22 +01:00
|
|
|
#include <spa/monitor/event.h>
|
2020-01-03 13:01:54 +01:00
|
|
|
#include <spa/pod/filter.h>
|
|
|
|
|
#include <spa/pod/parser.h>
|
|
|
|
|
#include <spa/param/param.h>
|
2021-01-07 18:10:22 +01:00
|
|
|
#include <spa/param/audio/raw.h>
|
2021-03-21 02:38:26 +02:00
|
|
|
#include <spa/param/bluetooth/audio.h>
|
|
|
|
|
#include <spa/param/bluetooth/type-info.h>
|
2020-01-03 13:01:54 +01:00
|
|
|
#include <spa/debug/pod.h>
|
2023-01-18 17:41:16 +01:00
|
|
|
#include <spa/debug/log.h>
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2018-11-27 17:08:36 +01:00
|
|
|
#include "defs.h"
|
2022-06-15 17:24:41 +02:00
|
|
|
#include "media-codecs.h"
|
2018-11-27 17:08:36 +01:00
|
|
|
|
2021-10-01 19:03:49 +03:00
|
|
|
static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.device");
|
|
|
|
|
#undef SPA_LOG_TOPIC_DEFAULT
|
|
|
|
|
#define SPA_LOG_TOPIC_DEFAULT &log_topic
|
2018-11-26 12:18:53 +01:00
|
|
|
|
|
|
|
|
#define MAX_DEVICES 64
|
|
|
|
|
|
2021-02-02 23:12:35 +02:00
|
|
|
#define DEVICE_ID_SOURCE 0
|
|
|
|
|
#define DEVICE_ID_SINK 1
|
2023-03-13 19:17:34 +02:00
|
|
|
#define DEVICE_ID_SOURCE_SET 2
|
|
|
|
|
#define DEVICE_ID_SINK_SET 3
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
#define DYNAMIC_NODE_ID_FLAG 0x1000
|
|
|
|
|
|
2021-04-15 17:42:02 +02:00
|
|
|
static struct spa_i18n *_i18n;
|
|
|
|
|
|
|
|
|
|
#define _(_str) spa_i18n_text(_i18n,(_str))
|
|
|
|
|
#define N_(_str) (_str)
|
|
|
|
|
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
enum {
|
|
|
|
|
DEVICE_PROFILE_OFF = 0,
|
|
|
|
|
DEVICE_PROFILE_AG = 1,
|
|
|
|
|
DEVICE_PROFILE_A2DP = 2,
|
|
|
|
|
DEVICE_PROFILE_HSP_HFP = 3,
|
2022-06-17 15:12:24 +02:00
|
|
|
DEVICE_PROFILE_BAP = 4,
|
2022-10-09 20:54:06 +03:00
|
|
|
DEVICE_PROFILE_LAST = DEVICE_PROFILE_BAP,
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
};
|
2021-02-02 23:12:35 +02:00
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
struct props {
|
2021-03-21 02:38:26 +02:00
|
|
|
enum spa_bluetooth_audio_codec codec;
|
2022-10-01 15:46:27 +03:00
|
|
|
bool offload_active;
|
2018-11-26 12:18:53 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void reset_props(struct props *props)
|
|
|
|
|
{
|
2021-03-21 02:38:26 +02:00
|
|
|
props->codec = 0;
|
2022-10-01 15:46:27 +03:00
|
|
|
props->offload_active = false;
|
2018-11-26 12:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
struct impl;
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
struct node {
|
2021-04-15 11:39:49 +08:00
|
|
|
struct impl *impl;
|
|
|
|
|
struct spa_bt_transport *transport;
|
|
|
|
|
struct spa_hook transport_listener;
|
2021-01-07 18:10:22 +01:00
|
|
|
uint32_t id;
|
|
|
|
|
unsigned int active:1;
|
|
|
|
|
unsigned int mute:1;
|
2021-03-30 16:31:17 +02:00
|
|
|
unsigned int save:1;
|
2021-08-21 18:37:44 +03:00
|
|
|
unsigned int a2dp_duplex:1;
|
2022-10-01 15:46:27 +03:00
|
|
|
unsigned int offload_acquired:1;
|
2021-01-07 18:10:22 +01:00
|
|
|
uint32_t n_channels;
|
2021-02-13 22:59:04 +02:00
|
|
|
int64_t latency_offset;
|
2021-01-07 18:10:22 +01:00
|
|
|
uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
|
|
|
|
|
float volumes[SPA_AUDIO_MAX_CHANNELS];
|
2021-04-15 11:39:49 +08:00
|
|
|
float soft_volumes[SPA_AUDIO_MAX_CHANNELS];
|
2021-01-07 18:10:22 +01:00
|
|
|
};
|
|
|
|
|
|
2021-03-17 22:00:44 +02:00
|
|
|
struct dynamic_node
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
struct spa_bt_transport *transport;
|
|
|
|
|
struct spa_hook transport_listener;
|
|
|
|
|
uint32_t id;
|
|
|
|
|
const char *factory_name;
|
2021-08-15 19:11:12 +03:00
|
|
|
bool a2dp_duplex;
|
2021-03-17 22:00:44 +02:00
|
|
|
};
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
struct device_set_member {
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
struct spa_bt_transport *transport;
|
|
|
|
|
struct spa_hook listener;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct device_set {
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
char *path;
|
|
|
|
|
bool leader;
|
|
|
|
|
uint32_t sinks;
|
|
|
|
|
uint32_t sources;
|
|
|
|
|
struct device_set_member sink[SPA_AUDIO_MAX_CHANNELS];
|
|
|
|
|
struct device_set_member source[SPA_AUDIO_MAX_CHANNELS];
|
|
|
|
|
};
|
|
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
struct impl {
|
|
|
|
|
struct spa_handle handle;
|
|
|
|
|
struct spa_device device;
|
|
|
|
|
|
|
|
|
|
struct spa_log *log;
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
uint32_t info_all;
|
|
|
|
|
struct spa_device_info info;
|
|
|
|
|
#define IDX_EnumProfile 0
|
|
|
|
|
#define IDX_Profile 1
|
|
|
|
|
#define IDX_EnumRoute 2
|
|
|
|
|
#define IDX_Route 3
|
2021-03-21 02:38:26 +02:00
|
|
|
#define IDX_PropInfo 4
|
|
|
|
|
#define IDX_Props 5
|
|
|
|
|
struct spa_param_info params[6];
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2019-03-01 12:00:42 +01:00
|
|
|
struct spa_hook_list hooks;
|
2018-11-26 12:18:53 +01:00
|
|
|
|
|
|
|
|
struct props props;
|
|
|
|
|
|
2018-11-27 17:08:36 +01:00
|
|
|
struct spa_bt_device *bt_dev;
|
2021-01-25 19:57:45 +02:00
|
|
|
struct spa_hook bt_dev_listener;
|
2019-07-31 12:11:56 -04:00
|
|
|
|
2020-01-03 13:01:54 +01:00
|
|
|
uint32_t profile;
|
2021-01-29 19:41:26 +02:00
|
|
|
unsigned int switching_codec:1;
|
2022-01-06 16:26:11 +02:00
|
|
|
unsigned int save_profile:1;
|
2021-01-29 19:41:26 +02:00
|
|
|
uint32_t prev_bt_connected_profiles;
|
2021-01-25 19:57:45 +02:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
struct device_set device_set;
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec **supported_codecs;
|
2021-01-25 23:55:09 +02:00
|
|
|
size_t supported_codec_count;
|
|
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
struct dynamic_node dyn_media_source;
|
|
|
|
|
struct dynamic_node dyn_media_sink;
|
2021-03-18 22:44:36 +02:00
|
|
|
struct dynamic_node dyn_sco_source;
|
|
|
|
|
struct dynamic_node dyn_sco_sink;
|
2021-03-17 22:00:44 +02:00
|
|
|
|
2021-03-14 17:53:31 +08:00
|
|
|
#define MAX_SETTINGS 32
|
|
|
|
|
struct spa_dict_item setting_items[MAX_SETTINGS];
|
|
|
|
|
struct spa_dict setting_dict;
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
struct node nodes[4];
|
2018-11-26 12:18:53 +01:00
|
|
|
};
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static void init_node(struct impl *this, struct node *node, uint32_t id)
|
|
|
|
|
{
|
|
|
|
|
uint32_t i;
|
|
|
|
|
|
|
|
|
|
spa_zero(*node);
|
|
|
|
|
node->id = id;
|
2021-06-03 06:08:20 +08:00
|
|
|
for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
|
|
|
|
|
node->volumes[i] = 1.0f;
|
|
|
|
|
node->soft_volumes[i] = 1.0f;
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size)
|
2021-03-21 02:38:26 +02:00
|
|
|
{
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec * const *c;
|
2021-03-21 02:38:26 +02:00
|
|
|
|
2021-08-15 20:12:51 +03:00
|
|
|
spa_assert(size > 0);
|
2021-09-01 00:33:43 +03:00
|
|
|
spa_assert(this->supported_codecs);
|
2021-03-21 02:38:26 +02:00
|
|
|
|
2021-09-01 00:33:43 +03:00
|
|
|
for (c = this->supported_codecs; *c && size > 1; ++c) {
|
2021-08-15 20:12:51 +03:00
|
|
|
if ((*c)->id == id || id == 0) {
|
|
|
|
|
*codecs++ = *c;
|
|
|
|
|
--size;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*codecs = NULL;
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-02 15:06:27 +03:00
|
|
|
static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id,
|
|
|
|
|
size_t *idx, enum spa_bt_profile profile)
|
2021-03-26 18:06:27 +02:00
|
|
|
{
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec *media_codec = NULL;
|
2021-03-26 18:06:27 +02:00
|
|
|
size_t i;
|
2023-09-02 15:06:27 +03:00
|
|
|
|
2022-01-03 16:15:52 +02:00
|
|
|
for (i = 0; i < this->supported_codec_count; ++i) {
|
|
|
|
|
if (this->supported_codecs[i]->id == id) {
|
2022-06-15 17:24:41 +02:00
|
|
|
media_codec = this->supported_codecs[i];
|
2022-01-03 16:15:52 +02:00
|
|
|
if (idx)
|
|
|
|
|
*idx = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-02 15:06:27 +03:00
|
|
|
|
|
|
|
|
if (!media_codec)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
|
|
if (!spa_bt_device_supports_media_codec(this->bt_dev, media_codec, profile))
|
|
|
|
|
return NULL;
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
return media_codec;
|
2021-03-26 18:06:27 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-05 16:08:46 +02:00
|
|
|
static bool is_bap_client(struct impl *this)
|
|
|
|
|
{
|
|
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
|
|
|
|
struct spa_bt_transport *t;
|
|
|
|
|
|
|
|
|
|
spa_list_for_each(t, &device->transport_list, device_link) {
|
|
|
|
|
if (t->bap_initiator)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool can_bap_codec_switch(struct impl *this)
|
|
|
|
|
{
|
|
|
|
|
if (!is_bap_client(this))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
/* XXX: codec switching for source/duplex is not currently
|
|
|
|
|
* XXX: implemented properly. TODO: fix this
|
|
|
|
|
*/
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
static unsigned int get_hfp_codec(enum spa_bluetooth_audio_codec id)
|
|
|
|
|
{
|
|
|
|
|
switch (id) {
|
|
|
|
|
case SPA_BLUETOOTH_AUDIO_CODEC_CVSD:
|
|
|
|
|
return HFP_AUDIO_CODEC_CVSD;
|
|
|
|
|
case SPA_BLUETOOTH_AUDIO_CODEC_MSBC:
|
|
|
|
|
return HFP_AUDIO_CODEC_MSBC;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static enum spa_bluetooth_audio_codec get_hfp_codec_id(unsigned int codec)
|
|
|
|
|
{
|
|
|
|
|
switch (codec) {
|
|
|
|
|
case HFP_AUDIO_CODEC_MSBC:
|
|
|
|
|
return SPA_BLUETOOTH_AUDIO_CODEC_MSBC;
|
|
|
|
|
case HFP_AUDIO_CODEC_CVSD:
|
|
|
|
|
return SPA_BLUETOOTH_AUDIO_CODEC_CVSD;
|
|
|
|
|
}
|
|
|
|
|
return SPA_ID_INVALID;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 00:11:12 +02:00
|
|
|
static const char *get_hfp_codec_description(unsigned int codec)
|
2021-03-18 23:15:03 +02:00
|
|
|
{
|
|
|
|
|
switch (codec) {
|
|
|
|
|
case HFP_AUDIO_CODEC_MSBC:
|
|
|
|
|
return "mSBC";
|
|
|
|
|
case HFP_AUDIO_CODEC_CVSD:
|
|
|
|
|
return "CVSD";
|
|
|
|
|
}
|
|
|
|
|
return "unknown";
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 00:11:12 +02:00
|
|
|
static const char *get_hfp_codec_name(unsigned int codec)
|
2021-03-18 23:15:03 +02:00
|
|
|
{
|
|
|
|
|
switch (codec) {
|
|
|
|
|
case HFP_AUDIO_CODEC_MSBC:
|
|
|
|
|
return "msbc";
|
|
|
|
|
case HFP_AUDIO_CODEC_CVSD:
|
|
|
|
|
return "cvsd";
|
|
|
|
|
}
|
|
|
|
|
return "unknown";
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-15 19:11:12 +03:00
|
|
|
static const char *get_codec_name(struct spa_bt_transport *t, bool a2dp_duplex)
|
2021-02-12 17:20:37 +01:00
|
|
|
{
|
2022-06-15 17:24:41 +02:00
|
|
|
if (t->media_codec != NULL) {
|
|
|
|
|
if (a2dp_duplex && t->media_codec->duplex_codec)
|
|
|
|
|
return t->media_codec->duplex_codec->name;
|
|
|
|
|
return t->media_codec->name;
|
2021-08-15 19:11:12 +03:00
|
|
|
}
|
2021-03-20 00:11:12 +02:00
|
|
|
return get_hfp_codec_name(t->codec);
|
2021-02-12 17:20:37 +01:00
|
|
|
}
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
static void transport_destroy(void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct node *node = userdata;
|
|
|
|
|
node->transport = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:48:25 +08:00
|
|
|
static void emit_node_props(struct impl *this, struct node *node, bool full)
|
2021-04-15 11:39:49 +08:00
|
|
|
{
|
|
|
|
|
struct spa_event *event;
|
|
|
|
|
uint8_t buffer[4096];
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
|
|
|
|
struct spa_pod_frame f[1];
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
spa_pod_builder_push_object(&b, &f[0],
|
|
|
|
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
|
|
|
|
|
spa_pod_builder_int(&b, node->id);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
|
|
|
|
spa_pod_builder_add_object(&b,
|
|
|
|
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
|
|
|
|
|
SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
|
2021-04-29 12:27:55 +02:00
|
|
|
SPA_TYPE_Float, node->n_channels, node->volumes),
|
|
|
|
|
SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float),
|
2021-04-15 11:39:49 +08:00
|
|
|
SPA_TYPE_Float, node->n_channels, node->soft_volumes),
|
|
|
|
|
SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
|
|
|
|
|
SPA_TYPE_Id, node->n_channels, node->channels));
|
2021-06-03 05:48:25 +08:00
|
|
|
if (full) {
|
|
|
|
|
spa_pod_builder_add(&b,
|
|
|
|
|
SPA_PROP_mute, SPA_POD_Bool(node->mute),
|
|
|
|
|
SPA_PROP_softMute, SPA_POD_Bool(node->mute),
|
|
|
|
|
SPA_PROP_latencyOffsetNsec, SPA_POD_Long(node->latency_offset),
|
|
|
|
|
0);
|
|
|
|
|
}
|
2021-04-15 11:39:49 +08:00
|
|
|
event = spa_pod_builder_pop(&b, &f[0]);
|
|
|
|
|
|
|
|
|
|
spa_device_emit_event(&this->hooks, event);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:48:25 +08:00
|
|
|
static void emit_volume(struct impl *this, struct node *node)
|
|
|
|
|
{
|
|
|
|
|
emit_node_props(this, node, false);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
static void emit_info(struct impl *this, bool full);
|
|
|
|
|
|
2021-08-21 18:37:44 +03:00
|
|
|
static float get_soft_volume_boost(struct node *node)
|
|
|
|
|
{
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec *codec = node->transport ? node->transport->media_codec : NULL;
|
2022-05-22 18:22:55 +03:00
|
|
|
|
2021-08-21 18:37:44 +03:00
|
|
|
/*
|
|
|
|
|
* For A2DP duplex, the duplex microphone channel sometimes does not appear
|
|
|
|
|
* to have hardware gain, and input volume is very low.
|
|
|
|
|
*
|
|
|
|
|
* Work around this by boosting the software volume level, i.e. adjust
|
|
|
|
|
* the scale on the user-visible volume control to something more sensible.
|
|
|
|
|
* If this causes clipping, the user can just reduce the mic volume to
|
|
|
|
|
* bring SW gain below 1.
|
|
|
|
|
*/
|
2022-05-22 18:22:55 +03:00
|
|
|
if (node->a2dp_duplex && node->transport && codec && codec->info &&
|
|
|
|
|
spa_atob(spa_dict_lookup(codec->info, "duplex.boost")) &&
|
2021-08-21 18:37:44 +03:00
|
|
|
node->id == DEVICE_ID_SOURCE &&
|
|
|
|
|
!node->transport->volumes[SPA_BT_VOLUME_ID_RX].active)
|
|
|
|
|
return 10.0f; /* 20 dB boost */
|
|
|
|
|
|
|
|
|
|
/* In all other cases, no boost */
|
|
|
|
|
return 1.0f;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
static float node_get_hw_volume(struct node *node)
|
|
|
|
|
{
|
|
|
|
|
uint32_t i;
|
|
|
|
|
float hw_volume = 0.0f;
|
|
|
|
|
for (i = 0; i < node->n_channels; i++)
|
|
|
|
|
hw_volume = SPA_MAX(node->volumes[i], hw_volume);
|
|
|
|
|
return SPA_MIN(hw_volume, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void node_update_soft_volumes(struct node *node, float hw_volume)
|
|
|
|
|
{
|
|
|
|
|
for (uint32_t i = 0; i < node->n_channels; ++i) {
|
|
|
|
|
node->soft_volumes[i] = hw_volume > 0.0f
|
|
|
|
|
? node->volumes[i] / hw_volume
|
|
|
|
|
: 0.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-06 18:52:04 +03:00
|
|
|
static bool node_update_volume_from_transport(struct node *node, bool reset)
|
2021-04-15 11:39:49 +08:00
|
|
|
{
|
|
|
|
|
struct impl *impl = node->impl;
|
|
|
|
|
struct spa_bt_transport_volume *t_volume;
|
|
|
|
|
float prev_hw_volume;
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
if (!node->active || !node->transport || !spa_bt_transport_volume_enabled(node->transport))
|
2022-07-06 18:52:04 +03:00
|
|
|
return false;
|
2021-04-15 11:39:49 +08:00
|
|
|
|
|
|
|
|
/* PW is the controller for remote device. */
|
|
|
|
|
if (impl->profile != DEVICE_PROFILE_A2DP
|
2022-06-17 15:12:24 +02:00
|
|
|
&& impl->profile != DEVICE_PROFILE_BAP
|
2021-04-15 11:39:49 +08:00
|
|
|
&& impl->profile != DEVICE_PROFILE_HSP_HFP)
|
2022-07-06 18:52:04 +03:00
|
|
|
return false;
|
2021-04-15 11:39:49 +08:00
|
|
|
|
|
|
|
|
t_volume = &node->transport->volumes[node->id];
|
|
|
|
|
|
|
|
|
|
if (!t_volume->active)
|
2022-07-06 18:52:04 +03:00
|
|
|
return false;
|
2021-04-15 11:39:49 +08:00
|
|
|
|
|
|
|
|
prev_hw_volume = node_get_hw_volume(node);
|
2022-07-06 18:52:04 +03:00
|
|
|
|
|
|
|
|
if (!reset) {
|
|
|
|
|
for (uint32_t i = 0; i < node->n_channels; ++i) {
|
|
|
|
|
node->volumes[i] = prev_hw_volume > 0.0f
|
|
|
|
|
? node->volumes[i] * t_volume->volume / prev_hw_volume
|
|
|
|
|
: t_volume->volume;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (uint32_t i = 0; i < node->n_channels; ++i)
|
|
|
|
|
node->volumes[i] = t_volume->volume;
|
2021-04-15 11:39:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node_update_soft_volumes(node, t_volume->volume);
|
|
|
|
|
|
2022-07-09 15:14:30 +03:00
|
|
|
/*
|
|
|
|
|
* Consider volume changes from the headset as requested
|
|
|
|
|
* by the user, and to be saved by the SM.
|
|
|
|
|
*/
|
|
|
|
|
node->save = true;
|
|
|
|
|
|
2022-07-06 18:52:04 +03:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void volume_changed(void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct node *node = userdata;
|
|
|
|
|
struct impl *impl = node->impl;
|
|
|
|
|
|
|
|
|
|
if (!node_update_volume_from_transport(node, false))
|
|
|
|
|
return;
|
|
|
|
|
|
2021-06-03 05:26:34 +08:00
|
|
|
emit_volume(impl, node);
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
impl->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
impl->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
emit_info(impl, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct spa_bt_transport_events transport_events = {
|
|
|
|
|
SPA_VERSION_BT_DEVICE_EVENTS,
|
|
|
|
|
.destroy = transport_destroy,
|
|
|
|
|
.volume_changed = volume_changed,
|
|
|
|
|
};
|
|
|
|
|
|
2022-10-01 15:46:27 +03:00
|
|
|
static int node_offload_set_active(struct node *node, bool active)
|
|
|
|
|
{
|
|
|
|
|
int res = 0;
|
|
|
|
|
|
|
|
|
|
if (node->transport == NULL || !node->active)
|
|
|
|
|
return -ENOTSUP;
|
|
|
|
|
|
|
|
|
|
if (active && !node->offload_acquired)
|
|
|
|
|
res = spa_bt_transport_acquire(node->transport, false);
|
|
|
|
|
else if (!active && node->offload_acquired)
|
|
|
|
|
res = spa_bt_transport_release(node->transport);
|
|
|
|
|
|
|
|
|
|
if (res >= 0)
|
|
|
|
|
node->offload_acquired = active;
|
|
|
|
|
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-21 18:37:44 +03:00
|
|
|
static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels, uint32_t *channels)
|
|
|
|
|
{
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec *codec;
|
2021-08-21 18:37:44 +03:00
|
|
|
struct spa_audio_info info = { 0 };
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
if (!a2dp_duplex || !t->media_codec || !t->media_codec->duplex_codec) {
|
2021-08-21 18:37:44 +03:00
|
|
|
*n_channels = t->n_channels;
|
|
|
|
|
memcpy(channels, t->channels, t->n_channels * sizeof(uint32_t));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
codec = t->media_codec->duplex_codec;
|
2021-08-21 18:37:44 +03:00
|
|
|
|
|
|
|
|
if (!codec->validate_config ||
|
|
|
|
|
codec->validate_config(codec, 0,
|
|
|
|
|
t->configuration, t->configuration_len,
|
|
|
|
|
&info) < 0) {
|
|
|
|
|
*n_channels = 1;
|
|
|
|
|
channels[0] = SPA_AUDIO_CHANNEL_MONO;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*n_channels = info.info.raw.channels;
|
|
|
|
|
memcpy(channels, info.info.raw.position,
|
|
|
|
|
info.info.raw.channels * sizeof(uint32_t));
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
static const char *get_channel_name(uint32_t channel)
|
|
|
|
|
{
|
|
|
|
|
int i;
|
|
|
|
|
for (i = 0; spa_type_audio_channel[i].name; i++) {
|
|
|
|
|
if (spa_type_audio_channel[i].type == channel)
|
|
|
|
|
return spa_debug_type_short_name(spa_type_audio_channel[i].name);
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int channel_position_cmp(const void *pa, const void *pb)
|
|
|
|
|
{
|
|
|
|
|
uint32_t a = *(uint32_t *)pa, b = *(uint32_t *)pb;
|
|
|
|
|
return (int)a - (int)b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void emit_device_set_node(struct impl *this, uint32_t id)
|
|
|
|
|
{
|
|
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
|
|
|
|
struct node *node = &this->nodes[id];
|
|
|
|
|
struct spa_device_object_info info;
|
|
|
|
|
struct spa_dict_item items[7];
|
|
|
|
|
char str_id[32], members_json[8192], channels_json[512];
|
|
|
|
|
struct device_set_member *members;
|
|
|
|
|
uint32_t n_members;
|
|
|
|
|
uint32_t n_items = 0;
|
|
|
|
|
struct spa_strbuf json;
|
|
|
|
|
unsigned int i;
|
|
|
|
|
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set", this->device_set.path);
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.leader", "true");
|
|
|
|
|
snprintf(str_id, sizeof(str_id), "%d", id);
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("card.profile.device", str_id);
|
|
|
|
|
|
|
|
|
|
if (id == DEVICE_ID_SOURCE_SET) {
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("media.class", "Audio/Source");
|
|
|
|
|
members = this->device_set.source;
|
|
|
|
|
n_members = this->device_set.sources;
|
|
|
|
|
} else if (id == DEVICE_ID_SINK_SET) {
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("media.class", "Audio/Sink");
|
|
|
|
|
members = this->device_set.sink;
|
|
|
|
|
n_members = this->device_set.sinks;
|
|
|
|
|
} else {
|
|
|
|
|
spa_assert_not_reached();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node->impl = this;
|
|
|
|
|
node->active = true;
|
|
|
|
|
node->transport = NULL;
|
|
|
|
|
node->a2dp_duplex = false;
|
|
|
|
|
node->offload_acquired = false;
|
|
|
|
|
node->mute = false;
|
|
|
|
|
node->save = false;
|
|
|
|
|
node->latency_offset = 0;
|
|
|
|
|
|
|
|
|
|
/* Form channel map from members */
|
|
|
|
|
node->n_channels = 0;
|
|
|
|
|
for (i = 0; i < n_members; ++i) {
|
|
|
|
|
struct spa_bt_transport *t = members[i].transport;
|
|
|
|
|
unsigned int j;
|
|
|
|
|
|
|
|
|
|
for (j = 0; j < t->n_channels; ++j) {
|
|
|
|
|
unsigned int k;
|
|
|
|
|
|
|
|
|
|
if (!get_channel_name(t->channels[j]))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
for (k = 0; k < node->n_channels; ++k) {
|
|
|
|
|
if (node->channels[k] == t->channels[j])
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (k == node->n_channels && node->n_channels < SPA_AUDIO_MAX_CHANNELS)
|
|
|
|
|
node->channels[node->n_channels++] = t->channels[j];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qsort(node->channels, node->n_channels, sizeof(uint32_t), channel_position_cmp);
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < node->n_channels; ++i) {
|
|
|
|
|
/* Session manager will override this, so put in some safe number */
|
|
|
|
|
node->volumes[i] = node->soft_volumes[i] = 0.064;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Produce member info json */
|
|
|
|
|
spa_strbuf_init(&json, members_json, sizeof(members_json));
|
|
|
|
|
spa_strbuf_append(&json, "[");
|
|
|
|
|
for (i = 0; i < n_members; ++i) {
|
|
|
|
|
struct spa_bt_transport *t = members[i].transport;
|
|
|
|
|
char object_path[512];
|
|
|
|
|
unsigned int j;
|
|
|
|
|
int member_id = (id == DEVICE_ID_SINK_SET) ? DEVICE_ID_SINK : DEVICE_ID_SOURCE;
|
|
|
|
|
|
|
|
|
|
if (i > 0)
|
|
|
|
|
spa_strbuf_append(&json, ",");
|
|
|
|
|
spa_scnprintf(object_path, sizeof(object_path), "%s/%s-%d",
|
|
|
|
|
this->device_set.path, t->device->address, member_id);
|
|
|
|
|
spa_strbuf_append(&json, "{\"object.path\":\"%s\",\"channels\":[", object_path);
|
|
|
|
|
for (j = 0; j < t->n_channels; ++j) {
|
|
|
|
|
if (j > 0)
|
|
|
|
|
spa_strbuf_append(&json, ",");
|
|
|
|
|
spa_strbuf_append(&json, "\"%s\"", get_channel_name(t->channels[j]));
|
|
|
|
|
}
|
|
|
|
|
spa_strbuf_append(&json, "]}");
|
|
|
|
|
}
|
|
|
|
|
spa_strbuf_append(&json, "]");
|
|
|
|
|
json.buffer[SPA_MIN(json.pos, json.maxsize-1)] = 0;
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.members", members_json);
|
|
|
|
|
|
|
|
|
|
spa_strbuf_init(&json, channels_json, sizeof(channels_json));
|
|
|
|
|
spa_strbuf_append(&json, "[");
|
|
|
|
|
for (i = 0; i < node->n_channels; ++i) {
|
|
|
|
|
if (i > 0)
|
|
|
|
|
spa_strbuf_append(&json, ",");
|
|
|
|
|
spa_strbuf_append(&json, "\"%s\"", get_channel_name(node->channels[i]));
|
|
|
|
|
}
|
|
|
|
|
spa_strbuf_append(&json, "]");
|
|
|
|
|
json.buffer[SPA_MIN(json.pos, json.maxsize-1)] = 0;
|
|
|
|
|
items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.channels", channels_json);
|
|
|
|
|
|
|
|
|
|
/* Emit */
|
|
|
|
|
info = SPA_DEVICE_OBJECT_INFO_INIT();
|
|
|
|
|
info.type = SPA_TYPE_INTERFACE_Node;
|
|
|
|
|
info.factory_name = (id == DEVICE_ID_SOURCE_SET) ? "source" : "sink";
|
|
|
|
|
info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
|
|
|
|
|
info.props = &SPA_DICT_INIT(items, n_items);
|
|
|
|
|
|
|
|
|
|
spa_device_emit_object_info(&this->hooks, id, &info);
|
|
|
|
|
|
|
|
|
|
emit_node_props(this, &this->nodes[id], true);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static void emit_node(struct impl *this, struct spa_bt_transport *t,
|
2021-08-15 19:11:12 +03:00
|
|
|
uint32_t id, const char *factory_name, bool a2dp_duplex)
|
2018-11-26 12:18:53 +01:00
|
|
|
{
|
2021-01-10 20:53:59 +01:00
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
2021-01-07 18:10:22 +01:00
|
|
|
struct spa_device_object_info info;
|
2023-03-13 19:17:34 +02:00
|
|
|
struct spa_dict_item items[11];
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
uint32_t n_items = 0;
|
2023-03-13 19:17:34 +02:00
|
|
|
char transport[32], str_id[32], object_path[512];
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG);
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2023-10-24 18:59:27 +03:00
|
|
|
spa_log_debug(this->log, "%p: node, transport:%p id:%08x factory:%s", this, t, id, factory_name);
|
2023-09-02 15:06:27 +03:00
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
snprintf(transport, sizeof(transport), "pointer:%p", t);
|
|
|
|
|
items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport);
|
|
|
|
|
items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PROFILE, spa_bt_profile_name(t->profile));
|
2021-08-15 19:11:12 +03:00
|
|
|
items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CODEC, get_codec_name(t, a2dp_duplex));
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
|
2021-03-22 16:37:52 +01:00
|
|
|
items[4] = SPA_DICT_ITEM_INIT("device.routes", "1");
|
|
|
|
|
n_items = 5;
|
2023-03-13 19:17:34 +02:00
|
|
|
if (!is_dyn_node && !this->device_set.path) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
snprintf(str_id, sizeof(str_id), "%d", id);
|
2023-03-13 19:17:34 +02:00
|
|
|
items[n_items] = SPA_DICT_ITEM_INIT("card.profile.device", str_id);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
n_items++;
|
|
|
|
|
}
|
2021-08-12 20:40:05 +05:30
|
|
|
if (spa_streq(spa_bt_profile_name(t->profile), "headset-head-unit")) {
|
|
|
|
|
items[n_items] = SPA_DICT_ITEM_INIT("device.intended-roles", "Communication");
|
|
|
|
|
n_items++;
|
|
|
|
|
}
|
2021-08-15 19:11:12 +03:00
|
|
|
if (a2dp_duplex) {
|
|
|
|
|
items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.a2dp-duplex", "true");
|
|
|
|
|
n_items++;
|
|
|
|
|
}
|
2023-03-13 19:17:34 +02:00
|
|
|
if (this->device_set.path) {
|
|
|
|
|
items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.set", this->device_set.path);
|
|
|
|
|
n_items++;
|
|
|
|
|
items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.internal", "true");
|
|
|
|
|
n_items++;
|
|
|
|
|
|
|
|
|
|
/* object.path can be used in match rules with only basic node props */
|
|
|
|
|
spa_scnprintf(object_path, sizeof(object_path), "%s/%s-%d",
|
|
|
|
|
this->device_set.path, device->address, id);
|
|
|
|
|
items[n_items] = SPA_DICT_ITEM_INIT("object.path", object_path);
|
|
|
|
|
n_items++;
|
|
|
|
|
}
|
2019-05-16 13:18:45 +02:00
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
info = SPA_DEVICE_OBJECT_INFO_INIT();
|
|
|
|
|
info.type = SPA_TYPE_INTERFACE_Node;
|
|
|
|
|
info.factory_name = factory_name;
|
|
|
|
|
info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
info.props = &SPA_DICT_INIT(items, n_items);
|
2019-04-19 13:26:07 -04:00
|
|
|
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_device_emit_object_info(&this->hooks, id, &info);
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
if (this->device_set.path) {
|
|
|
|
|
/* Device set member nodes don't have their own routes */
|
|
|
|
|
this->nodes[id].impl = this;
|
|
|
|
|
this->nodes[id].active = false;
|
|
|
|
|
if (this->nodes[id].transport)
|
|
|
|
|
spa_hook_remove(&this->nodes[id].transport_listener);
|
|
|
|
|
this->nodes[id].transport = NULL;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
if (!is_dyn_node) {
|
2021-08-21 18:37:44 +03:00
|
|
|
uint32_t prev_channels = this->nodes[id].n_channels;
|
|
|
|
|
float boost;
|
2021-03-15 23:07:46 +02:00
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
this->nodes[id].impl = this;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
this->nodes[id].active = true;
|
2022-10-01 15:46:27 +03:00
|
|
|
this->nodes[id].offload_acquired = false;
|
2021-08-21 18:37:44 +03:00
|
|
|
this->nodes[id].a2dp_duplex = a2dp_duplex;
|
|
|
|
|
get_channels(t, a2dp_duplex, &this->nodes[id].n_channels, this->nodes[id].channels);
|
2021-04-15 11:39:49 +08:00
|
|
|
if (this->nodes[id].transport)
|
|
|
|
|
spa_hook_remove(&this->nodes[id].transport_listener);
|
|
|
|
|
this->nodes[id].transport = t;
|
|
|
|
|
spa_bt_transport_add_listener(t, &this->nodes[id].transport_listener, &transport_events, &this->nodes[id]);
|
2021-06-03 05:48:25 +08:00
|
|
|
|
2021-08-21 18:37:44 +03:00
|
|
|
if (prev_channels > 0) {
|
|
|
|
|
size_t i;
|
|
|
|
|
/*
|
|
|
|
|
* Spread mono volume to all channels, if we had switched HFP -> A2DP.
|
|
|
|
|
* XXX: we should also use different route for hfp and a2dp
|
|
|
|
|
*/
|
|
|
|
|
for (i = prev_channels; i < this->nodes[id].n_channels; ++i)
|
|
|
|
|
this->nodes[id].volumes[i] = this->nodes[id].volumes[i % prev_channels];
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-06 18:52:04 +03:00
|
|
|
node_update_volume_from_transport(&this->nodes[id], true);
|
|
|
|
|
|
2021-08-21 18:37:44 +03:00
|
|
|
boost = get_soft_volume_boost(&this->nodes[id]);
|
|
|
|
|
if (boost != 1.0f) {
|
|
|
|
|
size_t i;
|
|
|
|
|
for (i = 0; i < this->nodes[id].n_channels; ++i)
|
|
|
|
|
this->nodes[id].soft_volumes[i] = this->nodes[id].volumes[i] * boost;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:48:25 +08:00
|
|
|
emit_node_props(this, &this->nodes[id], true);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
}
|
2019-04-19 13:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
static struct spa_bt_transport *find_device_transport(struct spa_bt_device *device, int profile,
|
|
|
|
|
enum spa_bluetooth_audio_codec codec)
|
2019-04-19 13:26:07 -04:00
|
|
|
{
|
2019-07-31 12:11:56 -04:00
|
|
|
struct spa_bt_transport *t;
|
2021-03-21 02:38:26 +02:00
|
|
|
|
|
|
|
|
spa_list_for_each(t, &device->transport_list, device_link) {
|
2021-08-15 20:12:51 +03:00
|
|
|
bool codec_ok = codec == 0 ||
|
2022-06-15 17:24:41 +02:00
|
|
|
(t->media_codec != NULL && t->media_codec->id == codec) ||
|
2021-08-15 20:12:51 +03:00
|
|
|
get_hfp_codec_id(t->codec) == codec;
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
if ((t->profile & device->connected_profiles) &&
|
|
|
|
|
(t->profile & profile) == t->profile &&
|
2021-08-15 20:12:51 +03:00
|
|
|
codec_ok)
|
2021-03-21 02:38:26 +02:00
|
|
|
return t;
|
2018-11-27 17:08:36 +01:00
|
|
|
}
|
2021-01-25 19:57:45 +02:00
|
|
|
|
2020-01-10 13:25:40 +01:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
static struct spa_bt_transport *find_transport(struct impl *this, int profile, enum spa_bluetooth_audio_codec codec)
|
|
|
|
|
{
|
|
|
|
|
return find_device_transport(this->bt_dev, profile, codec);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-17 22:00:44 +02:00
|
|
|
static void dynamic_node_transport_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct dynamic_node *this = data;
|
|
|
|
|
spa_log_debug(this->impl->log, "transport %p destroy", this->transport);
|
2021-04-15 11:39:49 +08:00
|
|
|
this->transport = NULL;
|
2021-03-17 22:00:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void dynamic_node_transport_state_changed(void *data,
|
|
|
|
|
enum spa_bt_transport_state old,
|
|
|
|
|
enum spa_bt_transport_state state)
|
|
|
|
|
{
|
|
|
|
|
struct dynamic_node *this = data;
|
|
|
|
|
struct impl *impl = this->impl;
|
|
|
|
|
struct spa_bt_transport *t = this->transport;
|
|
|
|
|
|
|
|
|
|
spa_log_debug(impl->log, "transport %p state %d->%d", t, old, state);
|
|
|
|
|
|
|
|
|
|
if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
if (!SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) {
|
|
|
|
|
SPA_FLAG_SET(this->id, DYNAMIC_NODE_ID_FLAG);
|
2022-01-31 00:08:26 +02:00
|
|
|
spa_bt_transport_keepalive(t, true);
|
2021-08-15 19:11:12 +03:00
|
|
|
emit_node(impl, t, this->id, this->factory_name, this->a2dp_duplex);
|
2021-03-17 22:00:44 +02:00
|
|
|
}
|
|
|
|
|
} else if (state < SPA_BT_TRANSPORT_STATE_PENDING && old >= SPA_BT_TRANSPORT_STATE_PENDING) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
if (SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) {
|
|
|
|
|
SPA_FLAG_CLEAR(this->id, DYNAMIC_NODE_ID_FLAG);
|
2022-01-31 00:08:26 +02:00
|
|
|
spa_bt_transport_keepalive(t, false);
|
2021-03-17 22:00:44 +02:00
|
|
|
spa_device_emit_object_info(&impl->hooks, this->id, NULL);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
static void dynamic_node_volume_changed(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct dynamic_node *node = data;
|
|
|
|
|
struct impl *impl = node->impl;
|
|
|
|
|
struct spa_event *event;
|
|
|
|
|
uint8_t buffer[4096];
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
|
|
|
|
struct spa_pod_frame f[1];
|
|
|
|
|
struct spa_bt_transport_volume *t_volume;
|
2021-04-22 06:52:47 +08:00
|
|
|
int id = node->id, volume_id;
|
|
|
|
|
|
|
|
|
|
SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
|
2021-04-15 11:39:49 +08:00
|
|
|
|
|
|
|
|
/* Remote device is the controller */
|
2021-04-17 18:53:28 +08:00
|
|
|
if (!node->transport || impl->profile != DEVICE_PROFILE_AG
|
|
|
|
|
|| !spa_bt_transport_volume_enabled(node->transport))
|
2021-04-15 11:39:49 +08:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (id == 0 || id == 2)
|
|
|
|
|
volume_id = SPA_BT_VOLUME_ID_RX;
|
|
|
|
|
else if (id == 1)
|
|
|
|
|
volume_id = SPA_BT_VOLUME_ID_TX;
|
|
|
|
|
else
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
t_volume = &node->transport->volumes[volume_id];
|
|
|
|
|
if (!t_volume->active)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
spa_pod_builder_push_object(&b, &f[0],
|
|
|
|
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
|
|
|
|
|
spa_pod_builder_int(&b, id);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
|
|
|
|
spa_pod_builder_add_object(&b,
|
|
|
|
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
|
|
|
|
|
SPA_PROP_volume, SPA_POD_Float(t_volume->volume));
|
|
|
|
|
event = spa_pod_builder_pop(&b, &f[0]);
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_debug(impl->log, "dynamic node %d: volume %d changed %f, profile %d",
|
|
|
|
|
node->id, volume_id, t_volume->volume, node->transport->profile);
|
2021-04-15 11:39:49 +08:00
|
|
|
|
|
|
|
|
/* Dynamic node doesn't has route, we can only set volume on adaptar node. */
|
|
|
|
|
spa_device_emit_event(&impl->hooks, event);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-17 22:00:44 +02:00
|
|
|
static const struct spa_bt_transport_events dynamic_node_transport_events = {
|
|
|
|
|
SPA_VERSION_BT_TRANSPORT_EVENTS,
|
|
|
|
|
.destroy = dynamic_node_transport_destroy,
|
|
|
|
|
.state_changed = dynamic_node_transport_state_changed,
|
2021-04-15 11:39:49 +08:00
|
|
|
.volume_changed = dynamic_node_volume_changed,
|
2021-03-17 22:00:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void emit_dynamic_node(struct dynamic_node *this, struct impl *impl,
|
2021-08-15 19:11:12 +03:00
|
|
|
struct spa_bt_transport *t, uint32_t id, const char *factory_name, bool a2dp_duplex)
|
2021-03-17 22:00:44 +02:00
|
|
|
{
|
2023-10-24 18:59:27 +03:00
|
|
|
spa_log_debug(impl->log, "%p: dynamic node, transport: %p->%p id: %08x->%08x",
|
|
|
|
|
this, this->transport, t, this->id, id);
|
2021-06-14 20:06:37 +08:00
|
|
|
|
|
|
|
|
if (this->transport) {
|
|
|
|
|
/* Session manager don't really handles transport ptr changing. */
|
|
|
|
|
spa_assert(this->transport == t);
|
|
|
|
|
spa_hook_remove(&this->transport_listener);
|
|
|
|
|
}
|
2021-03-17 22:00:44 +02:00
|
|
|
|
|
|
|
|
this->impl = impl;
|
|
|
|
|
this->transport = t;
|
|
|
|
|
this->id = id;
|
|
|
|
|
this->factory_name = factory_name;
|
2021-08-15 19:11:12 +03:00
|
|
|
this->a2dp_duplex = a2dp_duplex;
|
2021-03-17 22:00:44 +02:00
|
|
|
|
|
|
|
|
spa_bt_transport_add_listener(this->transport,
|
|
|
|
|
&this->transport_listener, &dynamic_node_transport_events, this);
|
|
|
|
|
|
|
|
|
|
/* emits the node if the state is already pending */
|
|
|
|
|
dynamic_node_transport_state_changed (this, SPA_BT_TRANSPORT_STATE_IDLE, t->state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void remove_dynamic_node(struct dynamic_node *this)
|
|
|
|
|
{
|
|
|
|
|
if (this->transport == NULL)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
/* destroy the node, if it exists */
|
|
|
|
|
dynamic_node_transport_state_changed (this, this->transport->state,
|
|
|
|
|
SPA_BT_TRANSPORT_STATE_IDLE);
|
|
|
|
|
|
|
|
|
|
spa_hook_remove(&this->transport_listener);
|
|
|
|
|
this->impl = NULL;
|
|
|
|
|
this->transport = NULL;
|
|
|
|
|
this->id = 0;
|
|
|
|
|
this->factory_name = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
static void device_set_clear(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
struct device_set *set = &impl->device_set;
|
|
|
|
|
unsigned int i;
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i) {
|
|
|
|
|
if (set->sink[i].transport)
|
|
|
|
|
spa_hook_remove(&set->sink[i].listener);
|
|
|
|
|
if (set->source[i].transport)
|
|
|
|
|
spa_hook_remove(&set->source[i].listener);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free(set->path);
|
|
|
|
|
spa_zero(*set);
|
|
|
|
|
set->impl = impl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void device_set_transport_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct device_set_member *member = data;
|
|
|
|
|
|
|
|
|
|
member->transport = NULL;
|
|
|
|
|
spa_hook_remove(&member->listener);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct spa_bt_transport_events device_set_transport_events = {
|
|
|
|
|
SPA_VERSION_BT_DEVICE_EVENTS,
|
|
|
|
|
.destroy = device_set_transport_destroy,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void device_set_update(struct impl *this)
|
|
|
|
|
{
|
|
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
|
|
|
|
struct device_set *dset = &this->device_set;
|
|
|
|
|
struct spa_bt_set_membership *set;
|
|
|
|
|
|
|
|
|
|
device_set_clear(this);
|
|
|
|
|
|
|
|
|
|
if (!is_bap_client(this))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
spa_list_for_each(set, &device->set_membership_list, link) {
|
|
|
|
|
struct spa_bt_set_membership *s;
|
|
|
|
|
int num_devices = 0;
|
|
|
|
|
|
|
|
|
|
spa_bt_for_each_set_member(s, set) {
|
|
|
|
|
struct spa_bt_transport *t;
|
|
|
|
|
bool active = false;
|
|
|
|
|
|
|
|
|
|
if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_DUPLEX))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
t = find_device_transport(s->device, SPA_BT_PROFILE_BAP_SOURCE, 0);
|
|
|
|
|
if (t && t->bap_initiator) {
|
|
|
|
|
active = true;
|
|
|
|
|
dset->source[dset->sources].transport = t;
|
|
|
|
|
spa_bt_transport_add_listener(t, &dset->source[dset->sources].listener,
|
|
|
|
|
&device_set_transport_events, &dset->source[dset->sources]);
|
|
|
|
|
++dset->sources;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t = find_device_transport(s->device, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
|
|
|
|
|
if (t && t->bap_initiator) {
|
|
|
|
|
active = true;
|
|
|
|
|
dset->sink[dset->sinks].transport = t;
|
|
|
|
|
spa_bt_transport_add_listener(t, &dset->sink[dset->sinks].listener,
|
|
|
|
|
&device_set_transport_events, &dset->sink[dset->sinks]);
|
|
|
|
|
++dset->sinks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (active)
|
|
|
|
|
++num_devices;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (num_devices <= 1 || (dset->sinks <= 1 && dset->sources <= 1)) {
|
|
|
|
|
device_set_clear(this);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-24 18:59:27 +03:00
|
|
|
spa_log_debug(this->log, "%p: %s belongs to set %s leader:%d", this,
|
|
|
|
|
device->path, set->path, set->leader);
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
dset->path = strdup(set->path);
|
|
|
|
|
dset->leader = set->leader;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-10 13:25:40 +01:00
|
|
|
static int emit_nodes(struct impl *this)
|
|
|
|
|
{
|
|
|
|
|
struct spa_bt_transport *t;
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
device_set_update(this);
|
|
|
|
|
|
2020-01-10 13:25:40 +01:00
|
|
|
switch (this->profile) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_OFF:
|
2020-07-03 16:12:19 +02:00
|
|
|
break;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_AG:
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) {
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HFP_AG, 0);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
if (!t)
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HSP_AG, 0);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
if (t) {
|
2021-04-21 11:08:45 +08:00
|
|
|
if (t->profile == SPA_BT_PROFILE_HSP_AG)
|
|
|
|
|
this->props.codec = 0;
|
|
|
|
|
else
|
|
|
|
|
this->props.codec = get_hfp_codec_id(t->codec);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
emit_dynamic_node(&this->dyn_sco_source, this, t,
|
2021-08-15 19:11:12 +03:00
|
|
|
0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
emit_dynamic_node(&this->dyn_sco_sink, this, t,
|
2021-08-15 19:11:12 +03:00
|
|
|
1, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-07-22 11:01:18 +02:00
|
|
|
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_A2DP_SOURCE)) {
|
|
|
|
|
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
|
2021-03-21 02:38:26 +02:00
|
|
|
if (t) {
|
2022-06-15 17:24:41 +02:00
|
|
|
this->props.codec = t->media_codec->id;
|
2022-06-17 15:12:24 +02:00
|
|
|
emit_dynamic_node(&this->dyn_media_source, this, t,
|
2022-09-15 23:02:51 +03:00
|
|
|
2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
|
2021-08-15 19:11:12 +03:00
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
if (t->media_codec->duplex_codec) {
|
2022-06-17 15:12:24 +02:00
|
|
|
emit_dynamic_node(&this->dyn_media_sink, this, t,
|
2022-09-15 23:02:51 +03:00
|
|
|
3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
|
2021-08-15 19:11:12 +03:00
|
|
|
}
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case DEVICE_PROFILE_A2DP:
|
2020-07-02 17:12:33 +02:00
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
|
|
|
|
|
if (t) {
|
2022-06-15 17:24:41 +02:00
|
|
|
this->props.codec = t->media_codec->id;
|
2022-06-17 15:12:24 +02:00
|
|
|
emit_dynamic_node(&this->dyn_media_source, this, t,
|
2022-09-15 23:02:51 +03:00
|
|
|
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
|
2021-08-15 19:11:12 +03:00
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
if (t->media_codec->duplex_codec) {
|
2021-08-15 19:11:12 +03:00
|
|
|
emit_node(this, t,
|
2022-09-15 23:02:51 +03:00
|
|
|
DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
|
2021-08-15 19:11:12 +03:00
|
|
|
}
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
2020-07-02 17:12:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->props.codec);
|
|
|
|
|
if (t) {
|
2022-06-15 17:24:41 +02:00
|
|
|
this->props.codec = t->media_codec->id;
|
2022-09-15 23:02:51 +03:00
|
|
|
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, false);
|
2021-08-15 19:11:12 +03:00
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
if (t->media_codec->duplex_codec) {
|
2021-08-15 19:11:12 +03:00
|
|
|
emit_node(this, t,
|
2022-09-15 23:02:51 +03:00
|
|
|
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, true);
|
2021-08-15 19:11:12 +03:00
|
|
|
}
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
2020-07-02 17:12:33 +02:00
|
|
|
}
|
2022-06-17 15:12:24 +02:00
|
|
|
break;
|
|
|
|
|
case DEVICE_PROFILE_BAP:
|
|
|
|
|
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SOURCE)) {
|
|
|
|
|
t = find_transport(this, SPA_BT_PROFILE_BAP_SOURCE, 0);
|
|
|
|
|
if (t) {
|
|
|
|
|
this->props.codec = t->media_codec->id;
|
2022-07-22 11:01:18 +02:00
|
|
|
if (t->bap_initiator)
|
|
|
|
|
emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
|
|
|
|
|
else
|
|
|
|
|
emit_dynamic_node(&this->dyn_media_source, this, t,
|
|
|
|
|
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
|
2022-06-17 15:12:24 +02:00
|
|
|
}
|
2023-03-13 19:17:34 +02:00
|
|
|
|
|
|
|
|
if (this->device_set.leader && this->device_set.sources > 0)
|
|
|
|
|
emit_device_set_node(this, DEVICE_ID_SOURCE_SET);
|
2022-06-17 15:12:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SINK)) {
|
|
|
|
|
t = find_transport(this, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
|
|
|
|
|
if (t) {
|
|
|
|
|
this->props.codec = t->media_codec->id;
|
2022-07-22 11:01:18 +02:00
|
|
|
if (t->bap_initiator)
|
|
|
|
|
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
|
|
|
|
|
else
|
|
|
|
|
emit_dynamic_node(&this->dyn_media_sink, this, t,
|
|
|
|
|
DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
|
2022-06-17 15:12:24 +02:00
|
|
|
}
|
2023-03-13 19:17:34 +02:00
|
|
|
|
|
|
|
|
if (this->device_set.leader && this->device_set.sinks > 0)
|
|
|
|
|
emit_device_set_node(this, DEVICE_ID_SINK_SET);
|
2022-06-17 15:12:24 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-23 22:16:17 +03:00
|
|
|
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SINK)) {
|
|
|
|
|
t = find_transport(this, SPA_BT_PROFILE_BAP_BROADCAST_SINK, this->props.codec);
|
|
|
|
|
if (t) {
|
|
|
|
|
this->props.codec = t->media_codec->id;
|
2023-08-09 13:10:36 +03:00
|
|
|
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
|
2023-07-23 22:16:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->device_set.leader && this->device_set.sinks > 0)
|
|
|
|
|
emit_device_set_node(this, DEVICE_ID_SINK_SET);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-17 18:52:48 +03:00
|
|
|
if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) {
|
|
|
|
|
t = find_transport(this, SPA_BT_PROFILE_BAP_BROADCAST_SOURCE, this->props.codec);
|
|
|
|
|
if (t) {
|
|
|
|
|
this->props.codec = t->media_codec->id;
|
|
|
|
|
emit_dynamic_node(&this->dyn_media_source, this, t,
|
|
|
|
|
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-10 13:25:40 +01:00
|
|
|
break;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) {
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HFP_HF, this->props.codec);
|
2021-03-18 22:44:36 +02:00
|
|
|
if (!t)
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HSP_HS, 0);
|
2021-03-18 22:44:36 +02:00
|
|
|
if (t) {
|
2021-04-21 11:08:45 +08:00
|
|
|
if (t->profile == SPA_BT_PROFILE_HSP_HS)
|
|
|
|
|
this->props.codec = 0;
|
|
|
|
|
else
|
|
|
|
|
this->props.codec = get_hfp_codec_id(t->codec);
|
2021-08-15 19:11:12 +03:00
|
|
|
emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
|
|
|
|
|
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
|
2020-07-02 17:12:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2021-09-19 16:31:08 +03:00
|
|
|
|
|
|
|
|
if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1)
|
|
|
|
|
this->props.codec = 0;
|
2020-01-10 13:25:40 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
2018-11-26 12:18:53 +01:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static const struct spa_dict_item info_items[] = {
|
|
|
|
|
{ SPA_KEY_DEVICE_API, "bluez5" },
|
2021-01-25 13:07:31 +01:00
|
|
|
{ SPA_KEY_DEVICE_BUS, "bluetooth" },
|
2021-01-07 18:10:22 +01:00
|
|
|
{ SPA_KEY_MEDIA_CLASS, "Audio/Device" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void emit_info(struct impl *this, bool full)
|
|
|
|
|
{
|
2021-05-25 15:22:13 +02:00
|
|
|
uint64_t old = full ? this->info.change_mask : 0;
|
2021-01-07 18:10:22 +01:00
|
|
|
if (full)
|
|
|
|
|
this->info.change_mask = this->info_all;
|
|
|
|
|
if (this->info.change_mask) {
|
|
|
|
|
this->info.props = &SPA_DICT_INIT_ARRAY(info_items);
|
|
|
|
|
|
|
|
|
|
spa_device_emit_info(&this->hooks, &this->info);
|
2021-05-25 15:22:13 +02:00
|
|
|
this->info.change_mask = old;
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
static void emit_remove_nodes(struct impl *this)
|
2020-01-10 13:25:40 +01:00
|
|
|
{
|
2023-10-24 18:59:27 +03:00
|
|
|
spa_log_debug(this->log, "%p: remove nodes", this);
|
2023-09-02 15:06:27 +03:00
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
remove_dynamic_node (&this->dyn_media_source);
|
|
|
|
|
remove_dynamic_node (&this->dyn_media_sink);
|
2021-03-18 22:44:36 +02:00
|
|
|
remove_dynamic_node (&this->dyn_sco_source);
|
|
|
|
|
remove_dynamic_node (&this->dyn_sco_sink);
|
2021-03-17 22:00:44 +02:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
for (uint32_t i = 0; i < SPA_N_ELEMENTS(this->nodes); i++) {
|
2021-04-15 11:39:49 +08:00
|
|
|
struct node * node = &this->nodes[i];
|
2022-10-01 15:46:27 +03:00
|
|
|
node_offload_set_active(node, false);
|
2021-04-15 11:39:49 +08:00
|
|
|
if (node->transport) {
|
|
|
|
|
spa_hook_remove(&node->transport_listener);
|
|
|
|
|
node->transport = NULL;
|
|
|
|
|
}
|
|
|
|
|
if (node->active) {
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_device_emit_object_info(&this->hooks, i, NULL);
|
2021-04-15 11:39:49 +08:00
|
|
|
node->active = false;
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
}
|
2022-10-01 15:46:27 +03:00
|
|
|
|
|
|
|
|
this->props.offload_active = false;
|
2021-01-25 19:57:45 +02:00
|
|
|
}
|
|
|
|
|
|
2021-05-11 06:40:13 +08:00
|
|
|
static bool validate_profile(struct impl *this, uint32_t profile,
|
|
|
|
|
enum spa_bluetooth_audio_codec codec);
|
|
|
|
|
|
2022-01-06 16:26:11 +02:00
|
|
|
static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec, bool save)
|
2021-01-25 19:57:45 +02:00
|
|
|
{
|
2021-05-11 06:40:13 +08:00
|
|
|
if (!validate_profile(this, profile, codec)) {
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_warn(this->log, "trying to set invalid profile %d, codec %d, %08x %08x",
|
2021-05-11 06:40:13 +08:00
|
|
|
profile, codec,
|
|
|
|
|
this->bt_dev->profiles, this->bt_dev->connected_profiles);
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-06 16:26:11 +02:00
|
|
|
this->save_profile = save;
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
if (this->profile == profile &&
|
2021-03-21 02:38:26 +02:00
|
|
|
(this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) &&
|
2023-03-13 19:17:34 +02:00
|
|
|
(this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec || this->device_set.path) &&
|
2021-03-21 02:38:26 +02:00
|
|
|
(this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec))
|
2021-01-25 19:57:45 +02:00
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
emit_remove_nodes(this);
|
|
|
|
|
|
2021-03-08 23:39:01 +02:00
|
|
|
spa_bt_device_release_transports(this->bt_dev);
|
|
|
|
|
|
2020-01-10 13:25:40 +01:00
|
|
|
this->profile = profile;
|
2021-03-18 23:15:03 +02:00
|
|
|
this->prev_bt_connected_profiles = this->bt_dev->connected_profiles;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->props.codec = codec;
|
2021-01-25 19:57:45 +02:00
|
|
|
|
2021-02-06 18:47:07 +02:00
|
|
|
/*
|
2022-06-17 15:12:24 +02:00
|
|
|
* A2DP/BAP: ensure there's a transport with the selected codec (0 means any).
|
2021-02-06 18:47:07 +02:00
|
|
|
* Don't try to switch codecs when the device is in the A2DP source role, since
|
|
|
|
|
* devices do not appear to like that.
|
2023-02-05 16:08:46 +02:00
|
|
|
*
|
|
|
|
|
* For BAP, only BAP client can configure the codec.
|
|
|
|
|
*
|
|
|
|
|
* XXX: codec switching also currently does not work in the duplex or
|
|
|
|
|
* XXX: source-only case, as it will only switch the sink, and we only
|
|
|
|
|
* XXX: list the sink codecs here. TODO: fix this
|
2021-02-06 18:47:07 +02:00
|
|
|
*/
|
2023-02-05 16:08:46 +02:00
|
|
|
if ((profile == DEVICE_PROFILE_A2DP || (profile == DEVICE_PROFILE_BAP && can_bap_codec_switch(this)))
|
|
|
|
|
&& !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) {
|
2021-01-25 19:57:45 +02:00
|
|
|
int ret;
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec *codecs[64];
|
2021-01-25 19:57:45 +02:00
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
get_media_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs));
|
2020-07-03 16:12:19 +02:00
|
|
|
|
2021-01-29 19:41:26 +02:00
|
|
|
this->switching_codec = true;
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs);
|
2021-03-18 23:15:03 +02:00
|
|
|
if (ret < 0) {
|
|
|
|
|
if (ret != -ENOTSUP)
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
|
2021-03-18 23:15:03 +02:00
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2021-03-21 02:38:26 +02:00
|
|
|
} else if (profile == DEVICE_PROFILE_HSP_HFP && get_hfp_codec(codec) && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) {
|
2021-03-18 23:15:03 +02:00
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
this->switching_codec = true;
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, get_hfp_codec(codec));
|
2021-03-18 23:15:03 +02:00
|
|
|
if (ret < 0) {
|
|
|
|
|
if (ret != -ENOTSUP)
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
|
2021-03-18 23:15:03 +02:00
|
|
|
} else {
|
2021-01-25 19:57:45 +02:00
|
|
|
return 0;
|
2021-03-18 23:15:03 +02:00
|
|
|
}
|
2021-01-25 19:57:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this->switching_codec = false;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->props.codec = 0;
|
2021-01-07 18:10:22 +01:00
|
|
|
emit_nodes(this);
|
2020-01-10 13:25:40 +01:00
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-01-07 18:10:22 +01:00
|
|
|
emit_info(this, false);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
static void codec_switched(void *userdata, int status)
|
|
|
|
|
{
|
|
|
|
|
struct impl *this = userdata;
|
|
|
|
|
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_debug(this->log, "codec switched (status %d)", status);
|
2021-01-25 19:57:45 +02:00
|
|
|
|
2021-01-29 19:41:26 +02:00
|
|
|
this->switching_codec = false;
|
|
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
if (status < 0) {
|
|
|
|
|
/* Failed to switch: return to a fallback profile */
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_error(this->log, "failed to switch codec (%d), setting fallback profile", status);
|
2021-03-21 02:38:26 +02:00
|
|
|
if (this->profile == DEVICE_PROFILE_A2DP && this->props.codec != 0) {
|
|
|
|
|
this->props.codec = 0;
|
2022-06-17 15:12:24 +02:00
|
|
|
} else if (this->profile == DEVICE_PROFILE_BAP && this->props.codec != 0) {
|
|
|
|
|
this->props.codec = 0;
|
2021-03-21 02:38:26 +02:00
|
|
|
} else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->props.codec != 0) {
|
|
|
|
|
this->props.codec = 0;
|
2021-01-25 19:57:45 +02:00
|
|
|
} else {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
this->profile = DEVICE_PROFILE_OFF;
|
2021-01-25 19:57:45 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit_remove_nodes(this);
|
|
|
|
|
emit_nodes(this);
|
|
|
|
|
|
|
|
|
|
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
2021-01-29 19:41:26 +02:00
|
|
|
if (this->prev_bt_connected_profiles != this->bt_dev->connected_profiles)
|
|
|
|
|
this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-01-25 19:57:45 +02:00
|
|
|
this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-01-25 19:57:45 +02:00
|
|
|
emit_info(this, false);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-29 19:41:26 +02:00
|
|
|
static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t prev_connected_profiles)
|
|
|
|
|
{
|
|
|
|
|
struct impl *this = userdata;
|
|
|
|
|
uint32_t connected_change;
|
|
|
|
|
bool nodes_changed = false;
|
|
|
|
|
|
|
|
|
|
connected_change = (this->bt_dev->connected_profiles ^ prev_connected_profiles);
|
|
|
|
|
|
|
|
|
|
/* Profiles changed. We have to re-emit device information. */
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_info(this->log, "profiles changed to %08x %08x (prev %08x %08x, change %08x)"
|
2021-01-29 19:41:26 +02:00
|
|
|
" switching_codec:%d",
|
|
|
|
|
this->bt_dev->profiles, this->bt_dev->connected_profiles,
|
|
|
|
|
prev_profiles, prev_connected_profiles, connected_change,
|
|
|
|
|
this->switching_codec);
|
|
|
|
|
|
|
|
|
|
if (this->switching_codec)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-09-02 15:06:27 +03:00
|
|
|
free(this->supported_codecs);
|
|
|
|
|
this->supported_codecs = spa_bt_device_get_supported_media_codecs(
|
|
|
|
|
this->bt_dev, &this->supported_codec_count);
|
2021-03-26 18:06:27 +02:00
|
|
|
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
switch (this->profile) {
|
|
|
|
|
case DEVICE_PROFILE_OFF:
|
2021-01-29 19:41:26 +02:00
|
|
|
/* Noop */
|
|
|
|
|
nodes_changed = false;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
break;
|
|
|
|
|
case DEVICE_PROFILE_AG:
|
|
|
|
|
nodes_changed = (connected_change & (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY |
|
2022-06-17 15:12:24 +02:00
|
|
|
SPA_BT_PROFILE_MEDIA_SOURCE));
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_debug(this->log, "profiles changed: AG nodes changed: %d",
|
2021-01-29 19:41:26 +02:00
|
|
|
nodes_changed);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
break;
|
|
|
|
|
case DEVICE_PROFILE_A2DP:
|
2022-06-17 15:12:24 +02:00
|
|
|
case DEVICE_PROFILE_BAP:
|
|
|
|
|
nodes_changed = (connected_change & (SPA_BT_PROFILE_MEDIA_SINK |
|
|
|
|
|
SPA_BT_PROFILE_MEDIA_SOURCE));
|
|
|
|
|
spa_log_debug(this->log, "profiles changed: media nodes changed: %d",
|
2021-01-29 19:41:26 +02:00
|
|
|
nodes_changed);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
break;
|
|
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
2021-04-21 11:08:45 +08:00
|
|
|
if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1)
|
2021-03-26 18:06:27 +02:00
|
|
|
this->props.codec = 0;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
nodes_changed = (connected_change & SPA_BT_PROFILE_HEADSET_HEAD_UNIT);
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_debug(this->log, "profiles changed: HSP/HFP nodes changed: %d",
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
nodes_changed);
|
|
|
|
|
break;
|
2021-01-29 19:41:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nodes_changed) {
|
|
|
|
|
emit_remove_nodes(this);
|
|
|
|
|
emit_nodes(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-03-20 14:20:46 +02:00
|
|
|
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; /* Profile changes may affect routes */
|
|
|
|
|
this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
|
2021-01-29 19:41:26 +02:00
|
|
|
emit_info(this, false);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
static void device_set_changed(void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct impl *this = userdata;
|
|
|
|
|
|
|
|
|
|
if (this->profile != DEVICE_PROFILE_BAP)
|
|
|
|
|
return;
|
|
|
|
|
if (!is_bap_client(this))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
spa_log_debug(this->log, "%p: device set changed", this);
|
|
|
|
|
|
|
|
|
|
emit_remove_nodes(this);
|
|
|
|
|
emit_nodes(this);
|
|
|
|
|
|
|
|
|
|
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
emit_info(this, false);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-14 14:06:50 +08:00
|
|
|
static void set_initial_profile(struct impl *this);
|
|
|
|
|
|
2023-03-16 19:02:09 +02:00
|
|
|
static void device_connected(void *userdata, bool connected)
|
|
|
|
|
{
|
2021-03-14 14:06:50 +08:00
|
|
|
struct impl *this = userdata;
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_debug(this->log, "%p: connected: %d", this, connected);
|
2021-03-14 14:06:50 +08:00
|
|
|
|
2023-03-16 19:02:09 +02:00
|
|
|
if (connected ^ (this->profile != DEVICE_PROFILE_OFF)) {
|
|
|
|
|
emit_remove_nodes(this);
|
2021-03-14 14:06:50 +08:00
|
|
|
set_initial_profile(this);
|
2023-03-16 19:02:09 +02:00
|
|
|
}
|
2021-03-14 14:06:50 +08:00
|
|
|
}
|
|
|
|
|
|
2023-11-27 13:07:31 +01:00
|
|
|
static void device_switch_profile(void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct impl *this = userdata;
|
|
|
|
|
uint32_t profile;
|
|
|
|
|
|
|
|
|
|
switch(this->profile) {
|
|
|
|
|
case DEVICE_PROFILE_OFF:
|
|
|
|
|
profile = DEVICE_PROFILE_HSP_HFP;
|
|
|
|
|
break;
|
|
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
|
|
|
|
profile = DEVICE_PROFILE_OFF;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_log_debug(this->log, "%p: device switch profile %d -> %d", this, this->profile, profile);
|
|
|
|
|
|
|
|
|
|
set_profile(this, profile, 0, false);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
static const struct spa_bt_device_events bt_dev_events = {
|
|
|
|
|
SPA_VERSION_BT_DEVICE_EVENTS,
|
2021-03-14 14:06:50 +08:00
|
|
|
.connected = device_connected,
|
2021-01-25 19:57:45 +02:00
|
|
|
.codec_switched = codec_switched,
|
2021-01-29 19:41:26 +02:00
|
|
|
.profiles_changed = profiles_changed,
|
2023-03-13 19:17:34 +02:00
|
|
|
.device_set_changed = device_set_changed,
|
2023-11-27 13:07:31 +01:00
|
|
|
.switch_profile = device_switch_profile,
|
2021-01-25 19:57:45 +02:00
|
|
|
};
|
|
|
|
|
|
2019-05-20 16:11:23 +02:00
|
|
|
static int impl_add_listener(void *object,
|
2019-03-01 12:00:42 +01:00
|
|
|
struct spa_hook *listener,
|
|
|
|
|
const struct spa_device_events *events,
|
|
|
|
|
void *data)
|
2018-11-26 12:18:53 +01:00
|
|
|
{
|
2019-05-20 16:11:23 +02:00
|
|
|
struct impl *this = object;
|
2019-03-01 12:00:42 +01:00
|
|
|
struct spa_hook_list save;
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2019-05-20 16:11:23 +02:00
|
|
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
2019-03-01 12:00:42 +01:00
|
|
|
spa_return_val_if_fail(events != NULL, -EINVAL);
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2019-03-01 12:00:42 +01:00
|
|
|
spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
if (events->info)
|
|
|
|
|
emit_info(this, true);
|
2019-02-13 11:13:46 +01:00
|
|
|
|
2019-03-01 12:00:42 +01:00
|
|
|
if (events->object_info)
|
|
|
|
|
emit_nodes(this);
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2019-03-01 12:00:42 +01:00
|
|
|
spa_hook_list_join(&this->hooks, &save);
|
2018-11-26 12:18:53 +01:00
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 13:01:54 +01:00
|
|
|
static int impl_sync(void *object, int seq)
|
|
|
|
|
{
|
|
|
|
|
struct impl *this = object;
|
|
|
|
|
|
|
|
|
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
|
|
|
|
|
|
|
|
|
spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2021-08-15 19:11:12 +03:00
|
|
|
static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum spa_bluetooth_audio_codec codec)
|
2021-01-07 18:10:22 +01:00
|
|
|
{
|
|
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
uint32_t mask;
|
2021-01-07 18:10:22 +01:00
|
|
|
bool have_output = false, have_input = false;
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec *media_codec;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
switch (index) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_A2DP:
|
2023-03-13 19:17:34 +02:00
|
|
|
if (device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK)
|
2021-01-07 18:10:22 +01:00
|
|
|
have_output = true;
|
2021-08-15 19:11:12 +03:00
|
|
|
|
2023-09-02 15:06:27 +03:00
|
|
|
media_codec = get_supported_media_codec(this, codec, NULL, device->connected_profiles);
|
2022-06-15 17:24:41 +02:00
|
|
|
if (media_codec && media_codec->duplex_codec)
|
2021-08-15 19:11:12 +03:00
|
|
|
have_input = true;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
break;
|
2023-03-13 19:17:34 +02:00
|
|
|
case DEVICE_PROFILE_BAP:
|
|
|
|
|
if (device->connected_profiles & SPA_BT_PROFILE_BAP_SINK)
|
|
|
|
|
have_output = true;
|
|
|
|
|
if (device->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE)
|
|
|
|
|
have_input = true;
|
|
|
|
|
break;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
|
|
|
|
if (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
|
2021-01-07 18:10:22 +01:00
|
|
|
have_output = have_input = true;
|
|
|
|
|
break;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
default:
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mask = 0;
|
|
|
|
|
if (have_output)
|
|
|
|
|
mask |= 1 << SPA_DIRECTION_OUTPUT;
|
|
|
|
|
if (have_input)
|
|
|
|
|
mask |= 1 << SPA_DIRECTION_INPUT;
|
|
|
|
|
return mask;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, enum spa_bluetooth_audio_codec *codec)
|
2021-01-25 23:55:09 +02:00
|
|
|
{
|
|
|
|
|
/*
|
2021-03-18 23:15:03 +02:00
|
|
|
* XXX: The codecs should probably become a separate param, and not have
|
2021-01-25 23:55:09 +02:00
|
|
|
* XXX: separate profiles for each one.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
*codec = 0;
|
2021-03-18 23:15:03 +02:00
|
|
|
*next = index + 1;
|
|
|
|
|
|
2022-10-09 20:54:06 +03:00
|
|
|
if (index <= DEVICE_PROFILE_LAST) {
|
2021-03-21 02:38:26 +02:00
|
|
|
return index;
|
|
|
|
|
} else if (index != SPA_ID_INVALID) {
|
|
|
|
|
const struct spa_type_info *info;
|
2022-06-17 15:12:24 +02:00
|
|
|
uint32_t profile;
|
2021-03-21 02:38:26 +02:00
|
|
|
|
2022-10-09 20:54:06 +03:00
|
|
|
*codec = index - DEVICE_PROFILE_LAST;
|
2021-03-21 02:38:26 +02:00
|
|
|
*next = SPA_ID_INVALID;
|
|
|
|
|
|
|
|
|
|
for (info = spa_type_bluetooth_audio_codec; info->type; ++info)
|
|
|
|
|
if (info->type > *codec)
|
2022-10-09 20:54:06 +03:00
|
|
|
*next = SPA_MIN(info->type + DEVICE_PROFILE_LAST, *next);
|
2021-03-21 02:38:26 +02:00
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
if (get_hfp_codec(*codec))
|
|
|
|
|
profile = DEVICE_PROFILE_HSP_HFP;
|
|
|
|
|
else if (*codec == SPA_BLUETOOTH_AUDIO_CODEC_LC3)
|
|
|
|
|
profile = DEVICE_PROFILE_BAP;
|
|
|
|
|
else
|
|
|
|
|
profile = DEVICE_PROFILE_A2DP;
|
|
|
|
|
|
|
|
|
|
return profile;
|
2021-01-25 23:55:09 +02:00
|
|
|
}
|
2021-03-18 23:15:03 +02:00
|
|
|
|
|
|
|
|
*next = SPA_ID_INVALID;
|
|
|
|
|
return SPA_ID_INVALID;
|
2021-01-25 23:55:09 +02:00
|
|
|
}
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec)
|
2021-01-25 23:55:09 +02:00
|
|
|
{
|
2021-03-18 23:15:03 +02:00
|
|
|
if (profile == DEVICE_PROFILE_OFF || profile == DEVICE_PROFILE_AG)
|
2021-01-25 23:55:09 +02:00
|
|
|
return profile;
|
|
|
|
|
|
2023-08-15 07:13:20 +05:30
|
|
|
if ((profile == DEVICE_PROFILE_A2DP) || (profile == DEVICE_PROFILE_BAP))
|
2023-01-17 20:14:10 +02:00
|
|
|
return codec + DEVICE_PROFILE_LAST;
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
if (profile == DEVICE_PROFILE_HSP_HFP) {
|
2021-03-21 02:38:26 +02:00
|
|
|
if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG))
|
2021-03-18 23:15:03 +02:00
|
|
|
return profile;
|
2021-02-06 18:47:07 +02:00
|
|
|
|
2022-10-09 20:54:06 +03:00
|
|
|
return codec + DEVICE_PROFILE_LAST;
|
2021-01-25 23:55:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return SPA_ID_INVALID;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-30 22:16:46 +03:00
|
|
|
static bool set_initial_hsp_hfp_profile(struct impl *this)
|
|
|
|
|
{
|
2021-04-04 08:43:09 +00:00
|
|
|
struct spa_bt_transport *t;
|
|
|
|
|
int i;
|
|
|
|
|
|
|
|
|
|
for (i = SPA_BT_PROFILE_HSP_HS; i <= SPA_BT_PROFILE_HFP_AG; i <<= 1) {
|
|
|
|
|
if (!(this->bt_dev->connected_profiles & i))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
t = find_transport(this, i, 0);
|
|
|
|
|
if (t) {
|
|
|
|
|
this->profile = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ?
|
|
|
|
|
DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP;
|
|
|
|
|
this->props.codec = get_hfp_codec_id(t->codec);
|
2021-09-30 22:16:46 +03:00
|
|
|
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_debug(this->log, "initial profile HSP/HFP profile:%d codec:%d",
|
2021-09-30 22:16:46 +03:00
|
|
|
this->profile, this->props.codec);
|
2021-04-04 08:43:09 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 21:30:15 +02:00
|
|
|
static void set_initial_profile(struct impl *this)
|
|
|
|
|
{
|
|
|
|
|
struct spa_bt_transport *t;
|
|
|
|
|
int i;
|
|
|
|
|
|
2022-04-25 21:20:10 +03:00
|
|
|
this->switching_codec = false;
|
|
|
|
|
|
2021-03-14 14:06:50 +08:00
|
|
|
if (this->supported_codecs)
|
|
|
|
|
free(this->supported_codecs);
|
2022-06-15 17:24:41 +02:00
|
|
|
this->supported_codecs = spa_bt_device_get_supported_media_codecs(
|
2023-09-02 15:06:27 +03:00
|
|
|
this->bt_dev, &this->supported_codec_count);
|
2021-03-14 14:06:50 +08:00
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
/* Prefer BAP, then A2DP, then HFP, then null, but select AG if the device
|
|
|
|
|
appears not to have BAP_SINK, A2DP_SINK or any HEAD_UNIT profile */
|
2021-02-10 21:30:15 +02:00
|
|
|
|
2021-09-30 22:16:46 +03:00
|
|
|
/* If default profile is set to HSP/HFP, first try those and exit if found. */
|
2021-04-04 08:43:09 +00:00
|
|
|
if (this->bt_dev->settings != NULL) {
|
2021-09-30 22:16:46 +03:00
|
|
|
const char *str = spa_dict_lookup(this->bt_dev->settings, "bluez5.profile");
|
2022-01-22 18:18:08 +02:00
|
|
|
if (spa_streq(str, "off"))
|
|
|
|
|
goto off;
|
2021-09-30 22:16:46 +03:00
|
|
|
if (spa_streq(str, "headset-head-unit") && set_initial_hsp_hfp_profile(this))
|
2021-04-04 08:43:09 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) {
|
2021-02-10 21:30:15 +02:00
|
|
|
if (!(this->bt_dev->connected_profiles & i))
|
|
|
|
|
continue;
|
|
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
t = find_transport(this, i, 0);
|
2021-02-10 21:30:15 +02:00
|
|
|
if (t) {
|
2022-06-17 15:12:24 +02:00
|
|
|
if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE)
|
|
|
|
|
this->profile = DEVICE_PROFILE_AG;
|
|
|
|
|
else if (i == SPA_BT_PROFILE_BAP_SINK)
|
|
|
|
|
this->profile = DEVICE_PROFILE_BAP;
|
|
|
|
|
else
|
|
|
|
|
this->profile = DEVICE_PROFILE_A2DP;
|
2022-06-15 17:24:41 +02:00
|
|
|
this->props.codec = t->media_codec->id;
|
2022-06-17 15:12:24 +02:00
|
|
|
spa_log_debug(this->log, "initial profile media profile:%d codec:%d",
|
2021-09-30 22:16:46 +03:00
|
|
|
this->profile, this->props.codec);
|
2021-02-10 21:30:15 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-30 22:16:46 +03:00
|
|
|
if (set_initial_hsp_hfp_profile(this))
|
|
|
|
|
return;
|
|
|
|
|
|
2022-01-22 18:18:08 +02:00
|
|
|
off:
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_debug(this->log, "initial profile off");
|
2021-02-10 21:30:15 +02:00
|
|
|
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
this->profile = DEVICE_PROFILE_OFF;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->props.codec = 0;
|
2021-02-10 21:30:15 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-17 11:08:26 +02:00
|
|
|
static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
|
2022-01-06 16:26:11 +02:00
|
|
|
uint32_t id, uint32_t index, uint32_t profile_index, enum spa_bluetooth_audio_codec codec,
|
|
|
|
|
bool current)
|
2020-08-17 11:08:26 +02:00
|
|
|
{
|
|
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
|
|
|
|
struct spa_pod_frame f[2];
|
|
|
|
|
const char *name, *desc;
|
2021-01-25 23:55:09 +02:00
|
|
|
char *name_and_codec = NULL;
|
|
|
|
|
char *desc_and_codec = NULL;
|
2020-08-17 11:08:26 +02:00
|
|
|
uint32_t n_source = 0, n_sink = 0;
|
2021-02-02 23:12:35 +02:00
|
|
|
uint32_t capture[1] = { DEVICE_ID_SOURCE }, playback[1] = { DEVICE_ID_SINK };
|
2022-01-03 16:15:52 +02:00
|
|
|
int priority;
|
2020-08-17 11:08:26 +02:00
|
|
|
|
2021-01-25 23:55:09 +02:00
|
|
|
switch (profile_index) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_OFF:
|
2020-08-17 11:08:26 +02:00
|
|
|
name = "off";
|
2021-04-15 17:42:02 +02:00
|
|
|
desc = _("Off");
|
2022-01-03 16:15:52 +02:00
|
|
|
priority = 0;
|
2020-08-17 11:08:26 +02:00
|
|
|
break;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_AG:
|
2020-08-17 11:08:26 +02:00
|
|
|
{
|
|
|
|
|
uint32_t profile = device->connected_profiles &
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
(SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
|
2021-01-10 20:53:59 +01:00
|
|
|
if (profile == 0) {
|
2020-08-17 11:08:26 +02:00
|
|
|
return NULL;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
} else {
|
|
|
|
|
name = "audio-gateway";
|
2021-04-15 17:42:02 +02:00
|
|
|
desc = _("Audio Gateway (A2DP Source & HSP/HFP AG)");
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
}
|
2023-09-24 15:21:45 +03:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* If the remote is A2DP sink and HF, we likely should prioritize being
|
|
|
|
|
* A2DP sender, not gateway. This can occur in PW<->PW if RFCOMM gets
|
|
|
|
|
* connected both as AG and HF.
|
|
|
|
|
*/
|
|
|
|
|
if ((device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) &&
|
|
|
|
|
(device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT))
|
|
|
|
|
priority = 15;
|
|
|
|
|
else
|
|
|
|
|
priority = 256;
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case DEVICE_PROFILE_A2DP:
|
|
|
|
|
{
|
|
|
|
|
/* make this device profile visible only if there is an A2DP sink */
|
|
|
|
|
uint32_t profile = device->connected_profiles &
|
|
|
|
|
(SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE);
|
|
|
|
|
if (!(profile & SPA_BT_PROFILE_A2DP_SINK)) {
|
|
|
|
|
return NULL;
|
2021-01-10 20:53:59 +01:00
|
|
|
}
|
2023-08-15 07:13:20 +05:30
|
|
|
|
|
|
|
|
/* A2DP will only enlist codec profiles */
|
|
|
|
|
if (!codec)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
2021-01-27 20:39:02 +01:00
|
|
|
name = spa_bt_profile_name(profile);
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
n_sink++;
|
2021-03-21 02:38:26 +02:00
|
|
|
if (codec) {
|
2022-01-03 16:15:52 +02:00
|
|
|
size_t idx;
|
2023-09-02 15:06:27 +03:00
|
|
|
const struct media_codec *media_codec = get_supported_media_codec(this, codec, &idx, profile);
|
2022-06-15 17:24:41 +02:00
|
|
|
if (media_codec == NULL) {
|
2021-04-21 11:08:45 +08:00
|
|
|
errno = EINVAL;
|
2021-03-21 02:38:26 +02:00
|
|
|
return NULL;
|
|
|
|
|
}
|
2022-06-15 17:24:41 +02:00
|
|
|
name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
|
2023-08-15 07:13:20 +05:30
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Give base name to highest priority profile, so that best codec can be
|
|
|
|
|
* selected at command line with out knowing which codecs are actually
|
|
|
|
|
* supported
|
|
|
|
|
*/
|
|
|
|
|
if (idx != 0)
|
|
|
|
|
name = name_and_codec;
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
if (profile == SPA_BT_PROFILE_A2DP_SINK && !media_codec->duplex_codec) {
|
2021-09-06 15:13:05 +10:00
|
|
|
desc_and_codec = spa_aprintf(_("High Fidelity Playback (A2DP Sink, codec %s)"),
|
2022-06-15 17:24:41 +02:00
|
|
|
media_codec->description);
|
2021-04-18 10:47:26 +08:00
|
|
|
} else {
|
2021-09-06 15:13:05 +10:00
|
|
|
desc_and_codec = spa_aprintf(_("High Fidelity Duplex (A2DP Source/Sink, codec %s)"),
|
2022-06-15 17:24:41 +02:00
|
|
|
media_codec->description);
|
2021-09-06 15:13:05 +10:00
|
|
|
|
2021-04-18 10:47:26 +08:00
|
|
|
}
|
2021-01-25 23:55:09 +02:00
|
|
|
desc = desc_and_codec;
|
2022-01-03 16:15:52 +02:00
|
|
|
priority = 16 + this->supported_codec_count - idx; /* order as in codec list */
|
2021-01-25 23:55:09 +02:00
|
|
|
} else {
|
2021-04-18 10:47:26 +08:00
|
|
|
if (profile == SPA_BT_PROFILE_A2DP_SINK) {
|
|
|
|
|
desc = _("High Fidelity Playback (A2DP Sink)");
|
|
|
|
|
} else {
|
|
|
|
|
desc = _("High Fidelity Duplex (A2DP Source/Sink)");
|
|
|
|
|
}
|
2022-01-03 16:15:52 +02:00
|
|
|
priority = 16;
|
2021-01-25 23:55:09 +02:00
|
|
|
}
|
2020-08-17 11:08:26 +02:00
|
|
|
break;
|
|
|
|
|
}
|
2022-06-17 15:12:24 +02:00
|
|
|
case DEVICE_PROFILE_BAP:
|
|
|
|
|
{
|
|
|
|
|
uint32_t profile = device->connected_profiles &
|
2023-07-23 22:16:17 +03:00
|
|
|
(SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE
|
|
|
|
|
| SPA_BT_PROFILE_BAP_BROADCAST_SOURCE
|
|
|
|
|
| SPA_BT_PROFILE_BAP_BROADCAST_SINK);
|
2022-06-17 15:12:24 +02:00
|
|
|
size_t idx;
|
|
|
|
|
const struct media_codec *media_codec;
|
|
|
|
|
|
2023-08-15 07:13:20 +05:30
|
|
|
/* BAP will only enlist codec profiles */
|
|
|
|
|
if (codec == 0)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
if (profile == 0)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
2023-07-23 22:16:17 +03:00
|
|
|
if ((profile & (SPA_BT_PROFILE_BAP_SINK)) ||
|
|
|
|
|
(profile & (SPA_BT_PROFILE_BAP_BROADCAST_SINK)))
|
2022-06-17 15:12:24 +02:00
|
|
|
n_sink++;
|
2023-07-23 22:16:17 +03:00
|
|
|
if ((profile & (SPA_BT_PROFILE_BAP_SOURCE)) ||
|
|
|
|
|
(profile & (SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)))
|
2022-06-17 15:12:24 +02:00
|
|
|
n_source++;
|
|
|
|
|
|
|
|
|
|
name = spa_bt_profile_name(profile);
|
|
|
|
|
|
2023-02-05 16:08:46 +02:00
|
|
|
if (codec) {
|
2023-09-02 15:06:27 +03:00
|
|
|
media_codec = get_supported_media_codec(this, codec, &idx, profile);
|
2023-02-05 16:08:46 +02:00
|
|
|
if (media_codec == NULL) {
|
|
|
|
|
errno = EINVAL;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
|
2023-08-15 07:13:20 +05:30
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Give base name to highest priority profile, so that best codec can be
|
|
|
|
|
* selected at command line with out knowing which codecs are actually
|
|
|
|
|
* supported
|
|
|
|
|
*/
|
|
|
|
|
if (idx != 0)
|
|
|
|
|
name = name_and_codec;
|
|
|
|
|
|
2023-02-05 16:08:46 +02:00
|
|
|
switch (profile) {
|
|
|
|
|
case SPA_BT_PROFILE_BAP_SINK:
|
2023-08-09 13:10:36 +03:00
|
|
|
case SPA_BT_PROFILE_BAP_BROADCAST_SINK:
|
2023-02-05 16:08:46 +02:00
|
|
|
desc_and_codec = spa_aprintf(_("High Fidelity Playback (BAP Sink, codec %s)"),
|
|
|
|
|
media_codec->description);
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_PROFILE_BAP_SOURCE:
|
2023-08-09 13:10:36 +03:00
|
|
|
case SPA_BT_PROFILE_BAP_BROADCAST_SOURCE:
|
2023-02-05 16:08:46 +02:00
|
|
|
desc_and_codec = spa_aprintf(_("High Fidelity Input (BAP Source, codec %s)"),
|
|
|
|
|
media_codec->description);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
desc_and_codec = spa_aprintf(_("High Fidelity Duplex (BAP Source/Sink, codec %s)"),
|
|
|
|
|
media_codec->description);
|
|
|
|
|
}
|
|
|
|
|
desc = desc_and_codec;
|
|
|
|
|
priority = 128 + this->supported_codec_count - idx; /* order as in codec list */
|
|
|
|
|
} else {
|
|
|
|
|
switch (profile) {
|
|
|
|
|
case SPA_BT_PROFILE_BAP_SINK:
|
2023-08-09 13:10:36 +03:00
|
|
|
case SPA_BT_PROFILE_BAP_BROADCAST_SINK:
|
2023-02-05 16:08:46 +02:00
|
|
|
desc = _("High Fidelity Playback (BAP Sink)");
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_PROFILE_BAP_SOURCE:
|
2023-08-09 13:10:36 +03:00
|
|
|
case SPA_BT_PROFILE_BAP_BROADCAST_SOURCE:
|
2023-02-05 16:08:46 +02:00
|
|
|
desc = _("High Fidelity Input (BAP Source)");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
desc = _("High Fidelity Duplex (BAP Source/Sink)");
|
|
|
|
|
}
|
|
|
|
|
priority = 128;
|
2022-06-17 15:12:24 +02:00
|
|
|
}
|
2023-03-13 19:17:34 +02:00
|
|
|
|
|
|
|
|
if (this->device_set.leader) {
|
|
|
|
|
n_sink = this->device_set.sinks ? 1 : 0;
|
|
|
|
|
n_source = this->device_set.sinks ? 1 : 0;
|
|
|
|
|
} else if (this->device_set.path) {
|
|
|
|
|
n_sink = 0;
|
|
|
|
|
n_source = 0;
|
|
|
|
|
}
|
2022-06-17 15:12:24 +02:00
|
|
|
break;
|
|
|
|
|
}
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
2020-08-17 11:08:26 +02:00
|
|
|
{
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
/* make this device profile visible only if there is a head unit */
|
2020-08-17 11:08:26 +02:00
|
|
|
uint32_t profile = device->connected_profiles &
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
SPA_BT_PROFILE_HEADSET_HEAD_UNIT;
|
2021-01-10 20:53:59 +01:00
|
|
|
if (profile == 0) {
|
2020-08-17 11:08:26 +02:00
|
|
|
return NULL;
|
2021-01-10 20:53:59 +01:00
|
|
|
}
|
2021-01-27 20:39:02 +01:00
|
|
|
name = spa_bt_profile_name(profile);
|
2020-08-17 11:08:26 +02:00
|
|
|
n_source++;
|
|
|
|
|
n_sink++;
|
2021-03-21 02:38:26 +02:00
|
|
|
if (codec) {
|
2021-03-18 23:15:03 +02:00
|
|
|
bool codec_ok = !(profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
|
2021-03-21 02:38:26 +02:00
|
|
|
unsigned int hfp_codec = get_hfp_codec(codec);
|
2021-04-21 11:08:45 +08:00
|
|
|
if (spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1)
|
2021-03-18 23:15:03 +02:00
|
|
|
codec_ok = false;
|
|
|
|
|
if (!codec_ok) {
|
2021-04-21 11:08:45 +08:00
|
|
|
errno = EINVAL;
|
2021-03-18 23:15:03 +02:00
|
|
|
return NULL;
|
|
|
|
|
}
|
2021-03-20 00:11:12 +02:00
|
|
|
name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec));
|
2021-03-18 23:15:03 +02:00
|
|
|
name = name_and_codec;
|
2021-04-18 10:47:26 +08:00
|
|
|
desc_and_codec = spa_aprintf(_("Headset Head Unit (HSP/HFP, codec %s)"),
|
|
|
|
|
get_hfp_codec_description(hfp_codec));
|
2021-03-18 23:15:03 +02:00
|
|
|
desc = desc_and_codec;
|
2022-01-03 16:15:52 +02:00
|
|
|
priority = 1 + hfp_codec; /* prefer msbc over cvsd */
|
2021-03-18 23:15:03 +02:00
|
|
|
} else {
|
2021-04-18 10:47:26 +08:00
|
|
|
desc = _("Headset Head Unit (HSP/HFP)");
|
2022-01-03 16:15:52 +02:00
|
|
|
priority = 1;
|
2021-03-18 23:15:03 +02:00
|
|
|
}
|
2020-08-17 11:08:26 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
2021-03-19 12:46:05 +01:00
|
|
|
errno = EINVAL;
|
2020-08-17 11:08:26 +02:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
|
|
|
|
|
spa_pod_builder_add(b,
|
|
|
|
|
SPA_PARAM_PROFILE_index, SPA_POD_Int(index),
|
|
|
|
|
SPA_PARAM_PROFILE_name, SPA_POD_String(name),
|
|
|
|
|
SPA_PARAM_PROFILE_description, SPA_POD_String(desc),
|
2021-01-25 15:53:36 +01:00
|
|
|
SPA_PARAM_PROFILE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes),
|
2022-01-03 16:15:52 +02:00
|
|
|
SPA_PARAM_PROFILE_priority, SPA_POD_Int(priority),
|
2020-08-17 11:08:26 +02:00
|
|
|
0);
|
|
|
|
|
if (n_source > 0 || n_sink > 0) {
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
|
|
|
|
|
spa_pod_builder_push_struct(b, &f[1]);
|
|
|
|
|
if (n_source > 0) {
|
|
|
|
|
spa_pod_builder_add_struct(b,
|
|
|
|
|
SPA_POD_String("Audio/Source"),
|
2021-02-02 23:12:35 +02:00
|
|
|
SPA_POD_Int(n_source),
|
|
|
|
|
SPA_POD_String("card.profile.devices"),
|
|
|
|
|
SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, capture));
|
2020-08-17 11:08:26 +02:00
|
|
|
}
|
|
|
|
|
if (n_sink > 0) {
|
|
|
|
|
spa_pod_builder_add_struct(b,
|
|
|
|
|
SPA_POD_String("Audio/Sink"),
|
2021-02-02 23:12:35 +02:00
|
|
|
SPA_POD_Int(n_sink),
|
|
|
|
|
SPA_POD_String("card.profile.devices"),
|
|
|
|
|
SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, playback));
|
2020-08-17 11:08:26 +02:00
|
|
|
}
|
|
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
|
|
|
}
|
2022-01-06 16:26:11 +02:00
|
|
|
if (current) {
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0);
|
|
|
|
|
spa_pod_builder_bool(b, this->save_profile);
|
|
|
|
|
}
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2021-04-18 10:47:26 +08:00
|
|
|
if (name_and_codec)
|
|
|
|
|
free(name_and_codec);
|
|
|
|
|
if (desc_and_codec)
|
|
|
|
|
free(desc_and_codec);
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2020-08-17 11:08:26 +02:00
|
|
|
return spa_pod_builder_pop(b, &f[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-11 06:40:13 +08:00
|
|
|
static bool validate_profile(struct impl *this, uint32_t profile,
|
|
|
|
|
enum spa_bluetooth_audio_codec codec)
|
|
|
|
|
{
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
|
|
|
|
uint8_t buffer[1024];
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
2022-01-06 16:26:11 +02:00
|
|
|
return (build_profile(this, &b, 0, 0, profile, codec, false) != NULL);
|
2021-05-11 06:40:13 +08:00
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
|
2022-03-05 18:31:41 +02:00
|
|
|
uint32_t id, uint32_t port, uint32_t profile)
|
2021-01-07 18:10:22 +01:00
|
|
|
{
|
2021-01-10 20:53:59 +01:00
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
2021-01-07 18:10:22 +01:00
|
|
|
struct spa_pod_frame f[2];
|
|
|
|
|
enum spa_direction direction;
|
2022-03-05 18:31:41 +02:00
|
|
|
const char *name_prefix, *description, *hfp_description, *port_type;
|
2021-01-10 20:53:59 +01:00
|
|
|
enum spa_bt_form_factor ff;
|
2021-03-21 02:38:26 +02:00
|
|
|
enum spa_bluetooth_audio_codec codec;
|
2023-03-13 19:17:34 +02:00
|
|
|
enum spa_param_availability available;
|
2021-01-10 20:53:59 +01:00
|
|
|
char name[128];
|
2021-03-18 23:15:03 +02:00
|
|
|
uint32_t i, j, mask, next;
|
2022-03-05 18:31:41 +02:00
|
|
|
uint32_t dev = SPA_ID_INVALID, enum_dev;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-01-10 20:53:59 +01:00
|
|
|
ff = spa_bt_form_factor_from_class(device->bluetooth_class);
|
|
|
|
|
|
|
|
|
|
switch (ff) {
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HEADSET:
|
|
|
|
|
name_prefix = "headset";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Headset");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "headset";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HANDSFREE:
|
|
|
|
|
name_prefix = "handsfree";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Handsfree");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree (HFP)");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "handsfree";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_MICROPHONE:
|
|
|
|
|
name_prefix = "microphone";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Microphone");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "mic";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_SPEAKER:
|
|
|
|
|
name_prefix = "speaker";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Speaker");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "speaker";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HEADPHONE:
|
|
|
|
|
name_prefix = "headphone";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Headphone");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "headphones";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_PORTABLE:
|
|
|
|
|
name_prefix = "portable";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Portable");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "portable";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_CAR:
|
|
|
|
|
name_prefix = "car";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Car");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "car";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HIFI:
|
|
|
|
|
name_prefix = "hifi";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("HiFi");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "hifi";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_PHONE:
|
|
|
|
|
name_prefix = "phone";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Phone");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Handsfree");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "phone";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_UNKNOWN:
|
|
|
|
|
default:
|
|
|
|
|
name_prefix = "bluetooth";
|
2021-04-15 17:42:02 +02:00
|
|
|
description = _("Bluetooth");
|
2022-03-05 18:31:41 +02:00
|
|
|
hfp_description = _("Bluetooth (HFP)");
|
2021-01-10 20:53:59 +01:00
|
|
|
port_type = "bluetooth";
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
switch (port) {
|
|
|
|
|
case 0:
|
|
|
|
|
direction = SPA_DIRECTION_INPUT;
|
2021-01-10 20:53:59 +01:00
|
|
|
snprintf(name, sizeof(name), "%s-input", name_prefix);
|
2022-03-05 18:31:41 +02:00
|
|
|
enum_dev = DEVICE_ID_SOURCE;
|
2023-03-13 19:17:34 +02:00
|
|
|
if (profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP)
|
2022-03-05 18:31:41 +02:00
|
|
|
dev = enum_dev;
|
|
|
|
|
else if (profile != SPA_ID_INVALID)
|
|
|
|
|
enum_dev = SPA_ID_INVALID;
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
direction = SPA_DIRECTION_OUTPUT;
|
2021-01-10 20:53:59 +01:00
|
|
|
snprintf(name, sizeof(name), "%s-output", name_prefix);
|
2022-03-05 18:31:41 +02:00
|
|
|
enum_dev = DEVICE_ID_SINK;
|
2023-03-13 19:17:34 +02:00
|
|
|
if (profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP)
|
2022-03-05 18:31:41 +02:00
|
|
|
dev = enum_dev;
|
|
|
|
|
else if (profile != SPA_ID_INVALID)
|
|
|
|
|
enum_dev = SPA_ID_INVALID;
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
direction = SPA_DIRECTION_INPUT;
|
|
|
|
|
snprintf(name, sizeof(name), "%s-hf-input", name_prefix);
|
|
|
|
|
description = hfp_description;
|
|
|
|
|
enum_dev = DEVICE_ID_SOURCE;
|
|
|
|
|
if (profile == DEVICE_PROFILE_HSP_HFP)
|
|
|
|
|
dev = enum_dev;
|
|
|
|
|
else if (profile != SPA_ID_INVALID)
|
|
|
|
|
enum_dev = SPA_ID_INVALID;
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
direction = SPA_DIRECTION_OUTPUT;
|
|
|
|
|
snprintf(name, sizeof(name), "%s-hf-output", name_prefix);
|
|
|
|
|
description = hfp_description;
|
|
|
|
|
enum_dev = DEVICE_ID_SINK;
|
|
|
|
|
if (profile == DEVICE_PROFILE_HSP_HFP)
|
|
|
|
|
dev = enum_dev;
|
|
|
|
|
else if (profile != SPA_ID_INVALID)
|
|
|
|
|
enum_dev = SPA_ID_INVALID;
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
2023-03-13 19:17:34 +02:00
|
|
|
case 4:
|
|
|
|
|
if (!this->device_set.leader) {
|
|
|
|
|
errno = EINVAL;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
direction = SPA_DIRECTION_INPUT;
|
|
|
|
|
snprintf(name, sizeof(name), "%s-set-input", name_prefix);
|
|
|
|
|
enum_dev = DEVICE_ID_SOURCE_SET;
|
|
|
|
|
if (profile == DEVICE_PROFILE_BAP)
|
|
|
|
|
dev = enum_dev;
|
|
|
|
|
else if (profile != SPA_ID_INVALID)
|
|
|
|
|
enum_dev = SPA_ID_INVALID;
|
|
|
|
|
break;
|
|
|
|
|
case 5:
|
|
|
|
|
if (!this->device_set.leader) {
|
|
|
|
|
errno = EINVAL;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
direction = SPA_DIRECTION_OUTPUT;
|
|
|
|
|
snprintf(name, sizeof(name), "%s-set-output", name_prefix);
|
|
|
|
|
enum_dev = DEVICE_ID_SINK_SET;
|
|
|
|
|
if (profile == DEVICE_PROFILE_BAP)
|
|
|
|
|
dev = enum_dev;
|
|
|
|
|
else if (profile != SPA_ID_INVALID)
|
|
|
|
|
enum_dev = SPA_ID_INVALID;
|
|
|
|
|
break;
|
2021-01-07 18:10:22 +01:00
|
|
|
default:
|
2021-03-19 12:46:05 +01:00
|
|
|
errno = EINVAL;
|
2021-01-07 18:10:22 +01:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-05 18:31:41 +02:00
|
|
|
if (enum_dev == SPA_ID_INVALID) {
|
|
|
|
|
errno = EINVAL;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
available = SPA_PARAM_AVAILABILITY_yes;
|
|
|
|
|
if (this->device_set.path && !(port == 4 || port == 5))
|
|
|
|
|
available = SPA_PARAM_AVAILABILITY_no;
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
|
|
|
|
|
spa_pod_builder_add(b,
|
|
|
|
|
SPA_PARAM_ROUTE_index, SPA_POD_Int(port),
|
|
|
|
|
SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction),
|
|
|
|
|
SPA_PARAM_ROUTE_name, SPA_POD_String(name),
|
|
|
|
|
SPA_PARAM_ROUTE_description, SPA_POD_String(description),
|
|
|
|
|
SPA_PARAM_ROUTE_priority, SPA_POD_Int(0),
|
2023-03-13 19:17:34 +02:00
|
|
|
SPA_PARAM_ROUTE_available, SPA_POD_Id(available),
|
2021-01-07 18:10:22 +01:00
|
|
|
0);
|
2021-01-10 20:53:59 +01:00
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, 0);
|
|
|
|
|
spa_pod_builder_push_struct(b, &f[1]);
|
|
|
|
|
spa_pod_builder_int(b, 1);
|
|
|
|
|
spa_pod_builder_add(b,
|
|
|
|
|
SPA_POD_String("port.type"),
|
|
|
|
|
SPA_POD_String(port_type),
|
|
|
|
|
NULL);
|
|
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0);
|
|
|
|
|
spa_pod_builder_push_array(b, &f[1]);
|
2021-08-15 19:11:12 +03:00
|
|
|
|
|
|
|
|
mask = 0;
|
2021-03-21 02:38:26 +02:00
|
|
|
for (i = 1; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) {
|
2021-08-15 19:11:12 +03:00
|
|
|
uint32_t profile_mask;
|
2021-03-20 14:20:46 +02:00
|
|
|
|
2022-03-05 18:31:41 +02:00
|
|
|
if (j == DEVICE_PROFILE_A2DP && !(port == 0 || port == 1))
|
|
|
|
|
continue;
|
2023-03-13 19:17:34 +02:00
|
|
|
if (j == DEVICE_PROFILE_BAP && !(port == 0 || port == 1 || port == 4 || port == 5))
|
|
|
|
|
continue;
|
2022-03-05 18:31:41 +02:00
|
|
|
if (j == DEVICE_PROFILE_HSP_HFP && !(port == 2 || port == 3))
|
|
|
|
|
continue;
|
|
|
|
|
|
2021-08-15 19:11:12 +03:00
|
|
|
profile_mask = profile_direction_mask(this, j, codec);
|
|
|
|
|
if (!(profile_mask & (1 << direction)))
|
2021-03-20 14:20:46 +02:00
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
/* Check the profile actually exists */
|
2021-05-11 06:40:13 +08:00
|
|
|
if (!validate_profile(this, j, codec))
|
2021-03-20 14:20:46 +02:00
|
|
|
continue;
|
|
|
|
|
|
2021-08-15 19:11:12 +03:00
|
|
|
mask |= profile_mask;
|
2021-03-20 14:20:46 +02:00
|
|
|
spa_pod_builder_int(b, i);
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
|
|
|
|
2021-08-15 19:11:12 +03:00
|
|
|
if (!(mask & (1 << direction))) {
|
|
|
|
|
/* No profile has route direction */
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
if (dev != SPA_ID_INVALID) {
|
|
|
|
|
struct node *node = &this->nodes[dev];
|
2021-04-15 11:39:49 +08:00
|
|
|
struct spa_bt_transport_volume *t_volume;
|
|
|
|
|
|
2021-08-15 19:11:12 +03:00
|
|
|
mask = profile_direction_mask(this, this->profile, this->props.codec);
|
|
|
|
|
if (!(mask & (1 << direction)))
|
|
|
|
|
return NULL;
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
t_volume = node->transport
|
|
|
|
|
? &node->transport->volumes[node->id]
|
|
|
|
|
: NULL;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0);
|
|
|
|
|
spa_pod_builder_int(b, dev);
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0);
|
|
|
|
|
spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id);
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PROP_mute, 0);
|
|
|
|
|
spa_pod_builder_bool(b, node->mute);
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
spa_pod_builder_prop(b, SPA_PROP_channelVolumes,
|
|
|
|
|
(t_volume && t_volume->active) ? SPA_POD_PROP_FLAG_HARDWARE : 0);
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
|
|
|
|
|
node->n_channels, node->volumes);
|
|
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
if (t_volume && t_volume->active) {
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY);
|
|
|
|
|
spa_pod_builder_float(b, 1.0f / (t_volume->hw_volume_max + 1));
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_prop(b, SPA_PROP_channelMap, 0);
|
|
|
|
|
spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
|
|
|
|
|
node->n_channels, node->channels);
|
|
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) &&
|
|
|
|
|
dev == DEVICE_ID_SINK) {
|
2021-02-13 22:59:04 +02:00
|
|
|
spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
|
|
|
|
|
spa_pod_builder_long(b, node->latency_offset);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
2021-03-30 16:31:17 +02:00
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0);
|
|
|
|
|
spa_pod_builder_bool(b, node->save);
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
|
|
|
|
|
spa_pod_builder_push_array(b, &f[1]);
|
2022-03-05 18:31:41 +02:00
|
|
|
spa_pod_builder_int(b, enum_dev);
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
|
|
|
|
|
|
|
|
if (profile != SPA_ID_INVALID) {
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0);
|
|
|
|
|
spa_pod_builder_int(b, profile);
|
|
|
|
|
}
|
|
|
|
|
return spa_pod_builder_pop(b, &f[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-15 17:24:41 +02:00
|
|
|
static bool iterate_supported_media_codecs(struct impl *this, int *j, const struct media_codec **codec)
|
2021-08-15 20:12:51 +03:00
|
|
|
{
|
|
|
|
|
int i;
|
|
|
|
|
|
|
|
|
|
next:
|
|
|
|
|
*j = *j + 1;
|
|
|
|
|
spa_assert(*j >= 0);
|
|
|
|
|
if ((size_t)*j >= this->supported_codec_count)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < *j; ++i)
|
|
|
|
|
if (this->supported_codecs[i]->id == this->supported_codecs[*j]->id)
|
|
|
|
|
goto next;
|
|
|
|
|
|
|
|
|
|
*codec = this->supported_codecs[*j];
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-01 15:46:27 +03:00
|
|
|
static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_builder *b, uint32_t id)
|
2021-03-21 02:38:26 +02:00
|
|
|
{
|
|
|
|
|
struct spa_pod_frame f[2];
|
|
|
|
|
struct spa_pod_choice *choice;
|
2022-06-15 17:24:41 +02:00
|
|
|
const struct media_codec *codec;
|
2021-08-15 20:12:51 +03:00
|
|
|
size_t n;
|
|
|
|
|
int j;
|
2021-03-21 02:38:26 +02:00
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
#define FOR_EACH_MEDIA_CODEC(j, codec) \
|
2022-06-15 17:24:41 +02:00
|
|
|
for (j = -1; iterate_supported_media_codecs(this, &j, &codec);)
|
2021-03-21 02:38:26 +02:00
|
|
|
#define FOR_EACH_HFP_CODEC(j) \
|
|
|
|
|
for (j = HFP_AUDIO_CODEC_MSBC; j >= HFP_AUDIO_CODEC_CVSD; --j) \
|
|
|
|
|
if (spa_bt_device_supports_hfp_codec(this->bt_dev, j) == 1)
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* XXX: the ids in principle should use builder_id, not builder_int,
|
|
|
|
|
* XXX: but the type info for _type and _labels doesn't work quite right now.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/* Transport codec */
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0);
|
|
|
|
|
spa_pod_builder_id(b, SPA_PROP_bluetoothAudioCodec);
|
2022-03-30 17:09:08 +02:00
|
|
|
spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0);
|
2021-03-21 02:38:26 +02:00
|
|
|
spa_pod_builder_string(b, "Air codec");
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
|
|
|
|
|
spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
|
|
|
|
|
choice = (struct spa_pod_choice *)spa_pod_builder_frame(b, &f[1]);
|
|
|
|
|
n = 0;
|
2022-06-17 15:12:24 +02:00
|
|
|
if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
|
|
|
|
|
FOR_EACH_MEDIA_CODEC(j, codec) {
|
2021-03-21 02:38:26 +02:00
|
|
|
if (n == 0)
|
|
|
|
|
spa_pod_builder_int(b, codec->id);
|
|
|
|
|
spa_pod_builder_int(b, codec->id);
|
|
|
|
|
++n;
|
|
|
|
|
}
|
|
|
|
|
} else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
|
|
|
|
|
FOR_EACH_HFP_CODEC(j) {
|
|
|
|
|
if (n == 0)
|
|
|
|
|
spa_pod_builder_int(b, get_hfp_codec_id(j));
|
|
|
|
|
spa_pod_builder_int(b, get_hfp_codec_id(j));
|
|
|
|
|
++n;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (n == 0)
|
|
|
|
|
choice->body.type = SPA_CHOICE_None;
|
|
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0);
|
|
|
|
|
spa_pod_builder_push_struct(b, &f[1]);
|
2022-06-17 15:12:24 +02:00
|
|
|
if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
|
|
|
|
|
FOR_EACH_MEDIA_CODEC(j, codec) {
|
2021-03-21 02:38:26 +02:00
|
|
|
spa_pod_builder_int(b, codec->id);
|
|
|
|
|
spa_pod_builder_string(b, codec->description);
|
|
|
|
|
}
|
|
|
|
|
} else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
|
|
|
|
|
FOR_EACH_HFP_CODEC(j) {
|
|
|
|
|
spa_pod_builder_int(b, get_hfp_codec_id(j));
|
|
|
|
|
spa_pod_builder_string(b, get_hfp_codec_description(j));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
|
|
|
return spa_pod_builder_pop(b, &f[0]);
|
|
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
#undef FOR_EACH_MEDIA_CODEC
|
2021-03-21 02:38:26 +02:00
|
|
|
#undef FOR_EACH_HFP_CODEC
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct spa_pod *build_props(struct impl *this, struct spa_pod_builder *b, uint32_t id)
|
|
|
|
|
{
|
|
|
|
|
struct props *p = &this->props;
|
|
|
|
|
|
|
|
|
|
return spa_pod_builder_add_object(b,
|
|
|
|
|
SPA_TYPE_OBJECT_Props, id,
|
2022-10-01 15:46:27 +03:00
|
|
|
SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(p->codec),
|
|
|
|
|
SPA_PROP_bluetoothOffloadActive, SPA_POD_Bool(p->offload_active));
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
|
|
|
|
|
2019-05-20 16:11:23 +02:00
|
|
|
static int impl_enum_params(void *object, int seq,
|
2019-02-25 12:29:57 +01:00
|
|
|
uint32_t id, uint32_t start, uint32_t num,
|
|
|
|
|
const struct spa_pod *filter)
|
2018-11-26 12:18:53 +01:00
|
|
|
{
|
2020-01-03 13:01:54 +01:00
|
|
|
struct impl *this = object;
|
|
|
|
|
struct spa_pod *param;
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
2021-03-21 02:38:26 +02:00
|
|
|
uint8_t buffer[2048];
|
2020-01-03 13:01:54 +01:00
|
|
|
struct spa_result_device_params result;
|
|
|
|
|
uint32_t count = 0;
|
|
|
|
|
|
|
|
|
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
|
|
|
|
spa_return_val_if_fail(num != 0, -EINVAL);
|
|
|
|
|
|
|
|
|
|
result.id = id;
|
|
|
|
|
result.next = start;
|
|
|
|
|
next:
|
|
|
|
|
result.index = result.next++;
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
|
|
|
|
|
switch (id) {
|
|
|
|
|
case SPA_PARAM_EnumProfile:
|
|
|
|
|
{
|
2021-01-25 23:55:09 +02:00
|
|
|
uint32_t profile;
|
2021-03-21 02:38:26 +02:00
|
|
|
enum spa_bluetooth_audio_codec codec;
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
profile = get_profile_from_index(this, result.index, &result.next, &codec);
|
2021-01-25 23:55:09 +02:00
|
|
|
|
|
|
|
|
switch (profile) {
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_OFF:
|
|
|
|
|
case DEVICE_PROFILE_AG:
|
|
|
|
|
case DEVICE_PROFILE_A2DP:
|
2022-06-17 15:12:24 +02:00
|
|
|
case DEVICE_PROFILE_BAP:
|
bluez: add a new "Audio Gateway" device profile
This profile is meant to be used with audio gateways, such as mobile
phones, making pipewire act as a headset. It activates all 3 "dynamic"
nodes (all of which are "Stream/*/Audio"), allowing both A2DP source
and HSP/HFP AG to be available at the same time. Ultimately, the remote
device (the AG), is the one that decides which profile to use and pipewire
just creates/destroys the appropriate stream nodes dynamically.
To make things less confusing, the HFP/HSP profile is now only available
if the remote device is a Head Unit and the A2DP profile is only available
if the remote device has an A2DP Sink.
If the device has both A2DP Source & A2DP Sink (not sure if this is a real world
possibility, but just in case...), the A2DP profile allows using them both,
while the AG profile will only allow the source.
In addition, to keep things less complex, the routes are now only used for
device nodes (the "Audio/*" ones). A2DP source and HSP/HFP AG never have a route.
Restoring their props should be possible to be handled by the restore-stream
module.
2021-03-19 19:34:35 +02:00
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
2022-01-06 16:26:11 +02:00
|
|
|
param = build_profile(this, &b, id, result.index, profile, codec, false);
|
2020-08-17 11:08:26 +02:00
|
|
|
if (param == NULL)
|
2020-07-03 12:14:00 +02:00
|
|
|
goto next;
|
2020-01-03 13:01:54 +01:00
|
|
|
break;
|
2020-07-02 17:12:33 +02:00
|
|
|
default:
|
|
|
|
|
return 0;
|
2020-01-03 13:01:54 +01:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case SPA_PARAM_Profile:
|
|
|
|
|
{
|
2021-01-25 23:55:09 +02:00
|
|
|
uint32_t index;
|
|
|
|
|
|
2020-01-03 13:01:54 +01:00
|
|
|
switch (result.index) {
|
|
|
|
|
case 0:
|
2021-03-21 02:38:26 +02:00
|
|
|
index = get_index_from_profile(this, this->profile, this->props.codec);
|
2022-01-06 16:26:11 +02:00
|
|
|
param = build_profile(this, &b, id, index, this->profile, this->props.codec, true);
|
2020-12-17 12:25:38 +01:00
|
|
|
if (param == NULL)
|
|
|
|
|
return 0;
|
2020-01-03 13:01:54 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
case SPA_PARAM_EnumRoute:
|
|
|
|
|
{
|
|
|
|
|
switch (result.index) {
|
2023-03-13 19:17:34 +02:00
|
|
|
case 0: case 1: case 2: case 3: case 4: case 5:
|
2022-03-05 18:31:41 +02:00
|
|
|
param = build_route(this, &b, id, result.index, SPA_ID_INVALID);
|
2021-01-22 17:35:27 +01:00
|
|
|
if (param == NULL)
|
|
|
|
|
goto next;
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case SPA_PARAM_Route:
|
|
|
|
|
{
|
|
|
|
|
switch (result.index) {
|
2023-03-13 19:17:34 +02:00
|
|
|
case 0: case 1: case 2: case 3: case 4: case 5:
|
2022-03-05 18:31:41 +02:00
|
|
|
param = build_route(this, &b, id, result.index, this->profile);
|
2021-01-07 18:10:22 +01:00
|
|
|
if (param == NULL)
|
|
|
|
|
goto next;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-03-21 02:38:26 +02:00
|
|
|
case SPA_PARAM_PropInfo:
|
|
|
|
|
{
|
|
|
|
|
switch (result.index) {
|
|
|
|
|
case 0:
|
2022-10-01 15:46:27 +03:00
|
|
|
param = build_prop_info_codec(this, &b, id);
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
param = spa_pod_builder_add_object(&b,
|
|
|
|
|
SPA_TYPE_OBJECT_PropInfo, id,
|
|
|
|
|
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_bluetoothOffloadActive),
|
|
|
|
|
SPA_PROP_INFO_description, SPA_POD_String("Bluetooth audio offload active"),
|
|
|
|
|
SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(false));
|
2021-03-21 02:38:26 +02:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case SPA_PARAM_Props:
|
|
|
|
|
{
|
|
|
|
|
switch (result.index) {
|
|
|
|
|
case 0:
|
|
|
|
|
param = build_props(this, &b, id);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2020-01-03 13:01:54 +01:00
|
|
|
default:
|
|
|
|
|
return -ENOENT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (spa_pod_filter(&b, &result.param, param, filter) < 0)
|
|
|
|
|
goto next;
|
|
|
|
|
|
|
|
|
|
spa_device_emit_result(&this->hooks, seq, 0,
|
|
|
|
|
SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
|
|
|
|
|
|
|
|
|
|
if (++count != num)
|
|
|
|
|
goto next;
|
|
|
|
|
|
|
|
|
|
return 0;
|
2018-11-26 12:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes)
|
|
|
|
|
{
|
2021-02-18 17:47:22 +01:00
|
|
|
uint32_t i;
|
2021-02-25 19:55:04 +02:00
|
|
|
int changed = 0;
|
2021-04-15 11:39:49 +08:00
|
|
|
struct spa_bt_transport_volume *t_volume;
|
2021-02-18 17:47:22 +01:00
|
|
|
|
|
|
|
|
if (n_volumes == 0)
|
|
|
|
|
return -EINVAL;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_info(this->log, "node %d volume %f", node->id, volumes[0]);
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-02-25 19:55:04 +02:00
|
|
|
for (i = 0; i < node->n_channels; i++) {
|
2021-04-15 11:39:49 +08:00
|
|
|
if (node->volumes[i] == volumes[i % n_volumes])
|
|
|
|
|
continue;
|
|
|
|
|
++changed;
|
2021-02-18 17:47:22 +01:00
|
|
|
node->volumes[i] = volumes[i % n_volumes];
|
2021-02-25 19:55:04 +02:00
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
t_volume = node->transport ? &node->transport->volumes[node->id]: NULL;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-04-17 18:53:28 +08:00
|
|
|
if (t_volume && t_volume->active
|
|
|
|
|
&& spa_bt_transport_volume_enabled(node->transport)) {
|
2021-04-15 11:39:49 +08:00
|
|
|
float hw_volume = node_get_hw_volume(node);
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_debug(this->log, "node %d hardware volume %f", node->id, hw_volume);
|
2021-04-15 11:39:49 +08:00
|
|
|
|
|
|
|
|
node_update_soft_volumes(node, hw_volume);
|
|
|
|
|
spa_bt_transport_set_volume(node->transport, node->id, hw_volume);
|
|
|
|
|
} else {
|
2021-08-21 18:37:44 +03:00
|
|
|
float boost = get_soft_volume_boost(node);
|
2021-04-15 11:39:49 +08:00
|
|
|
for (uint32_t i = 0; i < node->n_channels; ++i)
|
2021-08-21 18:37:44 +03:00
|
|
|
node->soft_volumes[i] = node->volumes[i] * boost;
|
2021-04-15 11:39:49 +08:00
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-06-03 05:26:34 +08:00
|
|
|
emit_volume(this, node);
|
|
|
|
|
|
2021-02-25 19:55:04 +02:00
|
|
|
return changed;
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int node_set_mute(struct impl *this, struct node *node, bool mute)
|
|
|
|
|
{
|
|
|
|
|
struct spa_event *event;
|
|
|
|
|
uint8_t buffer[4096];
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
|
|
|
|
struct spa_pod_frame f[1];
|
2021-02-25 19:55:04 +02:00
|
|
|
int changed = 0;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_info(this->log, "node %d mute %d", node->id, mute);
|
2021-02-25 19:55:04 +02:00
|
|
|
|
|
|
|
|
changed = (node->mute != mute);
|
2021-01-25 15:21:23 +01:00
|
|
|
node->mute = mute;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
spa_pod_builder_push_object(&b, &f[0],
|
|
|
|
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
|
|
|
|
|
spa_pod_builder_int(&b, node->id);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_add_object(&b,
|
2021-01-25 15:21:23 +01:00
|
|
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
|
2021-05-04 01:11:14 +08:00
|
|
|
SPA_PROP_mute, SPA_POD_Bool(mute),
|
|
|
|
|
SPA_PROP_softMute, SPA_POD_Bool(mute));
|
2021-01-07 18:10:22 +01:00
|
|
|
event = spa_pod_builder_pop(&b, &f[0]);
|
|
|
|
|
|
|
|
|
|
spa_device_emit_event(&this->hooks, event);
|
|
|
|
|
|
2021-02-25 19:55:04 +02:00
|
|
|
return changed;
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-13 22:59:04 +02:00
|
|
|
static int node_set_latency_offset(struct impl *this, struct node *node, int64_t latency_offset)
|
|
|
|
|
{
|
|
|
|
|
struct spa_event *event;
|
|
|
|
|
uint8_t buffer[4096];
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
|
|
|
|
struct spa_pod_frame f[1];
|
2021-02-25 19:55:04 +02:00
|
|
|
int changed = 0;
|
2021-02-13 22:59:04 +02:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_info(this->log, "node %d latency offset %"PRIi64" nsec", node->id, latency_offset);
|
2021-02-25 19:55:04 +02:00
|
|
|
|
|
|
|
|
changed = (node->latency_offset != latency_offset);
|
2021-02-13 22:59:04 +02:00
|
|
|
node->latency_offset = latency_offset;
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
spa_pod_builder_push_object(&b, &f[0],
|
|
|
|
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
|
|
|
|
|
spa_pod_builder_int(&b, node->id);
|
|
|
|
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_add_object(&b,
|
|
|
|
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
|
|
|
|
|
SPA_PROP_latencyOffsetNsec, SPA_POD_Long(latency_offset));
|
|
|
|
|
event = spa_pod_builder_pop(&b, &f[0]);
|
|
|
|
|
|
|
|
|
|
spa_device_emit_event(&this->hooks, event);
|
|
|
|
|
|
2021-02-25 19:55:04 +02:00
|
|
|
return changed;
|
2021-02-13 22:59:04 +02:00
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static int apply_device_props(struct impl *this, struct node *node, struct spa_pod *props)
|
|
|
|
|
{
|
|
|
|
|
float volume = 0;
|
|
|
|
|
bool mute = 0;
|
|
|
|
|
struct spa_pod_prop *prop;
|
|
|
|
|
struct spa_pod_object *obj = (struct spa_pod_object *) props;
|
|
|
|
|
int changed = 0;
|
|
|
|
|
float volumes[SPA_AUDIO_MAX_CHANNELS];
|
|
|
|
|
uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
|
2021-02-25 19:55:04 +02:00
|
|
|
uint32_t n_volumes = 0, SPA_UNUSED n_channels = 0;
|
2021-02-13 22:59:04 +02:00
|
|
|
int64_t latency_offset = 0;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props))
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
|
|
SPA_POD_OBJECT_FOREACH(obj, prop) {
|
|
|
|
|
switch (prop->key) {
|
|
|
|
|
case SPA_PROP_volume:
|
|
|
|
|
if (spa_pod_get_float(&prop->value, &volume) == 0) {
|
2021-02-25 19:55:04 +02:00
|
|
|
int res = node_set_volume(this, node, &volume, 1);
|
|
|
|
|
if (res > 0)
|
|
|
|
|
++changed;
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case SPA_PROP_mute:
|
|
|
|
|
if (spa_pod_get_bool(&prop->value, &mute) == 0) {
|
2021-02-25 19:55:04 +02:00
|
|
|
int res = node_set_mute(this, node, mute);
|
|
|
|
|
if (res > 0)
|
|
|
|
|
++changed;
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case SPA_PROP_channelVolumes:
|
2021-02-25 19:55:04 +02:00
|
|
|
n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
|
|
|
|
|
volumes, SPA_AUDIO_MAX_CHANNELS);
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
|
|
|
|
case SPA_PROP_channelMap:
|
2021-02-25 19:55:04 +02:00
|
|
|
n_channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
|
|
|
|
|
channels, SPA_AUDIO_MAX_CHANNELS);
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
2021-02-13 22:59:04 +02:00
|
|
|
case SPA_PROP_latencyOffsetNsec:
|
|
|
|
|
if (spa_pod_get_long(&prop->value, &latency_offset) == 0) {
|
2021-02-25 19:55:04 +02:00
|
|
|
int res = node_set_latency_offset(this, node, latency_offset);
|
|
|
|
|
if (res > 0)
|
|
|
|
|
++changed;
|
2021-02-13 22:59:04 +02:00
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
}
|
|
|
|
|
}
|
2021-02-25 19:55:04 +02:00
|
|
|
if (n_volumes > 0) {
|
|
|
|
|
int res = node_set_volume(this, node, volumes, n_volumes);
|
|
|
|
|
if (res > 0)
|
|
|
|
|
++changed;
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
return changed;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-01 15:46:27 +03:00
|
|
|
static void apply_prop_offload_active(struct impl *this, bool active)
|
|
|
|
|
{
|
|
|
|
|
bool old_value = this->props.offload_active;
|
2023-03-13 19:17:34 +02:00
|
|
|
unsigned int i;
|
2022-10-01 15:46:27 +03:00
|
|
|
|
|
|
|
|
this->props.offload_active = active;
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
for (i = 0; i < SPA_N_ELEMENTS(this->nodes); i++) {
|
2022-10-01 15:46:27 +03:00
|
|
|
node_offload_set_active(&this->nodes[i], active);
|
|
|
|
|
if (!this->nodes[i].offload_acquired)
|
|
|
|
|
this->props.offload_active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->props.offload_active != old_value) {
|
|
|
|
|
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
emit_info(this, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 16:11:23 +02:00
|
|
|
static int impl_set_param(void *object,
|
2018-11-26 12:18:53 +01:00
|
|
|
uint32_t id, uint32_t flags,
|
|
|
|
|
const struct spa_pod *param)
|
|
|
|
|
{
|
2020-01-03 13:01:54 +01:00
|
|
|
struct impl *this = object;
|
|
|
|
|
int res;
|
|
|
|
|
|
|
|
|
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
|
|
|
|
|
|
|
|
|
switch (id) {
|
|
|
|
|
case SPA_PARAM_Profile:
|
|
|
|
|
{
|
2021-07-06 17:55:16 +02:00
|
|
|
uint32_t idx, next;
|
2021-01-25 23:55:09 +02:00
|
|
|
uint32_t profile;
|
2021-03-21 02:38:26 +02:00
|
|
|
enum spa_bluetooth_audio_codec codec;
|
2022-01-06 16:26:11 +02:00
|
|
|
bool save = false;
|
2020-01-03 13:01:54 +01:00
|
|
|
|
2021-03-27 13:55:01 +02:00
|
|
|
if (param == NULL)
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
2020-01-03 13:01:54 +01:00
|
|
|
if ((res = spa_pod_parse_object(param,
|
|
|
|
|
SPA_TYPE_OBJECT_ParamProfile, NULL,
|
2022-01-06 16:26:11 +02:00
|
|
|
SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx),
|
|
|
|
|
SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) {
|
2020-01-03 13:01:54 +01:00
|
|
|
spa_log_warn(this->log, "can't parse profile");
|
2023-01-18 17:41:16 +01:00
|
|
|
spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
|
2020-01-03 13:01:54 +01:00
|
|
|
return res;
|
|
|
|
|
}
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2021-07-06 17:55:16 +02:00
|
|
|
profile = get_profile_from_index(this, idx, &next, &codec);
|
2021-01-25 23:55:09 +02:00
|
|
|
if (profile == SPA_ID_INVALID)
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
spa_log_debug(this->log, "%p: setting profile %d codec:%d save:%d", this,
|
|
|
|
|
profile, codec, (int)save);
|
2022-01-06 16:26:11 +02:00
|
|
|
return set_profile(this, profile, codec, save);
|
2020-01-03 13:01:54 +01:00
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
case SPA_PARAM_Route:
|
|
|
|
|
{
|
2021-07-06 17:55:16 +02:00
|
|
|
uint32_t idx, device;
|
2021-01-07 18:10:22 +01:00
|
|
|
struct spa_pod *props = NULL;
|
|
|
|
|
struct node *node;
|
2021-03-30 16:31:17 +02:00
|
|
|
bool save = false;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-03-27 13:55:01 +02:00
|
|
|
if (param == NULL)
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
if ((res = spa_pod_parse_object(param,
|
|
|
|
|
SPA_TYPE_OBJECT_ParamRoute, NULL,
|
2021-07-06 17:55:16 +02:00
|
|
|
SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
|
2021-01-07 18:10:22 +01:00
|
|
|
SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
|
2021-03-30 16:31:17 +02:00
|
|
|
SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props),
|
|
|
|
|
SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) {
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_log_warn(this->log, "can't parse route");
|
2023-01-18 17:41:16 +01:00
|
|
|
spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
|
2021-01-07 18:10:22 +01:00
|
|
|
return res;
|
|
|
|
|
}
|
2023-03-13 19:17:34 +02:00
|
|
|
if (device >= SPA_N_ELEMENTS(this->nodes) || !this->nodes[device].active)
|
2021-01-07 18:10:22 +01:00
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
|
|
node = &this->nodes[device];
|
2021-03-30 16:31:17 +02:00
|
|
|
node->save = save;
|
2021-01-07 18:10:22 +01:00
|
|
|
if (props) {
|
2021-02-25 19:55:04 +02:00
|
|
|
int changed = apply_device_props(this, node, props);
|
|
|
|
|
if (changed > 0) {
|
|
|
|
|
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
emit_info(this, false);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-03-21 02:38:26 +02:00
|
|
|
case SPA_PARAM_Props:
|
|
|
|
|
{
|
2021-03-27 09:27:33 +08:00
|
|
|
uint32_t codec_id = SPA_ID_INVALID;
|
2022-10-01 15:46:27 +03:00
|
|
|
bool offload_active = this->props.offload_active;
|
2021-03-21 02:38:26 +02:00
|
|
|
|
|
|
|
|
if (param == NULL)
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
if ((res = spa_pod_parse_object(param,
|
|
|
|
|
SPA_TYPE_OBJECT_Props, NULL,
|
2022-10-01 15:46:27 +03:00
|
|
|
SPA_PROP_bluetoothAudioCodec, SPA_POD_OPT_Id(&codec_id),
|
|
|
|
|
SPA_PROP_bluetoothOffloadActive, SPA_POD_OPT_Bool(&offload_active))) < 0) {
|
2021-03-21 02:38:26 +02:00
|
|
|
spa_log_warn(this->log, "can't parse props");
|
2023-01-18 17:41:16 +01:00
|
|
|
spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
|
2021-03-21 02:38:26 +02:00
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-01 15:46:27 +03:00
|
|
|
spa_log_debug(this->log, "setting props codec:%d offload:%d", (int)codec_id, (int)offload_active);
|
|
|
|
|
|
|
|
|
|
apply_prop_offload_active(this, offload_active);
|
2022-01-06 16:26:11 +02:00
|
|
|
|
2021-03-21 02:38:26 +02:00
|
|
|
if (codec_id == SPA_ID_INVALID)
|
|
|
|
|
return 0;
|
|
|
|
|
|
2022-06-17 15:12:24 +02:00
|
|
|
if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
|
2021-03-21 02:38:26 +02:00
|
|
|
size_t j;
|
|
|
|
|
for (j = 0; j < this->supported_codec_count; ++j) {
|
|
|
|
|
if (this->supported_codecs[j]->id == codec_id) {
|
2022-01-06 16:26:11 +02:00
|
|
|
return set_profile(this, this->profile, codec_id, true);
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
|
|
|
|
|
if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD &&
|
|
|
|
|
spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_CVSD) == 1) {
|
2022-01-06 16:26:11 +02:00
|
|
|
return set_profile(this, this->profile, codec_id, true);
|
2021-03-21 02:38:26 +02:00
|
|
|
} else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_MSBC &&
|
|
|
|
|
spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_MSBC) == 1) {
|
2022-01-06 16:26:11 +02:00
|
|
|
return set_profile(this, this->profile, codec_id, true);
|
2021-03-21 02:38:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
2020-01-03 13:01:54 +01:00
|
|
|
default:
|
|
|
|
|
return -ENOENT;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
2018-11-26 12:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
2019-05-20 16:11:23 +02:00
|
|
|
static const struct spa_device_methods impl_device = {
|
|
|
|
|
SPA_VERSION_DEVICE_METHODS,
|
|
|
|
|
.add_listener = impl_add_listener,
|
2020-01-03 13:01:54 +01:00
|
|
|
.sync = impl_sync,
|
2019-05-20 16:11:23 +02:00
|
|
|
.enum_params = impl_enum_params,
|
|
|
|
|
.set_param = impl_set_param,
|
2018-11-26 12:18:53 +01:00
|
|
|
};
|
|
|
|
|
|
2019-12-19 13:15:10 +01:00
|
|
|
static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
|
2018-11-26 12:18:53 +01:00
|
|
|
{
|
|
|
|
|
struct impl *this;
|
|
|
|
|
|
|
|
|
|
spa_return_val_if_fail(handle != NULL, -EINVAL);
|
|
|
|
|
spa_return_val_if_fail(interface != NULL, -EINVAL);
|
|
|
|
|
|
|
|
|
|
this = (struct impl *) handle;
|
|
|
|
|
|
2021-05-18 11:36:13 +10:00
|
|
|
if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
|
2018-11-26 12:18:53 +01:00
|
|
|
*interface = &this->device;
|
|
|
|
|
else
|
|
|
|
|
return -ENOENT;
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int impl_clear(struct spa_handle *handle)
|
|
|
|
|
{
|
2021-01-25 19:57:45 +02:00
|
|
|
struct impl *this = (struct impl *) handle;
|
2021-03-14 17:53:31 +08:00
|
|
|
const struct spa_dict_item *it;
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2021-04-15 11:39:49 +08:00
|
|
|
emit_remove_nodes(this);
|
|
|
|
|
|
2021-01-25 23:55:09 +02:00
|
|
|
free(this->supported_codecs);
|
2021-03-14 17:53:31 +08:00
|
|
|
if (this->bt_dev) {
|
|
|
|
|
this->bt_dev->settings = NULL;
|
2021-01-25 19:57:45 +02:00
|
|
|
spa_hook_remove(&this->bt_dev_listener);
|
2021-03-14 17:53:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_dict_for_each(it, &this->setting_dict) {
|
|
|
|
|
if(it->key)
|
|
|
|
|
free((void *)it->key);
|
|
|
|
|
if(it->value)
|
|
|
|
|
free((void *)it->value);
|
|
|
|
|
}
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
device_set_clear(this);
|
2018-11-26 12:18:53 +01:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static size_t
|
|
|
|
|
impl_get_size(const struct spa_handle_factory *factory,
|
|
|
|
|
const struct spa_dict *params)
|
|
|
|
|
{
|
|
|
|
|
return sizeof(struct impl);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-14 17:53:31 +08:00
|
|
|
static const struct spa_dict*
|
|
|
|
|
filter_bluez_device_setting(struct impl *this, const struct spa_dict *dict)
|
|
|
|
|
{
|
|
|
|
|
uint32_t n_items = 0;
|
|
|
|
|
for (uint32_t i = 0
|
|
|
|
|
; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->setting_items)
|
|
|
|
|
; i++)
|
|
|
|
|
{
|
|
|
|
|
const struct spa_dict_item *it = &dict->items[i];
|
|
|
|
|
if (it->key != NULL && strncmp(it->key, "bluez", 5) == 0 && it->value != NULL) {
|
|
|
|
|
this->setting_items[n_items++] =
|
|
|
|
|
SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this->setting_dict = SPA_DICT_INIT(this->setting_items, n_items);
|
|
|
|
|
return &this->setting_dict;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
static int
|
|
|
|
|
impl_init(const struct spa_handle_factory *factory,
|
|
|
|
|
struct spa_handle *handle,
|
|
|
|
|
const struct spa_dict *info,
|
|
|
|
|
const struct spa_support *support,
|
|
|
|
|
uint32_t n_support)
|
|
|
|
|
{
|
|
|
|
|
struct impl *this;
|
2019-12-19 13:15:10 +01:00
|
|
|
const char *str;
|
2018-11-26 12:18:53 +01:00
|
|
|
|
|
|
|
|
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
|
|
|
|
spa_return_val_if_fail(handle != NULL, -EINVAL);
|
|
|
|
|
|
|
|
|
|
handle->get_interface = impl_get_interface;
|
|
|
|
|
handle->clear = impl_clear;
|
|
|
|
|
|
|
|
|
|
this = (struct impl *) handle;
|
|
|
|
|
|
2019-12-19 13:15:10 +01:00
|
|
|
this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
|
2021-04-15 17:42:02 +02:00
|
|
|
_i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N);
|
2019-12-19 13:15:10 +01:00
|
|
|
|
2021-10-01 19:03:49 +03:00
|
|
|
spa_log_topic_init(this->log, &log_topic);
|
|
|
|
|
|
2019-12-19 13:15:10 +01:00
|
|
|
if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_DEVICE)))
|
|
|
|
|
sscanf(str, "pointer:%p", &this->bt_dev);
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2018-11-27 17:08:36 +01:00
|
|
|
if (this->bt_dev == NULL) {
|
|
|
|
|
spa_log_error(this->log, "a device is needed");
|
2018-11-26 12:18:53 +01:00
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
2021-03-14 17:53:31 +08:00
|
|
|
|
2021-03-26 12:53:04 +08:00
|
|
|
if (info) {
|
|
|
|
|
int profiles;
|
|
|
|
|
this->bt_dev->settings = filter_bluez_device_setting(this, info);
|
2021-04-17 18:53:28 +08:00
|
|
|
|
2021-04-22 08:26:46 +08:00
|
|
|
if ((str = spa_dict_lookup(info, "bluez5.auto-connect")) != NULL) {
|
2021-04-17 18:53:28 +08:00
|
|
|
if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0)
|
|
|
|
|
this->bt_dev->reconnect_profiles = profiles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((str = spa_dict_lookup(info, "bluez5.hw-volume")) != NULL) {
|
|
|
|
|
if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0)
|
|
|
|
|
this->bt_dev->hw_volume_profiles = profiles;
|
|
|
|
|
}
|
2021-03-26 12:53:04 +08:00
|
|
|
}
|
2021-03-14 17:53:31 +08:00
|
|
|
|
2019-05-20 16:11:23 +02:00
|
|
|
this->device.iface = SPA_INTERFACE_INIT(
|
|
|
|
|
SPA_TYPE_INTERFACE_Device,
|
|
|
|
|
SPA_VERSION_DEVICE,
|
|
|
|
|
&impl_device, this);
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2019-03-01 12:00:42 +01:00
|
|
|
spa_hook_list_init(&this->hooks);
|
|
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
reset_props(&this->props);
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
init_node(this, &this->nodes[0], 0);
|
|
|
|
|
init_node(this, &this->nodes[1], 1);
|
2023-03-13 19:17:34 +02:00
|
|
|
init_node(this, &this->nodes[2], 2);
|
|
|
|
|
init_node(this, &this->nodes[3], 3);
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
this->info = SPA_DEVICE_INFO_INIT();
|
|
|
|
|
this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS |
|
|
|
|
|
SPA_DEVICE_CHANGE_MASK_PARAMS;
|
|
|
|
|
|
|
|
|
|
this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
|
|
|
|
|
this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
|
|
|
|
|
this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ);
|
|
|
|
|
this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, SPA_PARAM_INFO_READWRITE);
|
2021-03-21 02:38:26 +02:00
|
|
|
this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
|
|
|
|
|
this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
|
2021-01-07 18:10:22 +01:00
|
|
|
this->info.params = this->params;
|
2021-03-21 02:38:26 +02:00
|
|
|
this->info.n_params = 6;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
spa_bt_device_add_listener(this->bt_dev, &this->bt_dev_listener, &bt_dev_events, this);
|
|
|
|
|
|
2023-03-13 19:17:34 +02:00
|
|
|
this->device_set.impl = this;
|
|
|
|
|
|
2021-02-10 21:30:15 +02:00
|
|
|
set_initial_profile(this);
|
|
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct spa_interface_info impl_interfaces[] = {
|
|
|
|
|
{SPA_TYPE_INTERFACE_Device,},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
impl_enum_interface_info(const struct spa_handle_factory *factory,
|
|
|
|
|
const struct spa_interface_info **info,
|
|
|
|
|
uint32_t *index)
|
|
|
|
|
{
|
|
|
|
|
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
|
|
|
|
spa_return_val_if_fail(info != NULL, -EINVAL);
|
|
|
|
|
spa_return_val_if_fail(index != NULL, -EINVAL);
|
|
|
|
|
|
|
|
|
|
if (*index >= SPA_N_ELEMENTS(impl_interfaces))
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
*info = &impl_interfaces[(*index)++];
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-03 16:48:01 +02:00
|
|
|
static const struct spa_dict_item handle_info_items[] = {
|
|
|
|
|
{ SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
|
|
|
|
|
{ SPA_KEY_FACTORY_DESCRIPTION, "A bluetooth device" },
|
|
|
|
|
{ SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_DEVICE"=<device>" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct spa_dict handle_info = SPA_DICT_INIT_ARRAY(handle_info_items);
|
|
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
const struct spa_handle_factory spa_bluez5_device_factory = {
|
|
|
|
|
SPA_VERSION_HANDLE_FACTORY,
|
2019-06-21 13:31:34 +02:00
|
|
|
SPA_NAME_API_BLUEZ5_DEVICE,
|
2019-06-03 16:48:01 +02:00
|
|
|
&handle_info,
|
2018-11-26 12:18:53 +01:00
|
|
|
impl_get_size,
|
|
|
|
|
impl_init,
|
|
|
|
|
impl_enum_interface_info,
|
|
|
|
|
};
|