diff --git a/spa/include/spa/utils/string.h b/spa/include/spa/utils/string.h index 11a222292..828d95751 100644 --- a/spa/include/spa/utils/string.h +++ b/spa/include/spa/utils/string.h @@ -307,6 +307,40 @@ static inline bool spa_atod(const char *str, double *val) return true; } +/** + * Find \a needle in the \a haystack array of \a n strings. + * + * \param haystack an array of \a n strings + * \param n number of strings in \a haystack + * \param cmp a three-way comparator + * \param index pointer to save the index into if \a needle is found, + * may be NULL; it is not modified if \a needle is not found + * + * \return true if \a needle is found, false otherwise + * + * \since 0.3.31 + */ +static inline bool spa_find_str(const char * const haystack[], size_t n, + const char *needle, + int (*cmp)(const char *, const char *), + size_t *index) +{ + for (size_t i = 0; i < n; i++) { + if (cmp(needle, haystack[i]) != 0) + continue; + + if (index != NULL) + *index = i; + + return true; + } + + return false; +} + +#define spa_find_str_arr(haystack, needle, cmp, index) \ + (spa_find_str((haystack), SPA_N_ELEMENTS(haystack), (needle), (cmp), (index))) + /** * \} */ diff --git a/spa/plugins/bluez5/a2dp-source.c b/spa/plugins/bluez5/a2dp-source.c index 46ad23e46..f9ec1688a 100644 --- a/spa/plugins/bluez5/a2dp-source.c +++ b/spa/plugins/bluez5/a2dp-source.c @@ -123,6 +123,9 @@ struct impl { struct spa_bt_transport *transport; struct spa_hook transport_listener; + const struct spa_bt_player *player; + struct spa_hook device_listener; + struct port port; unsigned int started:1; @@ -762,24 +765,44 @@ static int impl_node_send_command(void *object, const struct spa_command *comman static void emit_node_info(struct impl *this, bool full) { - char latency[64] = SPA_STRINGIFY(MIN_LATENCY)"/48000"; - uint64_t old = full ? this->info.change_mask : 0; - - struct spa_dict_item node_info_items[] = { - { SPA_KEY_DEVICE_API, "bluez5" }, - { SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" }, - { SPA_KEY_NODE_LATENCY, latency }, - { "media.name", ((this->transport && this->transport->device->name) ? - this->transport->device->name : "A2DP") }, - { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" }, - }; + const uint64_t old = full ? this->info.change_mask : 0; if (full) this->info.change_mask = this->info_all; + if (this->info.change_mask) { + const char * const device_name = this->transport && this->transport->device->name + ? this->transport->device->name + : "A2DP"; + const char * const artist = this->player ? this->player->track.artist : NULL; + const char * const title = this->player ? this->player->track.title : NULL; + const char * const album = this->player ? this->player->track.album : NULL; + const char * const software = this->player ? this->player->name : NULL; + char latency[64] = SPA_STRINGIFY(MIN_LATENCY) "/48000"; + char media_name[256]; + + const struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" }, + { SPA_KEY_NODE_LATENCY, latency }, + { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" }, + { "media.name", media_name }, + { "media.artist", artist }, + { "media.title", title }, + { "media.software", software }, + }; + + snprintf(media_name, sizeof(media_name), "[%s] %s: %s (%s)", + device_name, + artist ? artist : "?", + title ? title : "?", + album ? album : "?" + ); + if (this->transport && this->port.have_format) snprintf(latency, sizeof(latency), "%d/%d", (int)this->props.min_latency, (int)this->port.current_format.info.raw.rate); + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); spa_node_emit_info(&this->hooks, &this->info); this->info.change_mask = old; @@ -1232,6 +1255,16 @@ static const struct spa_node_methods impl_node = { .process = impl_node_process, }; +static void impl_remove_hooks(struct impl *this) +{ + if (this->transport) { + spa_hook_remove(&this->transport_listener); + + if (this->transport->device) + spa_hook_remove(&this->device_listener); + } +} + static int do_transport_destroy(struct spa_loop *loop, bool async, uint32_t seq, @@ -1240,8 +1273,13 @@ static int do_transport_destroy(struct spa_loop *loop, void *user_data) { struct impl *this = user_data; + + impl_remove_hooks(this); + this->transport = NULL; this->transport_acquired = false; + this->player = NULL; + return 0; } @@ -1257,6 +1295,76 @@ static const struct spa_bt_transport_events transport_events = { .destroy = transport_destroy, }; +static bool try_set_player(struct impl *this, const struct spa_bt_player *player) +{ + if (player->type != SPA_BT_PLAYER_TYPE_AUDIO && player->type != SPA_BT_PLAYER_TYPE_AUDIO_BROADCASTING) + return false; + + spa_assert(this->transport->device == player->device); + this->player = player; + + return true; +} + +static void try_find_player(struct impl *this) +{ + const struct spa_bt_player *p; + + if (this->transport == NULL) + return; + + if (this->transport->device == NULL) + return; + + spa_list_for_each(p, &this->transport->device->player_list, device_link) + if (try_set_player(this, p)) + break; +} + +static void player_added(void *data, struct spa_bt_player *player) +{ + struct impl * const this = data; + + if (this->player != NULL) + return; + + if (try_set_player(this, player)) + emit_node_info(this, true); +} + +static void player_removed(void *data, struct spa_bt_player *player) +{ + struct impl * const this = data; + + if (this->player != player) + return; + + this->player = NULL; + + try_find_player(this); + emit_node_info(this, true); +} + +static void player_changed(void *data, struct spa_bt_player *player) +{ + struct impl * const this = data; + + if (this->player == NULL) + try_set_player(this, player); + + if (this->player != player) + return; + + emit_node_info(this, true); +} + +static const struct spa_bt_device_events device_events = { + SPA_VERSION_BT_DEVICE_EVENTS, + .player_added = player_added, + .player_changed = player_changed, + .player_removed = player_removed, +}; + static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) { struct impl *this; @@ -1277,12 +1385,15 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void static int impl_clear(struct spa_handle *handle) { struct impl *this = (struct impl *) handle; + if (this->codec_data) this->codec->deinit(this->codec_data); + if (this->codec_props && this->codec->clear_props) this->codec->clear_props(this->codec_props); - if (this->transport) - spa_hook_remove(&this->transport_listener); + + impl_remove_hooks(this); + return 0; } @@ -1395,6 +1506,9 @@ impl_init(const struct spa_handle_factory *factory, spa_bt_transport_add_listener(this->transport, &this->transport_listener, &transport_events, this); + spa_bt_device_add_listener(this->transport->device, &this->device_listener, &device_events, this); + try_find_player(this); + return 0; } diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 9dfcf880a..586a87a73 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -77,6 +77,7 @@ struct spa_bt_monitor { struct spa_list device_list; struct spa_list remote_endpoint_list; struct spa_list transport_list; + struct spa_list player_list; unsigned int filters_added:1; unsigned int objects_listed:1; @@ -747,6 +748,238 @@ static void adapter_free(struct spa_bt_adapter *adapter) free(adapter); } +static void device_remove_player(struct spa_bt_player *player) +{ + if (player->device == NULL) + return; + + spa_list_remove(&player->device_link); + + spa_hook_list_call(&player->device->listener_list, + struct spa_bt_device_events, player_removed, SPA_VERSION_BT_DEVICE_EVENTS, + player); + + player->device = NULL; +} + +static void device_add_player(struct spa_bt_device *device, struct spa_bt_player *player) +{ + if (player->device == device) + return; + + device_remove_player(player); + + player->device = device; + if (player->device == NULL) + return; + + spa_list_append(&player->device->player_list, &player->device_link); + + spa_hook_list_call(&player->device->listener_list, + struct spa_bt_device_events, player_added, SPA_VERSION_BT_DEVICE_EVENTS, + player); +} + +static struct spa_bt_player *player_create(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_player *p = calloc(1, sizeof(*p)); + + if (p == NULL) + return NULL; + + p->monitor = monitor; + p->path = strdup(path); + + spa_list_append(&monitor->player_list, &p->link); + + return p; +} + +static struct spa_bt_player *player_find(const struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_player *p; + + spa_list_for_each(p, &monitor->player_list, link) + if (spa_streq(p->path, path)) + return p; + + return NULL; +} + +static const char * const player_state_names[] = +{ + [SPA_BT_PLAYER_STATE_UNKNOWN] = "unknown", + [SPA_BT_PLAYER_STATE_PLAYING] = "playing", + [SPA_BT_PLAYER_STATE_STOPPED] = "stopped", + [SPA_BT_PLAYER_STATE_PAUSED] = "paused", + [SPA_BT_PLAYER_STATE_FORWARD_SEEK] = "forward-seek", + [SPA_BT_PLAYER_STATE_ERROR] = "error", +}; + +static enum spa_bt_player_state str_to_player_state(const char *s) +{ + size_t index; + + if (!spa_find_str_arr(player_state_names, s, strcasecmp, &index)) + return SPA_BT_PLAYER_STATE_UNKNOWN; + + return index; +} + +static const char * const player_type_names[] = +{ + [SPA_BT_PLAYER_TYPE_UNKNOWN] = "unknown", + [SPA_BT_PLAYER_TYPE_AUDIO] = "audio", + [SPA_BT_PLAYER_TYPE_VIDEO] = "video", + [SPA_BT_PLAYER_TYPE_AUDIO_BROADCASTING] = "audio broadcasting", + [SPA_BT_PLAYER_TYPE_VIDEO_BROADCASTING] = "video broadcasting", +}; + +static enum spa_bt_player_type str_to_player_type(const char *s) +{ + size_t index; + + if (!spa_find_str_arr(player_type_names, s, strcasecmp, &index)) + return SPA_BT_PLAYER_TYPE_UNKNOWN; + + return index; +} + +static void player_parse_track(struct spa_bt_player *player, DBusMessageIter *props_iter) +{ + struct spa_bt_monitor *monitor = player->monitor; + + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + type = dbus_message_iter_get_arg_type(&it[1]); + + if (type == DBUS_TYPE_STRING) { + const char *value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "player %p track: %s=%s", player, key, value); + + if (spa_streq(key, "Title")) { + free(player->track.title); + player->track.title = strdup(value); + } + else if (spa_streq(key, "Artist")) { + free(player->track.artist); + player->track.artist = strdup(value); + } + else if (spa_streq(key, "Album")) { + free(player->track.album); + player->track.album = strdup(value); + } + else if (spa_streq(key, "Genre")) { + free(player->track.genre); + player->track.genre = strdup(value); + } + else if (spa_streq(key, "TrackNumber")) { + if (!spa_atou32(value, &player->track.number, 10)) + player->track.number = -1; + } + else if (spa_streq(key, "Duration")) { + if (!spa_atou64(value, &player->track.duration, 10)) + player->track.duration = -1; + } + } + + dbus_message_iter_next(props_iter); + } +} + +static int player_update_props(struct spa_bt_player *player, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + struct spa_bt_monitor * const monitor = player->monitor; + + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + type = dbus_message_iter_get_arg_type(&it[1]); + + if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { + const char *value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "player %p: %s=%s", player, key, value); + + if (spa_streq(key, "Status")) { + player->status = str_to_player_state(value); + } + else if (spa_streq(key, "Device")) { + struct spa_bt_device *d = spa_bt_device_find(monitor, value); + + spa_log_debug(monitor->log, "player %p: device -> %p", player, d); + + device_add_player(d, player); + } + else if (spa_streq(key, "Name")) { + free(player->name); + player->name = strdup(value); + } + else if (spa_streq(key, "Type")) { + player->type = str_to_player_type(value); + } + } + else if (type == DBUS_TYPE_ARRAY) { + if (spa_streq(key, "Track")) { + DBusMessageIter dict_iter; + + dbus_message_iter_recurse(&it[1], &dict_iter); + player_parse_track(player, &dict_iter); + } + } + + dbus_message_iter_next(props_iter); + } + + if (player->device != NULL) + spa_hook_list_call(&player->device->listener_list, + struct spa_bt_device_events, player_changed, SPA_VERSION_BT_DEVICE_EVENTS, + player); + + return 0; +} + +static void player_free(struct spa_bt_player *player) +{ + struct spa_bt_monitor *monitor = player->monitor; + + spa_log_debug(monitor->log, "%p", player); + + device_remove_player(player); + + spa_list_remove(&player->link); + + free(player->track.title); + free(player->track.artist); + free(player->track.album); + free(player->track.genre); + free(player->path); + free(player->name); + free(player); +} + struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path) { struct spa_bt_device *d; @@ -783,6 +1016,7 @@ static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const spa_list_init(&d->remote_endpoint_list); spa_list_init(&d->transport_list); spa_list_init(&d->codec_switch_list); + spa_list_init(&d->player_list); spa_hook_list_init(&d->listener_list); @@ -806,6 +1040,7 @@ static void device_free(struct spa_bt_device *device) struct spa_bt_remote_endpoint *ep, *tep; struct spa_bt_a2dp_codec_switch *sw; struct spa_bt_transport *t, *tt; + struct spa_bt_player *p, *tp; struct spa_bt_monitor *monitor = device->monitor; spa_log_debug(monitor->log, "%p", device); @@ -836,6 +1071,11 @@ static void device_free(struct spa_bt_device *device) spa_list_consume(sw, &device->codec_switch_list, device_link) a2dp_codec_switch_free(sw); + spa_list_for_each_safe(p, tp, &device->player_list, device_link) { + spa_assert(p->device == device); + device_remove_player(p); + } + spa_list_remove(&device->link); free(device->path); free(device->alias); @@ -3356,6 +3596,21 @@ static void interface_added(struct spa_bt_monitor *monitor, if (d) spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles); } + else if (spa_streq(interface_name, BLUEZ_MEDIA_PLAYER_INTERFACE)) { + struct spa_bt_player *p; + + p = player_find(monitor, object_path); + if (p == NULL) { + p = player_create(monitor, object_path); + if (p == NULL) { + spa_log_warn(monitor->log, "can't create Bluetooth player %s: %m", + object_path); + return; + } + } + + player_update_props(p, props_iter, NULL); + } } static void interfaces_added(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter) @@ -3418,6 +3673,12 @@ static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter * if (d) spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles); } + } else if (spa_streq(interface_name, BLUEZ_MEDIA_PLAYER_INTERFACE)) { + struct spa_bt_player *p; + + p = player_find(monitor, object_path); + if (p != NULL) + player_free(p); } dbus_message_iter_next(&it); @@ -3518,6 +3779,7 @@ static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *us struct spa_bt_device *d; struct spa_bt_remote_endpoint *ep; struct spa_bt_transport *t; + struct spa_bt_player *p; monitor->objects_listed = false; @@ -3534,6 +3796,8 @@ static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *us device_free(d); spa_list_consume(a, &monitor->adapter_list, link) adapter_free(a); + spa_list_consume(p, &monitor->player_list, link) + player_free(p); } if (has_new_owner) { @@ -3689,7 +3953,20 @@ static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *us transport_update_props(transport, &it[1], NULL); } - } + else if (spa_streq(iface, BLUEZ_MEDIA_PLAYER_INTERFACE)) { + struct spa_bt_player *p; + + p = player_find(monitor, path); + if (p == NULL) { + spa_log_warn(monitor->log, "properties changed in unknown player %s", path); + goto finish; + } + + spa_log_debug(monitor->log, "properties changed on player %s", path); + + player_update_props(p, &it[1], NULL); + } + } fail: dbus_error_free(&err); @@ -3749,6 +4026,10 @@ static void add_filters(struct spa_bt_monitor *this) "type='signal',sender='" BLUEZ_SERVICE "'," "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," "arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," + "arg0='" BLUEZ_MEDIA_PLAYER_INTERFACE "'", &err); this->filters_added = true; @@ -3813,6 +4094,7 @@ static int impl_clear(struct spa_handle *handle) struct spa_bt_device *d; struct spa_bt_remote_endpoint *ep; struct spa_bt_transport *t; + struct spa_bt_player *p; monitor = (struct spa_bt_monitor *) handle; @@ -3837,6 +4119,8 @@ static int impl_clear(struct spa_handle *handle) device_free(d); spa_list_consume(a, &monitor->adapter_list, link) adapter_free(a); + spa_list_consume(p, &monitor->player_list, link) + player_free(p); if (monitor->backend_native) { spa_bt_backend_free(monitor->backend_native); @@ -4049,6 +4333,7 @@ impl_init(const struct spa_handle_factory *factory, spa_list_init(&this->device_list); spa_list_init(&this->remote_endpoint_list); spa_list_init(&this->transport_list); + spa_list_init(&this->player_list); if ((res = parse_codec_array(this, info)) < 0) return res; diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 022f926e0..c941a3282 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -50,6 +50,7 @@ extern "C" { #define BLUEZ_MEDIA_INTERFACE BLUEZ_SERVICE ".Media1" #define BLUEZ_MEDIA_ENDPOINT_INTERFACE BLUEZ_SERVICE ".MediaEndpoint1" #define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1" +#define BLUEZ_MEDIA_PLAYER_INTERFACE BLUEZ_SERVICE ".MediaPlayer1" #define BLUEZ_INTERFACE_BATTERY_PROVIDER BLUEZ_SERVICE ".BatteryProvider1" #define BLUEZ_INTERFACE_BATTERY_PROVIDER_MANAGER BLUEZ_SERVICE ".BatteryProviderManager1" @@ -411,6 +412,7 @@ static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t blu struct spa_bt_a2dp_codec_switch; struct spa_bt_transport; +struct spa_bt_player; struct spa_bt_device_events { #define SPA_VERSION_BT_DEVICE_EVENTS 0 @@ -427,6 +429,12 @@ struct spa_bt_device_events { /** Device freed */ void (*destroy) (void *data); + + void (*player_added) (void *data, struct spa_bt_player *player); + + void (*player_changed) (void *data, struct spa_bt_player *player); + + void (*player_removed) (void *data, struct spa_bt_player *player); }; struct spa_bt_device { @@ -460,6 +468,7 @@ struct spa_bt_device { struct spa_list remote_endpoint_list; struct spa_list transport_list; struct spa_list codec_switch_list; + struct spa_list player_list; uint8_t battery; int has_battery; @@ -644,6 +653,48 @@ static inline enum spa_bt_transport_state spa_bt_transport_state_from_string(con return SPA_BT_TRANSPORT_STATE_IDLE; } +enum spa_bt_player_state { + SPA_BT_PLAYER_STATE_UNKNOWN, + SPA_BT_PLAYER_STATE_PLAYING, + SPA_BT_PLAYER_STATE_STOPPED, + SPA_BT_PLAYER_STATE_PAUSED, + SPA_BT_PLAYER_STATE_FORWARD_SEEK, + SPA_BT_PLAYER_STATE_ERROR, +}; + +enum spa_bt_player_type { + SPA_BT_PLAYER_TYPE_UNKNOWN, + SPA_BT_PLAYER_TYPE_AUDIO, + SPA_BT_PLAYER_TYPE_VIDEO, + SPA_BT_PLAYER_TYPE_AUDIO_BROADCASTING, + SPA_BT_PLAYER_TYPE_VIDEO_BROADCASTING, +}; + +struct spa_bt_player { + struct spa_list link; + + struct spa_bt_monitor *monitor; + + struct spa_bt_device *device; + struct spa_list device_link; + + char *name; + char *path; + + struct { + uint32_t number; + uint64_t duration; + + char *title; + char *artist; + char *album; + char *genre; + } track; + + enum spa_bt_player_state status; + enum spa_bt_player_type type; +}; + #define DEFAULT_AG_VOLUME 1.0f #define DEFAULT_RX_VOLUME 1.0f #define DEFAULT_TX_VOLUME 0.064f /* spa_bt_volume_hw_to_linear(40, 100) */