diff options
Diffstat (limited to '')
-rw-r--r-- | spa/plugins/bluez5/player.c | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/spa/plugins/bluez5/player.c b/spa/plugins/bluez5/player.c new file mode 100644 index 0000000..a77ca25 --- /dev/null +++ b/spa/plugins/bluez5/player.c @@ -0,0 +1,428 @@ +/* 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 = userdata; + 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; + int res = 0; + + 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)) + res = -EIO; + + dbus_message_unref(m); + + return res; +} + +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); + + 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; +} |