2019-02-27 16:43:01 +01:00
|
|
|
/* Spa Bluez5 Device
|
2018-11-26 12:18:53 +01:00
|
|
|
*
|
|
|
|
|
* Copyright © 2018 Wim Taymans
|
|
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
|
|
|
* to deal in the Software without restriction, including without limitation
|
|
|
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
|
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice (including the next
|
|
|
|
|
* paragraph) shall be included in all copies or substantial portions of the
|
|
|
|
|
* Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
|
|
|
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
|
|
* DEALINGS IN THE SOFTWARE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#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>
|
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>
|
|
|
|
|
#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>
|
2020-01-03 13:01:54 +01:00
|
|
|
#include <spa/debug/pod.h>
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2018-11-27 17:08:36 +01:00
|
|
|
#include "defs.h"
|
2020-12-14 13:29:12 +01:00
|
|
|
#include "a2dp-codecs.h"
|
2018-11-27 17:08:36 +01:00
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
#define NAME "bluez5-device"
|
|
|
|
|
|
|
|
|
|
#define MAX_DEVICES 64
|
|
|
|
|
|
2021-02-02 23:12:35 +02:00
|
|
|
#define DEVICE_ID_SOURCE 0
|
|
|
|
|
#define DEVICE_ID_SINK 1
|
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
|
|
|
|
|
|
|
|
|
|
enum {
|
|
|
|
|
DEVICE_PROFILE_OFF = 0,
|
|
|
|
|
DEVICE_PROFILE_AG = 1,
|
|
|
|
|
DEVICE_PROFILE_A2DP = 2,
|
|
|
|
|
DEVICE_PROFILE_HSP_HFP = 3,
|
|
|
|
|
};
|
2021-02-02 23:12:35 +02:00
|
|
|
|
2018-11-26 12:18:53 +01:00
|
|
|
static const char default_device[] = "";
|
|
|
|
|
|
|
|
|
|
struct props {
|
|
|
|
|
char device[64];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void reset_props(struct props *props)
|
|
|
|
|
{
|
|
|
|
|
strncpy(props->device, default_device, 64);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
struct node {
|
|
|
|
|
uint32_t id;
|
|
|
|
|
unsigned int active:1;
|
|
|
|
|
unsigned int mute:1;
|
|
|
|
|
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-03-17 22:00:44 +02:00
|
|
|
struct impl;
|
|
|
|
|
struct dynamic_node
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
struct spa_bt_transport *transport;
|
|
|
|
|
struct spa_hook transport_listener;
|
|
|
|
|
uint32_t id;
|
|
|
|
|
const char *factory_name;
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
|
|
struct spa_param_info params[4];
|
|
|
|
|
|
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-25 19:57:45 +02:00
|
|
|
const struct a2dp_codec *selected_a2dp_codec; /**< Codec wanted. NULL means any. */
|
2021-03-18 23:15:03 +02:00
|
|
|
int selected_hfp_codec;
|
2021-01-29 19:41:26 +02:00
|
|
|
unsigned int switching_codec:1;
|
|
|
|
|
uint32_t prev_bt_connected_profiles;
|
2021-01-25 19:57:45 +02:00
|
|
|
|
2021-01-25 23:55:09 +02:00
|
|
|
const struct a2dp_codec **supported_codecs;
|
|
|
|
|
size_t supported_codec_count;
|
|
|
|
|
|
2021-03-17 22:00:44 +02:00
|
|
|
struct dynamic_node dyn_a2dp_source;
|
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;
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
struct node nodes[2];
|
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;
|
|
|
|
|
for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
|
|
|
|
|
node->volumes[i] = 1.0;
|
|
|
|
|
}
|
|
|
|
|
|
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-02-12 17:20:37 +01:00
|
|
|
static const char *get_codec_name(struct spa_bt_transport *t)
|
|
|
|
|
{
|
|
|
|
|
if (t->a2dp_codec != NULL)
|
|
|
|
|
return t->a2dp_codec->name;
|
2021-03-20 00:11:12 +02:00
|
|
|
return get_hfp_codec_name(t->codec);
|
2021-02-12 17:20:37 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static void emit_node(struct impl *this, struct spa_bt_transport *t,
|
|
|
|
|
uint32_t id, const char *factory_name)
|
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;
|
2021-03-22 16:37:52 +01:00
|
|
|
struct spa_dict_item items[6];
|
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;
|
2021-01-07 18:10:22 +01:00
|
|
|
char transport[32], str_id[32];
|
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
|
|
|
|
|
|
|
|
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-02-12 17:20:37 +01:00
|
|
|
items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CODEC, get_codec_name(t));
|
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;
|
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) {
|
|
|
|
|
snprintf(str_id, sizeof(str_id), "%d", id);
|
2021-03-22 16:37:52 +01:00
|
|
|
items[5] = 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++;
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
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) {
|
|
|
|
|
if (this->nodes[id].n_channels > 0) {
|
|
|
|
|
size_t i;
|
2021-03-15 23:07:46 +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
|
|
|
/*
|
|
|
|
|
* 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 = this->nodes[id].n_channels; i < t->n_channels; ++i)
|
|
|
|
|
this->nodes[id].volumes[i] = this->nodes[id].volumes[i % this->nodes[id].n_channels];
|
|
|
|
|
}
|
2021-03-15 23:07:46 +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->nodes[id].active = true;
|
|
|
|
|
this->nodes[id].n_channels = t->n_channels;
|
|
|
|
|
memcpy(this->nodes[id].channels, t->channels,
|
|
|
|
|
t->n_channels * sizeof(uint32_t));
|
|
|
|
|
}
|
2019-04-19 13:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
static struct spa_bt_transport *find_transport(struct impl *this, int profile, const struct a2dp_codec *a2dp_codec, unsigned int hfp_codec)
|
2019-04-19 13:26:07 -04:00
|
|
|
{
|
|
|
|
|
struct spa_bt_device *device = this->bt_dev;
|
2019-07-31 12:11:56 -04:00
|
|
|
struct spa_bt_transport *t;
|
2021-01-25 19:57:45 +02:00
|
|
|
const struct a2dp_codec **codecs;
|
|
|
|
|
size_t i, num_codecs;
|
|
|
|
|
|
|
|
|
|
codecs = &a2dp_codec;
|
|
|
|
|
num_codecs = 1;
|
|
|
|
|
|
|
|
|
|
if (a2dp_codec == NULL && (profile == SPA_BT_PROFILE_A2DP_SOURCE || profile == SPA_BT_PROFILE_A2DP_SINK)) {
|
|
|
|
|
codecs = a2dp_codecs;
|
|
|
|
|
num_codecs = 0;
|
|
|
|
|
while (codecs[num_codecs] != NULL)
|
|
|
|
|
++num_codecs;
|
|
|
|
|
}
|
2018-11-26 12:18:53 +01:00
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
for (i = 0; i < num_codecs; ++i) {
|
|
|
|
|
spa_list_for_each(t, &device->transport_list, device_link) {
|
2021-03-06 14:46:57 +02:00
|
|
|
if ((t->profile & device->connected_profiles) &&
|
2021-01-25 19:57:45 +02:00
|
|
|
(t->profile & profile) == t->profile &&
|
2021-03-18 23:15:03 +02:00
|
|
|
(codecs[i] == NULL || t->a2dp_codec == codecs[i]) &&
|
|
|
|
|
(hfp_codec == 0 || t->codec == hfp_codec))
|
2021-01-25 19:57:45 +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;
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2021-03-17 22:00:44 +02:00
|
|
|
emit_node(impl, t, this->id, this->factory_name);
|
|
|
|
|
}
|
|
|
|
|
} 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);
|
2021-03-17 22:00:44 +02:00
|
|
|
spa_device_emit_object_info(&impl->hooks, this->id, NULL);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void emit_dynamic_node(struct dynamic_node *this, struct impl *impl,
|
|
|
|
|
struct spa_bt_transport *t, uint32_t id, const char *factory_name)
|
|
|
|
|
{
|
|
|
|
|
if (this->transport != NULL)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
this->impl = impl;
|
|
|
|
|
this->transport = t;
|
|
|
|
|
this->id = id;
|
|
|
|
|
this->factory_name = factory_name;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
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-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HFP_AG, NULL, 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-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HSP_AG, NULL, 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) {
|
|
|
|
|
emit_dynamic_node(&this->dyn_sco_source, this, t,
|
|
|
|
|
0, SPA_NAME_API_BLUEZ5_SCO_SOURCE);
|
|
|
|
|
emit_dynamic_node(&this->dyn_sco_sink, this, t,
|
|
|
|
|
1, SPA_NAME_API_BLUEZ5_SCO_SINK);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) {
|
2021-03-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_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
|
|
|
if (t)
|
|
|
|
|
emit_dynamic_node(&this->dyn_a2dp_source, this, t,
|
|
|
|
|
2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE);
|
|
|
|
|
}
|
|
|
|
|
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-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, this->selected_a2dp_codec, 0);
|
2020-07-02 17:12:33 +02:00
|
|
|
if (t)
|
2021-03-17 22:00:44 +02:00
|
|
|
emit_dynamic_node(&this->dyn_a2dp_source, this, t,
|
|
|
|
|
DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE);
|
2020-07-02 17:12:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
|
2021-03-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->selected_a2dp_codec, 0);
|
2020-07-02 17:12:33 +02:00
|
|
|
if (t)
|
2021-02-02 23:12:35 +02:00
|
|
|
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK);
|
2020-07-02 17:12:33 +02:00
|
|
|
}
|
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-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HFP_HF, NULL, this->selected_hfp_codec);
|
2021-03-18 22:44:36 +02:00
|
|
|
if (!t)
|
2021-03-18 23:15:03 +02:00
|
|
|
t = find_transport(this, SPA_BT_PROFILE_HSP_HS, NULL, 0);
|
2021-03-18 22:44:36 +02:00
|
|
|
if (t) {
|
|
|
|
|
emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE);
|
|
|
|
|
emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK);
|
2020-07-02 17:12:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
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)
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
this->info.change_mask = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
static void emit_remove_nodes(struct impl *this)
|
2020-01-10 13:25:40 +01:00
|
|
|
{
|
2020-07-03 16:12:19 +02:00
|
|
|
uint32_t i;
|
|
|
|
|
|
2021-03-17 22:00:44 +02:00
|
|
|
remove_dynamic_node (&this->dyn_a2dp_source);
|
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
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
for (i = 0; i < 2; i++) {
|
|
|
|
|
if (this->nodes[i].active) {
|
|
|
|
|
spa_device_emit_object_info(&this->hooks, i, NULL);
|
|
|
|
|
this->nodes[i].active = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-01-25 19:57:45 +02:00
|
|
|
}
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
static int set_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *a2dp_codec, int hfp_codec)
|
2021-01-25 19:57:45 +02:00
|
|
|
{
|
2021-03-18 23:15:03 +02:00
|
|
|
if (this->profile == profile &&
|
|
|
|
|
(this->profile != DEVICE_PROFILE_A2DP || a2dp_codec == this->selected_a2dp_codec) &&
|
|
|
|
|
(this->profile != DEVICE_PROFILE_HSP_HFP || hfp_codec == this->selected_hfp_codec))
|
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-01-25 19:57:45 +02:00
|
|
|
|
2021-02-06 18:47:07 +02:00
|
|
|
/*
|
|
|
|
|
* A2DP: ensure there's a transport with the selected codec (NULL means any).
|
|
|
|
|
* Don't try to switch codecs when the device is in the A2DP source role, since
|
|
|
|
|
* devices do not appear to like that.
|
|
|
|
|
*/
|
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 (profile == DEVICE_PROFILE_A2DP && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) {
|
2021-01-25 19:57:45 +02:00
|
|
|
int ret;
|
|
|
|
|
const struct a2dp_codec *codec_list[2], **codecs;
|
|
|
|
|
|
|
|
|
|
if (a2dp_codec == NULL) {
|
|
|
|
|
codecs = a2dp_codecs;
|
|
|
|
|
} else {
|
|
|
|
|
codec_list[0] = a2dp_codec;
|
|
|
|
|
codec_list[1] = NULL;
|
|
|
|
|
codecs = codec_list;
|
|
|
|
|
}
|
2020-07-03 16:12:19 +02:00
|
|
|
|
2021-01-29 19:41:26 +02:00
|
|
|
this->switching_codec = true;
|
2021-03-18 23:15:03 +02:00
|
|
|
this->selected_a2dp_codec = a2dp_codec;
|
2021-01-29 19:41:26 +02:00
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
ret = spa_bt_device_ensure_a2dp_codec(this->bt_dev, codecs);
|
2021-03-18 23:15:03 +02:00
|
|
|
if (ret < 0) {
|
|
|
|
|
if (ret != -ENOTSUP)
|
|
|
|
|
spa_log_error(this->log, NAME": failed to switch codec (%d), setting basic profile", ret);
|
|
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
} else if (profile == DEVICE_PROFILE_HSP_HFP && hfp_codec != 0 && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) {
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
this->switching_codec = true;
|
|
|
|
|
this->selected_hfp_codec = hfp_codec;
|
|
|
|
|
|
|
|
|
|
ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, hfp_codec);
|
|
|
|
|
if (ret < 0) {
|
|
|
|
|
if (ret != -ENOTSUP)
|
|
|
|
|
spa_log_error(this->log, NAME": failed to switch codec (%d), setting basic profile", ret);
|
|
|
|
|
} else {
|
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;
|
|
|
|
|
this->selected_a2dp_codec = NULL;
|
2021-03-18 23:15:03 +02:00
|
|
|
this->selected_hfp_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;
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
spa_log_debug(this->log, NAME": codec switched (status %d)", status);
|
|
|
|
|
|
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 */
|
|
|
|
|
spa_log_error(this->log, NAME": failed to switch codec (%d), setting fallback profile", status);
|
2021-03-18 23:15:03 +02:00
|
|
|
if (this->profile == DEVICE_PROFILE_A2DP && this->selected_a2dp_codec != NULL) {
|
2021-01-25 19:57:45 +02:00
|
|
|
this->selected_a2dp_codec = NULL;
|
2021-03-18 23:15:03 +02:00
|
|
|
} else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->selected_hfp_codec != 0) {
|
|
|
|
|
this->selected_hfp_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;
|
|
|
|
|
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. */
|
|
|
|
|
spa_log_info(this->log, NAME": profiles changed to %08x %08x (prev %08x %08x, change %08x)"
|
|
|
|
|
" 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;
|
|
|
|
|
|
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 |
|
|
|
|
|
SPA_BT_PROFILE_A2DP_SOURCE));
|
|
|
|
|
spa_log_debug(this->log, NAME": 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:
|
2021-01-29 19:41:26 +02:00
|
|
|
nodes_changed = (connected_change & (SPA_BT_PROFILE_A2DP_SINK |
|
|
|
|
|
SPA_BT_PROFILE_A2DP_SOURCE));
|
|
|
|
|
spa_log_debug(this->log, NAME": profiles changed: A2DP nodes changed: %d",
|
|
|
|
|
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:
|
|
|
|
|
nodes_changed = (connected_change & SPA_BT_PROFILE_HEADSET_HEAD_UNIT);
|
|
|
|
|
spa_log_debug(this->log, NAME": profiles changed: HSP/HFP nodes changed: %d",
|
|
|
|
|
nodes_changed);
|
|
|
|
|
break;
|
2021-01-29 19:41:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nodes_changed) {
|
|
|
|
|
emit_remove_nodes(this);
|
|
|
|
|
emit_nodes(this);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-15 20:35:24 +02:00
|
|
|
if (connected_change & SPA_BT_PROFILE_A2DP_SINK) {
|
|
|
|
|
free(this->supported_codecs);
|
|
|
|
|
this->supported_codecs = spa_bt_device_get_supported_a2dp_codecs(
|
|
|
|
|
this->bt_dev, &this->supported_codec_count);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-29 19:41:26 +02:00
|
|
|
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-01-29 19:41:26 +02:00
|
|
|
emit_info(this, false);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-14 14:06:50 +08:00
|
|
|
static void set_initial_profile(struct impl *this);
|
|
|
|
|
|
|
|
|
|
static void device_connected(void *userdata, bool connected) {
|
|
|
|
|
struct impl *this = userdata;
|
|
|
|
|
|
|
|
|
|
spa_log_debug(this->log, "connected: %d", connected);
|
|
|
|
|
|
|
|
|
|
if (connected ^ (this->profile != 0))
|
|
|
|
|
set_initial_profile(this);
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
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-01-07 18:10:22 +01:00
|
|
|
static uint32_t profile_direction_mask(struct impl *this, uint32_t index)
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
if (device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK)
|
2021-01-07 18:10:22 +01:00
|
|
|
have_output = 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;
|
|
|
|
|
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-18 23:15:03 +02:00
|
|
|
static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, const struct a2dp_codec **codec, int *hfp_codec)
|
2021-01-25 23:55:09 +02:00
|
|
|
{
|
2021-03-18 23:15:03 +02:00
|
|
|
uint32_t a2dp_codec_mask = 0x100;
|
|
|
|
|
uint32_t hfp_codec_mask = 0x200;
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
*codec = NULL;
|
2021-03-18 23:15:03 +02:00
|
|
|
*hfp_codec = 0;
|
|
|
|
|
*next = index + 1;
|
|
|
|
|
|
|
|
|
|
if (index < a2dp_codec_mask) {
|
|
|
|
|
if (index >= 3)
|
|
|
|
|
*next = a2dp_codec_mask;
|
|
|
|
|
if (index <= 3)
|
|
|
|
|
return index;
|
|
|
|
|
} else if (index & a2dp_codec_mask) {
|
|
|
|
|
uint32_t i = index & ~a2dp_codec_mask;
|
|
|
|
|
if (i + 1 >= this->supported_codec_count)
|
|
|
|
|
*next = hfp_codec_mask;
|
|
|
|
|
if (i < this->supported_codec_count) {
|
|
|
|
|
*codec = this->supported_codecs[i];
|
|
|
|
|
return DEVICE_PROFILE_A2DP;
|
|
|
|
|
}
|
|
|
|
|
} else if (index & hfp_codec_mask) {
|
|
|
|
|
uint32_t i = index & ~hfp_codec_mask;
|
|
|
|
|
if (i == 0) {
|
|
|
|
|
*hfp_codec = HFP_AUDIO_CODEC_CVSD;
|
|
|
|
|
return DEVICE_PROFILE_HSP_HFP;
|
|
|
|
|
} else if (i == 1) {
|
|
|
|
|
*hfp_codec = HFP_AUDIO_CODEC_MSBC;
|
|
|
|
|
return DEVICE_PROFILE_HSP_HFP;
|
|
|
|
|
}
|
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-18 23:15:03 +02:00
|
|
|
static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, const struct a2dp_codec *codec, int hfp_codec)
|
2021-01-25 23:55:09 +02:00
|
|
|
{
|
2021-03-18 23:15:03 +02:00
|
|
|
uint32_t a2dp_codec_mask = 0x100;
|
|
|
|
|
uint32_t hfp_codec_mask = 0x200;
|
|
|
|
|
uint32_t i;
|
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;
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
if (profile == DEVICE_PROFILE_A2DP) {
|
|
|
|
|
if (codec == NULL)
|
|
|
|
|
return profile;
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < this->supported_codec_count; ++i) {
|
|
|
|
|
if (this->supported_codecs[i] == codec)
|
|
|
|
|
return a2dp_codec_mask | i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (profile == DEVICE_PROFILE_HSP_HFP) {
|
|
|
|
|
if (hfp_codec == 0)
|
|
|
|
|
return profile;
|
2021-02-06 18:47:07 +02:00
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
return hfp_codec_mask | ((hfp_codec == HFP_AUDIO_CODEC_MSBC) ? 1 : 0);
|
2021-01-25 23:55:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return SPA_ID_INVALID;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 21:30:15 +02:00
|
|
|
static void set_initial_profile(struct impl *this)
|
|
|
|
|
{
|
|
|
|
|
struct spa_bt_transport *t;
|
|
|
|
|
int i;
|
|
|
|
|
|
2021-03-14 14:06:50 +08:00
|
|
|
if (this->supported_codecs)
|
|
|
|
|
free(this->supported_codecs);
|
|
|
|
|
this->supported_codecs = spa_bt_device_get_supported_a2dp_codecs(
|
|
|
|
|
this->bt_dev, &this->supported_codec_count);
|
|
|
|
|
|
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
|
|
|
/* Prefer A2DP, then HFP, then null, but select AG if the device
|
|
|
|
|
appears not to have A2DP_SINK or any HEAD_UNIT profile */
|
2021-02-10 21:30:15 +02:00
|
|
|
|
|
|
|
|
for (i = SPA_BT_PROFILE_A2DP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) {
|
|
|
|
|
if (!(this->bt_dev->connected_profiles & i))
|
|
|
|
|
continue;
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
t = find_transport(this, i, NULL, 0);
|
2021-02-10 21:30:15 +02:00
|
|
|
if (t) {
|
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 = (i == SPA_BT_PROFILE_A2DP_SOURCE) ?
|
|
|
|
|
DEVICE_PROFILE_AG : DEVICE_PROFILE_A2DP;
|
2021-03-18 23:15:03 +02:00
|
|
|
this->selected_hfp_codec = 0;
|
2021-03-10 21:32:01 +02:00
|
|
|
|
|
|
|
|
/* Source devices don't have codec selection */
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)
|
|
|
|
|
this->selected_a2dp_codec = NULL;
|
|
|
|
|
else
|
|
|
|
|
this->selected_a2dp_codec = t->a2dp_codec;
|
2021-02-10 21:30:15 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (i = SPA_BT_PROFILE_HSP_HS; i <= SPA_BT_PROFILE_HFP_AG; i <<= 1) {
|
|
|
|
|
if (!(this->bt_dev->connected_profiles & i))
|
|
|
|
|
continue;
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
t = find_transport(this, i, NULL, 0);
|
2021-02-10 21:30:15 +02:00
|
|
|
if (t) {
|
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 = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ?
|
|
|
|
|
DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP;
|
2021-02-10 21:30:15 +02:00
|
|
|
this->selected_a2dp_codec = NULL;
|
2021-03-18 23:15:03 +02:00
|
|
|
|
|
|
|
|
/* Source devices don't have codec selection */
|
|
|
|
|
if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)
|
|
|
|
|
this->selected_hfp_codec = 0;
|
|
|
|
|
else
|
|
|
|
|
this->selected_hfp_codec = t->codec;
|
2021-02-10 21:30:15 +02:00
|
|
|
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
|
|
|
this->profile = DEVICE_PROFILE_OFF;
|
2021-02-10 21:30:15 +02:00
|
|
|
this->selected_a2dp_codec = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-17 11:08:26 +02:00
|
|
|
static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
|
2021-03-18 23:15:03 +02:00
|
|
|
uint32_t id, uint32_t index, uint32_t profile_index, const struct a2dp_codec *codec, int hfp_codec)
|
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 };
|
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";
|
|
|
|
|
desc = "Off";
|
|
|
|
|
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";
|
|
|
|
|
desc = "Audio Gateway (A2DP Source & HSP/HFP AG)";
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
} else if (profile == SPA_BT_PROFILE_A2DP_SINK) {
|
2021-01-25 23:55:09 +02:00
|
|
|
desc = "High Fidelity Playback (A2DP Sink%s%s)";
|
2021-01-10 20:53:59 +01:00
|
|
|
} else {
|
2021-01-25 23:55:09 +02:00
|
|
|
desc = "High Fidelity Duplex (A2DP Source/Sink%s%s)";
|
2021-01-10 20:53:59 +01:00
|
|
|
}
|
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-01-25 23:55:09 +02:00
|
|
|
if (codec != NULL) {
|
|
|
|
|
name_and_codec = spa_aprintf("%s-%s", name, codec->name);
|
|
|
|
|
desc_and_codec = spa_aprintf(desc, ", codec ", codec->description);
|
|
|
|
|
name = name_and_codec;
|
|
|
|
|
desc = desc_and_codec;
|
|
|
|
|
} else {
|
|
|
|
|
desc_and_codec = spa_aprintf(desc, "", "");
|
|
|
|
|
desc = desc_and_codec;
|
|
|
|
|
}
|
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_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
|
|
|
} else {
|
2021-03-18 23:15:03 +02:00
|
|
|
desc = "Headset Head Unit (HSP/HFP%s%s)";
|
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-18 23:15:03 +02:00
|
|
|
if (hfp_codec != 0) {
|
|
|
|
|
bool codec_ok = !(profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
|
|
|
|
|
if (spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1)
|
|
|
|
|
codec_ok = false;
|
|
|
|
|
if (!codec_ok) {
|
|
|
|
|
errno = -EINVAL;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
2021-03-20 00:11:12 +02:00
|
|
|
name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec));
|
|
|
|
|
desc_and_codec = spa_aprintf(desc, ", codec ", get_hfp_codec_description(hfp_codec));
|
2021-03-18 23:15:03 +02:00
|
|
|
name = name_and_codec;
|
|
|
|
|
desc = desc_and_codec;
|
|
|
|
|
} else {
|
|
|
|
|
desc_and_codec = spa_aprintf(desc, "", "");
|
|
|
|
|
desc = desc_and_codec;
|
|
|
|
|
}
|
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),
|
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]);
|
|
|
|
|
}
|
2021-01-25 23:55:09 +02:00
|
|
|
|
|
|
|
|
free(name_and_codec);
|
|
|
|
|
free(desc_and_codec);
|
|
|
|
|
|
2020-08-17 11:08:26 +02:00
|
|
|
return spa_pod_builder_pop(b, &f[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-07 18:10:22 +01:00
|
|
|
static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
|
|
|
|
|
uint32_t id, uint32_t port, uint32_t dev, uint32_t profile)
|
|
|
|
|
{
|
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;
|
2021-01-10 20:53:59 +01:00
|
|
|
const char *name_prefix, *description, *port_type;
|
|
|
|
|
enum spa_bt_form_factor ff;
|
2021-02-14 14:27:58 +02:00
|
|
|
const struct a2dp_codec *codec;
|
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;
|
|
|
|
|
int hfp_codec;
|
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";
|
|
|
|
|
description = "Headset";
|
|
|
|
|
port_type = "headset";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HANDSFREE:
|
|
|
|
|
name_prefix = "handsfree";
|
|
|
|
|
description = "Handsfree";
|
|
|
|
|
port_type = "handsfree";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_MICROPHONE:
|
|
|
|
|
name_prefix = "microphone";
|
|
|
|
|
description = "Microphone";
|
|
|
|
|
port_type = "mic";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_SPEAKER:
|
|
|
|
|
name_prefix = "speaker";
|
|
|
|
|
description = "Speaker";
|
|
|
|
|
port_type = "speaker";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HEADPHONE:
|
|
|
|
|
name_prefix = "headphone";
|
|
|
|
|
description = "Headphone";
|
|
|
|
|
port_type = "headphones";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_PORTABLE:
|
|
|
|
|
name_prefix = "portable";
|
|
|
|
|
description = "Portable";
|
|
|
|
|
port_type = "portable";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_CAR:
|
|
|
|
|
name_prefix = "car";
|
|
|
|
|
description = "Car";
|
|
|
|
|
port_type = "car";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_HIFI:
|
|
|
|
|
name_prefix = "hifi";
|
|
|
|
|
description = "HiFi";
|
|
|
|
|
port_type = "hifi";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_PHONE:
|
|
|
|
|
name_prefix = "phone";
|
|
|
|
|
description = "Phone";
|
|
|
|
|
port_type = "phone";
|
|
|
|
|
break;
|
|
|
|
|
case SPA_BT_FORM_FACTOR_UNKNOWN:
|
|
|
|
|
default:
|
|
|
|
|
name_prefix = "bluetooth";
|
|
|
|
|
description = "Bluetooth";
|
|
|
|
|
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);
|
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);
|
2021-01-07 18:10:22 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
2021-03-19 12:46:05 +01:00
|
|
|
errno = EINVAL;
|
2021-01-07 18:10:22 +01:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-07 00:53:04 +02:00
|
|
|
if (dev != SPA_ID_INVALID && !(profile_direction_mask(this, this->profile) & (1 << direction)))
|
2021-01-07 18:10:22 +01:00
|
|
|
return NULL;
|
|
|
|
|
|
2021-01-22 17:35:27 +01:00
|
|
|
mask = 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
|
|
|
for (i = 1; i < 4; i++)
|
2021-01-22 17:35:27 +01:00
|
|
|
mask |= profile_direction_mask(this, i);
|
|
|
|
|
if ((mask & (1 << direction)) == 0)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
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),
|
2021-03-07 00:53:04 +02:00
|
|
|
SPA_PARAM_ROUTE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes),
|
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-03-20 14:20:46 +02:00
|
|
|
for (i = 1; (j = get_profile_from_index(this, i, &next, &codec, &hfp_codec)) != SPA_ID_INVALID; i = next) {
|
|
|
|
|
struct spa_pod_builder b2 = { 0 };
|
|
|
|
|
uint8_t buffer[1024];
|
|
|
|
|
struct spa_pod *param;
|
|
|
|
|
|
|
|
|
|
if (!(profile_direction_mask(this, j) & (1 << direction)))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
/* Check the profile actually exists */
|
|
|
|
|
spa_pod_builder_init(&b2, buffer, sizeof(buffer));
|
|
|
|
|
param = build_profile(this, &b2, 0, i, j, codec, hfp_codec);
|
|
|
|
|
if (param == NULL)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_int(b, i);
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
spa_pod_builder_pop(b, &f[1]);
|
|
|
|
|
|
|
|
|
|
if (dev != SPA_ID_INVALID) {
|
|
|
|
|
struct node *node = &this->nodes[dev];
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PROP_channelVolumes, 0);
|
|
|
|
|
spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
|
|
|
|
|
node->n_channels, node->volumes);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
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 (this->profile == DEVICE_PROFILE_A2DP && 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]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
|
|
|
|
|
spa_pod_builder_push_array(b, &f[1]);
|
|
|
|
|
/* port and device indexes are the same, 0=source, 1=sink */
|
|
|
|
|
spa_pod_builder_int(b, port);
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
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 };
|
|
|
|
|
uint8_t buffer[1024];
|
|
|
|
|
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;
|
|
|
|
|
const struct a2dp_codec *codec;
|
2021-03-18 23:15:03 +02:00
|
|
|
int hfp_codec;
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
profile = get_profile_from_index(this, result.index, &result.next, &codec, &hfp_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:
|
|
|
|
|
case DEVICE_PROFILE_HSP_HFP:
|
2021-03-18 23:15:03 +02:00
|
|
|
param = build_profile(this, &b, id, result.index, profile, codec, hfp_codec);
|
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-18 23:15:03 +02:00
|
|
|
index = get_index_from_profile(this, this->profile, this->selected_a2dp_codec, this->selected_hfp_codec);
|
|
|
|
|
param = build_profile(this, &b, id, index, this->profile, this->selected_a2dp_codec, this->selected_hfp_codec);
|
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) {
|
|
|
|
|
case 0: case 1:
|
|
|
|
|
param = build_route(this, &b, id, result.index,
|
|
|
|
|
SPA_ID_INVALID, 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) {
|
|
|
|
|
case 0: case 1:
|
|
|
|
|
param = build_route(this, &b, id, result.index,
|
|
|
|
|
result.index, this->profile);
|
|
|
|
|
if (param == NULL)
|
|
|
|
|
goto next;
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
struct spa_event *event;
|
|
|
|
|
uint8_t buffer[4096];
|
|
|
|
|
struct spa_pod_builder b = { 0 };
|
|
|
|
|
struct spa_pod_frame f[1];
|
2021-02-18 17:47:22 +01:00
|
|
|
uint32_t i;
|
2021-02-25 19:55:04 +02:00
|
|
|
int changed = 0;
|
2021-02-18 17:47:22 +01:00
|
|
|
|
|
|
|
|
if (n_volumes == 0)
|
|
|
|
|
return -EINVAL;
|
2021-01-07 18:10:22 +01:00
|
|
|
|
|
|
|
|
spa_log_info(this->log, "node %p volume %f", node, volumes[0]);
|
|
|
|
|
|
2021-02-25 19:55:04 +02:00
|
|
|
for (i = 0; i < node->n_channels; i++) {
|
|
|
|
|
if (node->volumes[i] != volumes[i % n_volumes])
|
|
|
|
|
++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
|
|
|
|
|
|
|
|
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,
|
2021-01-25 15:21:23 +01:00
|
|
|
SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
|
2021-02-18 19:25:04 +01:00
|
|
|
SPA_TYPE_Float, node->n_channels, node->volumes),
|
2021-01-25 15:21:23 +01:00
|
|
|
SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
|
|
|
|
|
SPA_TYPE_Id, node->n_channels, node->channels));
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
spa_log_info(this->log, "node %p mute %d", node, 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-01-07 18:10:22 +01:00
|
|
|
SPA_PROP_mute, SPA_POD_Bool(mute));
|
|
|
|
|
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
|
|
|
|
|
|
|
|
spa_log_info(this->log, "node %p latency offset %"PRIi64" nsec", node, 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;
|
|
|
|
|
}
|
|
|
|
|
|
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-03-18 23:15:03 +02:00
|
|
|
uint32_t id, next;
|
2021-01-25 23:55:09 +02:00
|
|
|
uint32_t profile;
|
|
|
|
|
const struct a2dp_codec *codec;
|
2021-03-18 23:15:03 +02:00
|
|
|
int hfp_codec;
|
2020-01-03 13:01:54 +01:00
|
|
|
|
|
|
|
|
if ((res = spa_pod_parse_object(param,
|
|
|
|
|
SPA_TYPE_OBJECT_ParamProfile, NULL,
|
|
|
|
|
SPA_PARAM_PROFILE_index, SPA_POD_Int(&id))) < 0) {
|
|
|
|
|
spa_log_warn(this->log, "can't parse profile");
|
|
|
|
|
spa_debug_pod(0, NULL, param);
|
|
|
|
|
return res;
|
|
|
|
|
}
|
2021-01-25 23:55:09 +02:00
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
profile = get_profile_from_index(this, id, &next, &codec, &hfp_codec);
|
2021-01-25 23:55:09 +02:00
|
|
|
if (profile == SPA_ID_INVALID)
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
2021-03-18 23:15:03 +02:00
|
|
|
spa_log_debug(this->log, NAME": setting profile %d codec:%s hfp-codec:%d", profile,
|
|
|
|
|
codec ? codec->name : "<null>", hfp_codec);
|
|
|
|
|
set_profile(this, profile, codec, hfp_codec);
|
2020-01-03 13:01:54 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2021-01-07 18:10:22 +01:00
|
|
|
case SPA_PARAM_Route:
|
|
|
|
|
{
|
|
|
|
|
uint32_t id, device;
|
|
|
|
|
struct spa_pod *props = NULL;
|
|
|
|
|
struct node *node;
|
|
|
|
|
|
|
|
|
|
if ((res = spa_pod_parse_object(param,
|
|
|
|
|
SPA_TYPE_OBJECT_ParamRoute, NULL,
|
|
|
|
|
SPA_PARAM_ROUTE_index, SPA_POD_Int(&id),
|
|
|
|
|
SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
|
|
|
|
|
SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props))) < 0) {
|
|
|
|
|
spa_log_warn(this->log, "can't parse route");
|
|
|
|
|
spa_debug_pod(0, NULL, param);
|
|
|
|
|
return res;
|
|
|
|
|
}
|
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 (device > 1 || !this->nodes[device].active)
|
2021-01-07 18:10:22 +01:00
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
|
|
node = &this->nodes[device];
|
|
|
|
|
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;
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
2019-12-19 13:15:10 +01:00
|
|
|
if (strcmp(type, SPA_TYPE_INTERFACE_Device) == 0)
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
this->bt_dev->settings = filter_bluez_device_setting(this, info);
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
this->info.params = this->params;
|
|
|
|
|
this->info.n_params = 4;
|
|
|
|
|
|
2021-01-25 19:57:45 +02:00
|
|
|
spa_bt_device_add_listener(this->bt_dev, &this->bt_dev_listener, &bt_dev_events, 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,
|
|
|
|
|
};
|