2023-02-08 18:12:00 +01:00
|
|
|
/* Spa Bluez5 AVRCP Player */
|
|
|
|
|
/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */
|
|
|
|
|
/* SPDX-License-Identifier: MIT */
|
2021-10-09 19:11:51 +03:00
|
|
|
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <stdbool.h>
|
|
|
|
|
#include <dbus/dbus.h>
|
|
|
|
|
|
|
|
|
|
#include <spa/utils/string.h>
|
2023-12-30 01:08:31 +01:00
|
|
|
#include <spa-private/dbus-helpers.h>
|
2021-10-09 19:11:51 +03:00
|
|
|
|
|
|
|
|
#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>"
|
|
|
|
|
|
2023-12-23 21:07:45 +02:00
|
|
|
SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.player");
|
2021-10-09 19:11:51 +03:00
|
|
|
#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;
|
2023-07-11 19:24:46 +02:00
|
|
|
spa_autoptr(DBusMessage) r = NULL;
|
2021-10-09 19:11:51 +03:00
|
|
|
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;
|
2023-07-11 19:24:46 +02:00
|
|
|
return spa_steal_ptr(r);
|
2021-10-09 19:11:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
|
|
|
|
|
{
|
2021-10-13 19:10:21 +03:00
|
|
|
struct impl *impl = userdata;
|
2023-07-11 19:24:46 +02:00
|
|
|
spa_autoptr(DBusMessage) r = NULL;
|
2021-10-09 19:11:51 +03:00
|
|
|
|
|
|
|
|
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;
|
2023-07-11 19:24:46 +02:00
|
|
|
if (!dbus_connection_send(impl->conn, r, NULL))
|
2021-10-09 19:11:51 +03:00
|
|
|
return DBUS_HANDLER_RESULT_NEED_MEMORY;
|
|
|
|
|
return DBUS_HANDLER_RESULT_HANDLED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int send_update_signal(struct impl *impl)
|
|
|
|
|
{
|
2023-07-11 19:24:46 +02:00
|
|
|
spa_autoptr(DBusMessage) m = NULL;
|
2021-10-09 19:11:51 +03:00
|
|
|
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);
|
|
|
|
|
|
2021-10-13 19:10:21 +03:00
|
|
|
if (!dbus_connection_send(impl->conn, m, NULL))
|
2023-07-11 19:24:46 +02:00
|
|
|
return -EIO;
|
2021-10-09 19:11:51 +03:00
|
|
|
|
2023-07-11 19:24:46 +02:00
|
|
|
return 0;
|
2021-10-09 19:11:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2022-01-03 10:41:00 +01:00
|
|
|
free(impl->path);
|
2021-10-09 19:11:51 +03:00
|
|
|
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);
|
|
|
|
|
|
2023-07-11 20:44:23 +02:00
|
|
|
spa_auto(DBusError) err = DBUS_ERROR_INIT;
|
2021-10-09 19:11:51 +03:00
|
|
|
DBusMessageIter i;
|
2023-07-11 19:24:46 +02:00
|
|
|
spa_autoptr(DBusMessage) m = NULL, r = NULL;
|
2021-10-09 19:11:51 +03:00
|
|
|
|
|
|
|
|
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");
|
2023-07-11 19:24:46 +02:00
|
|
|
return -EIO;
|
2021-10-09 19:11:51 +03:00
|
|
|
}
|
|
|
|
|
|
2023-07-11 19:24:46 +02:00
|
|
|
return 0;
|
2021-10-09 19:11:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
|
|
|
|
|
|
2023-07-11 20:44:23 +02:00
|
|
|
spa_auto(DBusError) err = DBUS_ERROR_INIT;
|
2021-10-09 19:11:51 +03:00
|
|
|
DBusMessageIter i;
|
2023-07-11 19:24:46 +02:00
|
|
|
spa_autoptr(DBusMessage) m = NULL, r = NULL;
|
2021-10-09 19:11:51 +03:00
|
|
|
|
|
|
|
|
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");
|
2023-07-11 19:24:46 +02:00
|
|
|
return -EIO;
|
2021-10-09 19:11:51 +03:00
|
|
|
}
|
|
|
|
|
|
2023-07-11 19:24:46 +02:00
|
|
|
return 0;
|
2021-10-09 19:11:51 +03:00
|
|
|
}
|