pipewire/spa/plugins/bluez5/player.c
Barnabás Pőcze 4aa2e44796 spa: move dbus helpers out of bluez plugin
The file is moved into a new "include-private" directory. This is done
because otherwise adjustments would have to be made to the list of installed
headers, the way include tests currently work and which files are
used for generating documentation.
2024-02-05 13:03:20 +00:00

389 lines
11 KiB
C

/* Spa Bluez5 AVRCP Player */
/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */
/* SPDX-License-Identifier: MIT */
#include <errno.h>
#include <stdbool.h>
#include <dbus/dbus.h>
#include <spa/utils/string.h>
#include <spa-private/dbus-helpers.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>"
SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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;
spa_autoptr(DBusMessage) r = NULL;
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 spa_steal_ptr(r);
}
static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
{
struct impl *impl = userdata;
spa_autoptr(DBusMessage) r = NULL;
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))
return DBUS_HANDLER_RESULT_NEED_MEMORY;
return DBUS_HANDLER_RESULT_HANDLED;
}
static int send_update_signal(struct impl *impl)
{
spa_autoptr(DBusMessage) m = NULL;
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);
if (!dbus_connection_send(impl->conn, m, NULL))
return -EIO;
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->path);
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);
spa_auto(DBusError) err = DBUS_ERROR_INIT;
DBusMessageIter i;
spa_autoptr(DBusMessage) m = NULL, r = NULL;
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);
r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
if (r == NULL) {
spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message);
return -EIO;
}
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
spa_log_error(impl->log, "RegisterPlayer() failed");
return -EIO;
}
return 0;
}
int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
{
struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
spa_auto(DBusError) err = DBUS_ERROR_INIT;
DBusMessageIter i;
spa_autoptr(DBusMessage) m = NULL, r = NULL;
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);
r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
if (r == NULL) {
spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message);
return -EIO;
}
if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
spa_log_error(impl->log, "UnregisterPlayer() failed");
return -EIO;
}
return 0;
}