bluez5: add a dummy AVRCP player as a workaround

Some devices (Bose Mini Soundlink II, Air 1 Plus, ...) don't enable
AVRCP volume control, or fail to enable it before a hardware button is
pressed.  However, these devices appear to enable it, if an AVRCP player
is present.

As a workaround, register a dummy AVRCP player for each adapter. It only
displays the current transport acquisition state as playing/stopped, but
just its presence appears to be enough to make devices behave.

Multiple AVRCP players interfere with each other, as BlueZ uses the one
registered earliest as the default player. So add also a config option
for disabling this. (It's not common to have mpris-proxy etc. running,
so defaulting to true should be OK.)

See pipewire#1157
This commit is contained in:
Pauli Virtanen 2021-10-09 19:11:51 +03:00
parent 336caa9db3
commit 4b831021fb
6 changed files with 514 additions and 0 deletions

View file

@ -51,6 +51,7 @@
#include <spa/utils/json.h>
#include "codec-loader.h"
#include "player.h"
#include "defs.h"
static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5");
@ -103,6 +104,7 @@ struct spa_bt_monitor {
struct spa_dict enabled_codecs;
unsigned int connection_info_supported:1;
unsigned int dummy_avrcp_player:1;
struct spa_bt_quirks *quirks;
@ -670,6 +672,15 @@ static int adapter_update_props(struct spa_bt_adapter *adapter,
return 0;
}
static void adapter_register_player(struct spa_bt_adapter *adapter)
{
if (adapter->player_registered || !adapter->monitor->dummy_avrcp_player)
return;
if (spa_bt_player_register(adapter->dummy_player, adapter->path) == 0)
adapter->player_registered = true;
}
static int adapter_init_bus_type(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d)
{
char path[1024], buf[1024];
@ -735,6 +746,12 @@ static struct spa_bt_adapter *adapter_create(struct spa_bt_monitor *monitor, con
if (d == NULL)
return NULL;
d->dummy_player = spa_bt_player_new(monitor->conn, monitor->log);
if (d->dummy_player == NULL) {
free(d);
return NULL;
}
d->monitor = monitor;
d->path = strdup(path);
@ -751,6 +768,8 @@ static void adapter_free(struct spa_bt_adapter *adapter)
struct spa_bt_monitor *monitor = adapter->monitor;
spa_log_debug(monitor->log, "%p", adapter);
spa_bt_player_destroy(adapter->dummy_player);
spa_list_remove(&adapter->link);
free(adapter->alias);
free(adapter->name);
@ -1735,6 +1754,8 @@ void spa_bt_transport_free(struct spa_bt_transport *transport)
spa_bt_transport_destroy(transport);
if (transport->fd >= 0) {
spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
shutdown(transport->fd, SHUT_RDWR);
close(transport->fd);
transport->fd = -1;
@ -2247,6 +2268,8 @@ static int transport_acquire(void *data, bool optional)
spa_log_debug(monitor->log, "transport %p: %s %s, fd %d MTU %d:%d", transport, method,
transport->path, transport->fd, transport->read_mtu, transport->write_mtu);
spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_PLAYING);
transport_sync_volume(transport);
finish:
@ -2265,6 +2288,8 @@ static int transport_release(void *data)
spa_log_debug(monitor->log, "transport %p: Release %s",
transport, transport->path);
spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
close(transport->fd);
transport->fd = -1;
@ -3412,6 +3437,7 @@ static void interface_added(struct spa_bt_monitor *monitor,
}
adapter_update_props(a, props_iter, NULL);
adapter_register_application(a);
adapter_register_player(a);
}
else if (spa_streq(interface_name, BLUEZ_PROFILE_MANAGER_INTERFACE)) {
if (monitor->backends[BACKEND_NATIVE])
@ -4165,6 +4191,11 @@ impl_init(const struct spa_handle_factory *factory,
else if (spa_streq(str, "native"))
this->backend_selection = BACKEND_NATIVE;
}
if ((str = spa_dict_lookup(info, "bluez5.dummy-avrcp-player")) != NULL)
this->dummy_avrcp_player = spa_atob(str);
else
this->dummy_avrcp_player = true;
}
register_media_application(this);

View file

@ -314,10 +314,12 @@ static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) {
struct spa_bt_monitor;
struct spa_bt_backend;
struct spa_bt_player;
struct spa_bt_adapter {
struct spa_list link;
struct spa_bt_monitor *monitor;
struct spa_bt_player *dummy_player;
char *path;
char *alias;
char *address;
@ -332,6 +334,7 @@ struct spa_bt_adapter {
int powered;
unsigned int endpoints_registered:1;
unsigned int application_registered:1;
unsigned int player_registered:1;
unsigned int has_battery_provider;
unsigned int battery_provider_unavailable;
};

View file

@ -30,6 +30,7 @@ bluez5_sources = [
'sco-source.c',
'sco-io.c',
'quirks.c',
'player.c',
'bluez5-device.c',
'bluez5-dbus.c'
]

424
spa/plugins/bluez5/player.c Normal file
View file

@ -0,0 +1,424 @@
/* Spa Bluez5 AVRCP Player
*
* Copyright © 2021 Pauli Virtanen
*
* 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 <errno.h>
#include <stdbool.h>
#include <dbus/dbus.h>
#include <spa/utils/string.h>
#include "defs.h"
#include "player.h"
#define PLAYER_OBJECT_PATH_BASE "/media_player"
#define PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player"
#define PLAYER_INTROSPECT_XML \
DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
"<node>" \
" <interface name='" PLAYER_INTERFACE "'>" \
" <property name='PlaybackStatus' type='s' access='read'/>" \
" </interface>" \
" <interface name='" DBUS_INTERFACE_PROPERTIES "'>" \
" <method name='Get'>" \
" <arg name='interface' type='s' direction='in' />" \
" <arg name='name' type='s' direction='in' />" \
" <arg name='value' type='v' direction='out' />" \
" </method>" \
" <method name='Set'>" \
" <arg name='interface' type='s' direction='in' />" \
" <arg name='name' type='s' direction='in' />" \
" <arg name='value' type='v' direction='in' />" \
" </method>" \
" <method name='GetAll'>" \
" <arg name='interface' type='s' direction='in' />" \
" <arg name='properties' type='a{sv}' direction='out' />" \
" </method>" \
" <signal name='PropertiesChanged'>" \
" <arg name='interface' type='s' />" \
" <arg name='changed_properties' type='a{sv}' />" \
" <arg name='invalidated_properties' type='as' />" \
" </signal>" \
" </interface>" \
" <interface name='" DBUS_INTERFACE_INTROSPECTABLE "'>" \
" <method name='Introspect'>" \
" <arg name='xml' type='s' direction='out'/>" \
" </method>" \
" </interface>" \
"</node>"
static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.player");
#undef SPA_LOG_TOPIC_DEFAULT
#define SPA_LOG_TOPIC_DEFAULT &log_topic
#define MAX_PROPERTIES 1
struct impl {
struct spa_bt_player this;
DBusConnection *conn;
char *path;
struct spa_log *log;
struct spa_dict_item properties_items[MAX_PROPERTIES];
struct spa_dict properties;
unsigned int playing_count;
};
static size_t instance_counter = 0;
static DBusMessage *properties_get(struct impl *impl, DBusMessage *m)
{
const char *iface, *name;
size_t j;
if (!dbus_message_get_args(m, NULL,
DBUS_TYPE_STRING, &iface,
DBUS_TYPE_STRING, &name,
DBUS_TYPE_INVALID))
return NULL;
if (!spa_streq(iface, PLAYER_INTERFACE))
return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
"No such interface");
for (j = 0; j < impl->properties.n_items; ++j) {
const struct spa_dict_item *item = &impl->properties.items[j];
if (spa_streq(item->key, name)) {
DBusMessage *r;
DBusMessageIter i, v;
r = dbus_message_new_method_return(m);
if (r == NULL)
return NULL;
dbus_message_iter_init_append(r, &i);
dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
"s", &v);
dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
&item->value);
dbus_message_iter_close_container(&i, &v);
return r;
}
}
return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
"No such property");
}
static void append_properties(struct impl *impl, DBusMessageIter *i)
{
DBusMessageIter d, e, v;
size_t j;
dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY,
DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d);
for (j = 0; j < impl->properties.n_items; ++j) {
const struct spa_dict_item *item = &impl->properties.items[j];
spa_log_debug(impl->log, "player %s: %s=%s", impl->path,
item->key, item->value);
dbus_message_iter_open_container(&d, DBUS_TYPE_DICT_ENTRY, NULL, &e);
dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &item->key);
dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "s", &v);
dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, &item->value);
dbus_message_iter_close_container(&e, &v);
dbus_message_iter_close_container(&d, &e);
}
dbus_message_iter_close_container(i, &d);
}
static DBusMessage *properties_get_all(struct impl *impl, DBusMessage *m)
{
const char *iface, *name;
DBusMessage *r;
DBusMessageIter i;
if (!dbus_message_get_args(m, NULL,
DBUS_TYPE_STRING, &iface,
DBUS_TYPE_STRING, &name,
DBUS_TYPE_INVALID))
return NULL;
if (!spa_streq(iface, PLAYER_INTERFACE))
return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
"No such interface");
r = dbus_message_new_method_return(m);
if (r == NULL)
return NULL;
dbus_message_iter_init_append(r, &i);
append_properties(impl, &i);
return r;
}
static DBusMessage *properties_set(struct impl *impl, DBusMessage *m)
{
return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
"Property not writable");
}
static DBusMessage *introspect(struct impl *impl, DBusMessage *m)
{
const char *xml = PLAYER_INTROSPECT_XML;
DBusMessage *r;
if ((r = dbus_message_new_method_return(m)) == NULL)
return NULL;
if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
return NULL;
return r;
}
static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
{
struct impl *impl = impl;
DBusMessage *r;
if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
r = introspect(impl, m);
} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
r = properties_get(impl, m);
} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
r = properties_get_all(impl, m);
} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
r = properties_set(impl, m);
} else {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (r == NULL)
return DBUS_HANDLER_RESULT_NEED_MEMORY;
if (!dbus_connection_send(impl->conn, r, NULL)) {
dbus_message_unref(r);
return DBUS_HANDLER_RESULT_NEED_MEMORY;
}
dbus_message_unref(r);
return DBUS_HANDLER_RESULT_HANDLED;
}
static int send_update_signal(struct impl *impl)
{
DBusMessage *m;
const char *iface = PLAYER_INTERFACE;
DBusMessageIter i, a;
m = dbus_message_new_signal(impl->path, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged");
if (m == NULL)
return -ENOMEM;
dbus_message_iter_init_append(m, &i);
dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &iface);
append_properties(impl, &i);
dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
DBUS_TYPE_STRING_AS_STRING, &a);
dbus_message_iter_close_container(&i, &a);
dbus_connection_send(impl->conn, m, NULL);
dbus_message_unref(m);
return 0;
}
static void update_properties(struct impl *impl, bool send_signal)
{
int nitems = 0;
switch (impl->this.state) {
case SPA_BT_PLAYER_PLAYING:
impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Playing");
break;
case SPA_BT_PLAYER_STOPPED:
impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Stopped");
break;
}
impl->properties = SPA_DICT_INIT(impl->properties_items, nitems);
if (!send_signal)
return;
send_update_signal(impl);
}
struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log)
{
struct impl *impl;
const DBusObjectPathVTable vtable = {
.message_function = player_handler,
};
spa_log_topic_init(log, &log_topic);
impl = calloc(1, sizeof(struct impl));
if (impl == NULL)
return NULL;
impl->this.state = SPA_BT_PLAYER_STOPPED;
impl->conn = dbus_connection;
impl->log = log;
impl->path = spa_aprintf("%s%zu", PLAYER_OBJECT_PATH_BASE, instance_counter++);
if (impl->path == NULL) {
free(impl);
return NULL;
}
dbus_connection_ref(impl->conn);
update_properties(impl, false);
if (!dbus_connection_register_object_path(impl->conn, impl->path, &vtable, impl)) {
spa_bt_player_destroy(&impl->this);
errno = EIO;
return NULL;
}
return &impl->this;
}
void spa_bt_player_destroy(struct spa_bt_player *player)
{
struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
/*
* We unregister only the object path, but don't unregister it from
* BlueZ, to avoid hanging on BlueZ DBus activation. The assumption is
* that the DBus connection is terminated immediately after.
*/
dbus_connection_unregister_object_path(impl->conn, impl->path);
dbus_connection_unref(impl->conn);
free(impl);
}
int spa_bt_player_set_state(struct spa_bt_player *player, enum spa_bt_player_state state)
{
struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
switch (state) {
case SPA_BT_PLAYER_PLAYING:
if (impl->playing_count++ > 0)
return 0;
break;
case SPA_BT_PLAYER_STOPPED:
if (impl->playing_count == 0)
return -EINVAL;
if (--impl->playing_count > 0)
return 0;
break;
default:
return -EINVAL;
}
impl->this.state = state;
update_properties(impl, true);
return 0;
}
int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path)
{
struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
DBusError err;
DBusMessageIter i;
DBusMessage *m, *r;
int res = 0;
spa_log_debug(impl->log, "RegisterPlayer() for dummy AVRCP player %s for %s",
impl->path, adapter_path);
m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
BLUEZ_MEDIA_INTERFACE, "RegisterPlayer");
if (m == NULL)
return -EIO;
dbus_message_iter_init_append(m, &i);
dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
append_properties(impl, &i);
dbus_error_init(&err);
r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
dbus_message_unref(m);
if (r == NULL) {
spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message);
dbus_error_free(&err);
return -EIO;
}
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
spa_log_error(impl->log, "RegisterPlayer() failed");
res = -EIO;
}
dbus_message_unref(r);
return res;
}
int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
{
struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
DBusError err;
DBusMessageIter i;
DBusMessage *m, *r;
int res = 0;
spa_log_debug(impl->log, "UnregisterPlayer() for dummy AVRCP player %s for %s",
impl->path, adapter_path);
m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
BLUEZ_MEDIA_INTERFACE, "UnregisterPlayer");
if (m == NULL)
return -EIO;
dbus_message_iter_init_append(m, &i);
dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
dbus_error_init(&err);
r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
dbus_message_unref(m);
if (r == NULL) {
spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message);
dbus_error_free(&err);
return -EIO;
}
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
spa_log_error(impl->log, "UnregisterPlayer() failed");
res = -EIO;
}
dbus_message_unref(r);
return res;
}

View file

@ -0,0 +1,51 @@
/* Spa Bluez5 AVRCP Player
*
* Copyright © 2021 Pauli Virtanen
*
* 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.
*/
#ifndef SPA_BLUEZ5_PLAYER_H_
#define SPA_BLUEZ5_PLAYER_H_
enum spa_bt_player_state {
SPA_BT_PLAYER_STOPPED,
SPA_BT_PLAYER_PLAYING,
};
/**
* Dummy AVRCP player.
*
* Some headsets require an AVRCP player to be present, before their
* AVRCP volume synchronization works. To work around this, we
* register a dummy player that does nothing.
*/
struct spa_bt_player {
enum spa_bt_player_state state;
};
struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log);
void spa_bt_player_destroy(struct spa_bt_player *player);
int spa_bt_player_set_state(struct spa_bt_player *player,
enum spa_bt_player_state state);
int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path);
int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path);
#endif

View file

@ -37,6 +37,10 @@ properties = {
# Properties for the A2DP codec configuration
#bluez5.default.rate = 48000
#bluez5.default.channels = 2
# Register dummy AVRCP player, required for AVRCP volume function.
# Disable if you are running mpris-proxy or equivalent.
#bluez5.dummy-avrcp-player = true
}
rules = [