Hi Frédéric, On Fri, Jan 24, 2025 at 11:48 AM Frédéric Danis <frederic.danis@xxxxxxxxxxxxx> wrote: > > This commit connects to the bip-avrcp Obex service if the > org.bluez.MediaPlayer ObexPort property exists. > Once connected, the Track properties update may contain an > ImgHandle which is automatically downloaded, then a Metadata > property updated signal is sent on org.mpris.MediaPlayer2.Player > interface. > > Some devices share the Obex session between multiple players. So > the Obex session is created by device. Can you add a sample output? Also our long term plan is to integrate mpris-player functionality into bluetoothctl. > --- > tools/mpris-proxy.c | 435 +++++++++++++++++++++++++++++++++++++++++++- > 1 file changed, 425 insertions(+), 10 deletions(-) > > diff --git a/tools/mpris-proxy.c b/tools/mpris-proxy.c > index e5fc91fdb..1f6c86777 100644 > --- a/tools/mpris-proxy.c > +++ b/tools/mpris-proxy.c > @@ -30,11 +30,18 @@ > #define BLUEZ_BUS_NAME "org.bluez" > #define BLUEZ_PATH "/org/bluez" > #define BLUEZ_ADAPTER_INTERFACE "org.bluez.Adapter1" > +#define BLUEZ_DEVICE_INTERFACE "org.bluez.Device1" > #define BLUEZ_MEDIA_INTERFACE "org.bluez.Media1" > #define BLUEZ_MEDIA_PLAYER_INTERFACE "org.bluez.MediaPlayer1" > #define BLUEZ_MEDIA_FOLDER_INTERFACE "org.bluez.MediaFolder1" > #define BLUEZ_MEDIA_ITEM_INTERFACE "org.bluez.MediaItem1" > #define BLUEZ_MEDIA_TRANSPORT_INTERFACE "org.bluez.MediaTransport1" > +#define BLUEZ_OBEX_BUS_NAME "org.bluez.obex" > +#define BLUEZ_OBEX_PATH "/org/bluez/obex" > +#define BLUEZ_OBEX_CLIENT_PATH BLUEZ_OBEX_PATH "/client" > +#define BLUEZ_OBEX_CLIENT_INTERFACE "org.bluez.obex.Client1" > +#define BLUEZ_OBEX_IMAGE_INTERFACE "org.bluez.obex.Image1" > +#define BLUEZ_OBEX_TRANSFER_INTERFACE "org.bluez.obex.Transfer1" > #define MPRIS_BUS_NAME "org.mpris.MediaPlayer2." > #define MPRIS_INTERFACE "org.mpris.MediaPlayer2" > #define MPRIS_PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player" > @@ -48,8 +55,10 @@ static GDBusProxy *adapter = NULL; > static DBusConnection *sys = NULL; > static DBusConnection *session = NULL; > static GDBusClient *client = NULL; > +static GDBusClient *obex_client; > static GSList *players = NULL; > static GSList *transports = NULL; > +static GSList *obex_sessions; > > static gboolean option_version = FALSE; > static gboolean option_export = FALSE; > @@ -59,6 +68,12 @@ struct tracklist { > GSList *items; > }; > > +struct obex_session { > + GDBusProxy *device; > + GDBusProxy *obex; > + uint16_t port; > +}; > + > struct player { > char *bus_name; > DBusConnection *conn; > @@ -67,11 +82,14 @@ struct player { > GDBusProxy *device; > GDBusProxy *transport; > GDBusProxy *playlist; > + struct obex_session *obex; > struct tracklist *tracklist; > + char *filename; > }; > > typedef int (* parse_metadata_func) (DBusMessageIter *iter, const char *key, > - DBusMessageIter *metadata); > + DBusMessageIter *metadata, > + void *userdata); > > static void dict_append_entry(DBusMessageIter *dict, const char *key, int type, > void *val); > @@ -240,7 +258,8 @@ static void dict_append_iter(DBusMessageIter *dict, const char *key, > } > > static int parse_metadata_entry(DBusMessageIter *entry, const char *key, > - DBusMessageIter *metadata) > + DBusMessageIter *metadata, > + void *userdata) > { > if (dbus_message_iter_get_arg_type(entry) != DBUS_TYPE_VARIANT) > return -EINVAL; > @@ -251,7 +270,8 @@ static int parse_metadata_entry(DBusMessageIter *entry, const char *key, > } > > static int parse_metadata(DBusMessageIter *args, DBusMessageIter *metadata, > - parse_metadata_func func) > + parse_metadata_func func, > + void *userdata) > { > DBusMessageIter dict; > int ctype; > @@ -277,7 +297,7 @@ static int parse_metadata(DBusMessageIter *args, DBusMessageIter *metadata, > dbus_message_iter_get_basic(&entry, &key); > dbus_message_iter_next(&entry); > > - if (func(&entry, key, metadata) < 0) > + if (func(&entry, key, metadata, userdata) < 0) > return -EINVAL; > > dbus_message_iter_next(&dict); > @@ -299,7 +319,7 @@ static void append_metadata(DBusMessageIter *iter, DBusMessageIter *dict, > DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING > DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &metadata); > > - parse_metadata(dict, &metadata, func); > + parse_metadata(dict, &metadata, func, NULL); > > dbus_message_iter_close_container(&value, &metadata); > dbus_message_iter_close_container(iter, &value); > @@ -1223,7 +1243,8 @@ static gboolean parse_path_metadata(DBusMessageIter *iter, const char *key, > } > > static int parse_track_entry(DBusMessageIter *entry, const char *key, > - DBusMessageIter *metadata) > + DBusMessageIter *metadata, > + void *userdata) > { > DBusMessageIter var; > > @@ -1253,6 +1274,30 @@ static int parse_track_entry(DBusMessageIter *entry, const char *key, > } else if (strcasecmp(key, "Item") == 0) { > if (!parse_path_metadata(&var, "mpris:trackid", metadata)) > return -EINVAL; > + } else if (strcasecmp(key, "ImgHandle") == 0) { > + struct player *player = userdata; > + const char *handle, *path; > + char *filename, *uri; > + > + if (!player || !player->obex) > + return -EINVAL; > + > + path = g_dbus_proxy_get_path(player->obex->obex); > + > + if (dbus_message_iter_get_arg_type(&var) != DBUS_TYPE_STRING) > + return -EINVAL; > + dbus_message_iter_get_basic(&var, &handle); > + > + filename = g_strconcat(g_get_tmp_dir(), "/", > + path + strlen(BLUEZ_OBEX_CLIENT_PATH "/"), > + "-", handle, NULL); > + if (access(filename, F_OK) == 0) { > + uri = g_strconcat("file://", filename, NULL); > + dict_append_entry(metadata, "mpris:artUrl", > + DBUS_TYPE_STRING, &uri); > + g_free(uri); > + } > + g_free(filename); > } > > return 0; > @@ -1272,7 +1317,7 @@ static gboolean get_track(const GDBusPropertyTable *property, > DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING > DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &metadata); > > - parse_metadata(&var, &metadata, parse_track_entry); > + parse_metadata(&var, &metadata, parse_track_entry, player); > > dbus_message_iter_close_container(iter, &metadata); > > @@ -1443,7 +1488,7 @@ static void append_item_metadata(void *data, void *user_data) > &path); > > if (g_dbus_proxy_get_property(item, "Metadata", &var)) > - parse_metadata(&var, &metadata, parse_track_entry); > + parse_metadata(&var, &metadata, parse_track_entry, NULL); > > dbus_message_iter_close_container(iter, &metadata); > > @@ -1938,11 +1983,72 @@ static void register_tracklist(GDBusProxy *proxy) > player, NULL); > } > > +static GDBusProxy *connect_obex_session(const char *address, uint16_t port) > +{ > + static const char *target_str = "bip-avrcp"; > + DBusMessage *msg, *reply; > + DBusMessageIter iter, array; > + const char *path; > + DBusError err; > + > + msg = dbus_message_new_method_call(BLUEZ_OBEX_BUS_NAME, > + BLUEZ_OBEX_PATH, > + BLUEZ_OBEX_CLIENT_INTERFACE, > + "CreateSession"); > + dbus_message_iter_init_append(msg, &iter); > + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &address); > + dbus_message_iter_open_container(&iter, 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, > + &array); > + dict_append_entry(&array, "Target", DBUS_TYPE_STRING, &target_str); > + dict_append_entry(&array, "PSM", DBUS_TYPE_UINT16, &port); > + dbus_message_iter_close_container(&iter, &array); > + > + dbus_error_init(&err); > + reply = dbus_connection_send_with_reply_and_block(session, msg, -1, > + &err); > + dbus_message_unref(msg); > + if (!reply) { > + if (dbus_error_is_set(&err)) { > + fprintf(stderr, "%s\n", err.message); > + dbus_error_free(&err); > + } > + return NULL; > + } > + > + if (!dbus_message_get_args(reply, NULL, > + DBUS_TYPE_OBJECT_PATH, &path, > + DBUS_TYPE_INVALID)) { > + dbus_message_unref(reply); > + return NULL; > + } > + > + return g_dbus_proxy_new(obex_client, path, BLUEZ_OBEX_IMAGE_INTERFACE); > +} > + > +static struct obex_session *find_obex_session_by_device(const char *device) > +{ > + GSList *l; > + > + for (l = obex_sessions; l; l = l->next) { > + struct obex_session *session = l->data; > + const char *path = g_dbus_proxy_get_path(session->device); > + > + if (g_strcmp0(device, path) == 0) > + return session; > + } > + > + return NULL; > +} > + > static void register_player(GDBusProxy *proxy) > { > struct player *player; > DBusMessageIter iter; > - const char *path, *alias, *name; > + const char *path, *alias, *name, *address; > char *busname; > GDBusProxy *device, *transport; > > @@ -1960,6 +2066,11 @@ static void register_player(GDBusProxy *proxy) > > dbus_message_iter_get_basic(&iter, &alias); > > + if (!g_dbus_proxy_get_property(device, "Address", &iter)) > + return; > + > + dbus_message_iter_get_basic(&iter, &address); > + > if (g_dbus_proxy_get_property(proxy, "Name", &iter)) { > dbus_message_iter_get_basic(&iter, &name); > busname = g_strconcat(alias, " ", name, NULL); > @@ -1971,6 +2082,27 @@ static void register_player(GDBusProxy *proxy) > player->proxy = g_dbus_proxy_ref(proxy); > player->device = device; > > + if (g_dbus_proxy_get_property(proxy, "ObexPort", &iter)) { > + uint16_t port; > + struct obex_session *session; > + > + dbus_message_iter_get_basic(&iter, &port); > + > + session = find_obex_session_by_device(path); > + if (session == NULL || session->port != port) { > + printf("Create new session\n"); > + session = g_new0(struct obex_session, 1); > + session->obex = connect_obex_session(address, port); > + session->device = g_dbus_proxy_ref(device); > + session->port = port; > + > + obex_sessions = g_slist_prepend(obex_sessions, session); > + } > + player->obex = session; > + } else { > + player->obex = NULL; > + } > + > g_free(busname); > > players = g_slist_prepend(players, player); > @@ -2177,7 +2309,7 @@ static void register_item(struct player *player, GDBusProxy *proxy) > DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING > DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &metadata); > > - parse_metadata(&iter, &metadata, parse_track_entry); > + parse_metadata(&iter, &metadata, parse_track_entry, player); > > dbus_message_iter_close_container(&args, &metadata); > > @@ -2377,6 +2509,121 @@ static const char *property_to_mpris(const char *property) > return NULL; > } > > +static const char *obex_get_image_handle(DBusMessageIter *args) > +{ > + DBusMessageIter dict, var; > + int ctype; > + > + ctype = dbus_message_iter_get_arg_type(args); > + if (ctype != DBUS_TYPE_ARRAY) > + return NULL; > + > + dbus_message_iter_recurse(args, &dict); > + > + while ((ctype = dbus_message_iter_get_arg_type(&dict)) != > + DBUS_TYPE_INVALID) { > + DBusMessageIter entry; > + const char *key; > + > + if (ctype != DBUS_TYPE_DICT_ENTRY) > + return NULL; > + > + dbus_message_iter_recurse(&dict, &entry); > + if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING) > + return NULL; > + > + dbus_message_iter_get_basic(&entry, &key); > + dbus_message_iter_next(&entry); > + > + if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT) > + return NULL; > + > + dbus_message_iter_recurse(&entry, &var); > + > + if (strcasecmp(key, "ImgHandle") == 0) { > + const char *handle; > + > + dbus_message_iter_get_basic(&var, &handle); > + return handle; > + } > + > + dbus_message_iter_next(&dict); > + } > + > + return NULL; > +} > + > +static void obex_get_image(struct player *player, const char *handle) > +{ > + DBusMessage *msg; > + DBusMessageIter iter, array; > + struct obex_session *obex_session = player->obex; > + const char *path = g_dbus_proxy_get_path(obex_session->obex); > + char *filename; > + > + player->filename = g_strconcat(g_get_tmp_dir(), "/", > + path + strlen(BLUEZ_OBEX_CLIENT_PATH "/"), > + "-", handle, NULL); > + filename = g_strconcat(player->filename, ".tmp", NULL); > + > + msg = dbus_message_new_method_call(BLUEZ_OBEX_BUS_NAME, > + path, > + BLUEZ_OBEX_IMAGE_INTERFACE, > + "Get"); > + dbus_message_iter_init_append(msg, &iter); > + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &filename); > + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &handle); > + dbus_message_iter_open_container(&iter, 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, > + &array); > + dbus_message_iter_close_container(&iter, &array); > + > + if (!g_dbus_send_message(session, msg)) { > + g_free(player->filename); > + player->filename = NULL; > + } > + g_free(filename); > +} > + > +static void device_property_changed(GDBusProxy *proxy, const char *name, > + DBusMessageIter *iter, void *user_data) > +{ > + const char *path; > + struct obex_session *session; > + gboolean connected; > + GSList *l; > + > + path = g_dbus_proxy_get_path(proxy); > + > + if (strcasecmp(name, "Connected") != 0) > + return; > + > + dbus_message_iter_get_basic(iter, &connected); > + > + if (connected) > + return; > + > + printf("Bluetooth Device %s disconnected\n", path); > + session = find_obex_session_by_device(path); > + if (session == NULL) > + return; > + > + for (l = players; l; l = l->next) { > + struct player *player = l->data; > + > + if (player->obex == session) > + player->obex = NULL; > + } > + > + g_dbus_proxy_unref(session->obex); > + g_dbus_proxy_unref(session->device); > + obex_sessions = g_slist_remove(obex_sessions, session); > + g_free(session); > +} > + > static void player_property_changed(GDBusProxy *proxy, const char *name, > DBusMessageIter *iter, void *user_data) > { > @@ -2397,6 +2644,13 @@ static void player_property_changed(GDBusProxy *proxy, const char *name, > MPRIS_PLAYER_INTERFACE, > property); > > + if (strcasecmp(name, "Track") == 0 && player->obex) { > + const char *handle = obex_get_image_handle(iter); > + > + if (handle) > + obex_get_image(player, handle); > + } > + > if (strcasecmp(name, "Position") != 0) > return; > > @@ -2485,6 +2739,9 @@ static void property_changed(GDBusProxy *proxy, const char *name, > > interface = g_dbus_proxy_get_interface(proxy); > > + if (strcmp(interface, BLUEZ_DEVICE_INTERFACE) == 0) > + return device_property_changed(proxy, name, iter, user_data); > + > if (strcmp(interface, BLUEZ_MEDIA_PLAYER_INTERFACE) == 0) > return player_property_changed(proxy, name, iter, user_data); > > @@ -2496,6 +2753,151 @@ static void property_changed(GDBusProxy *proxy, const char *name, > return item_property_changed(proxy, name, iter, user_data); > } > > +static struct player *find_player_by_obex(const char *path) > +{ > + GSList *l; > + > + for (l = players; l; l = l->next) { > + struct player *player = l->data; > + struct obex_session *session = player->obex; > + const char *obex_path = g_dbus_proxy_get_path(session->obex); > + > + if (g_str_has_prefix(path, obex_path)) > + return player; > + } > + > + return NULL; > +} > + > +static void obex_connect_handler(DBusConnection *connection, void *user_data) > +{ > + printf("org.bluez.obex appeared\n"); > +} > + > +static void obex_disconnect_handler(DBusConnection *connection, > + void *user_data) > +{ > + printf("org.bluez.obex disappeared\n"); > +} > + > +static void obex_proxy_added(GDBusProxy *proxy, void *user_data) > +{ > + const char *interface; > + const char *path; > + > + interface = g_dbus_proxy_get_interface(proxy); > + path = g_dbus_proxy_get_path(proxy); > + > + if (!strcmp(interface, BLUEZ_OBEX_CLIENT_INTERFACE)) { > + GSList *l; > + > + printf("Bluetooth Obex Client %s found\n", path); > + > + for (l = players; l; l = l->next) { > + struct player *player = l->data; > + DBusMessageIter iter; > + const char *address; > + uint16_t port; > + struct obex_session *session; > + > + if (!g_dbus_proxy_get_property(player->proxy, > + "ObexPort", &iter) || > + player->obex) > + continue; > + > + dbus_message_iter_get_basic(&iter, &port); > + > + if (!g_dbus_proxy_get_property(player->device, > + "Address", &iter)) > + continue; > + > + dbus_message_iter_get_basic(&iter, &address); > + > + session = find_obex_session_by_device(path); > + if (session == NULL || session->port != port) { > + printf("Bluetooth Obex Create new session\n"); > + session = g_new0(struct obex_session, 1); > + session->obex = connect_obex_session(address, > + port); > + session->device = g_dbus_proxy_ref( > + player->device); > + session->port = port; > + > + obex_sessions = g_slist_prepend(obex_sessions, > + session); > + } > + player->obex = session; > + } > + } > +} > + > +static void obex_proxy_removed(GDBusProxy *proxy, void *user_data) > +{ > + const char *interface; > + const char *path; > + > + if (adapter == NULL) > + return; > + > + interface = g_dbus_proxy_get_interface(proxy); > + path = g_dbus_proxy_get_path(proxy); > + > + if (strcmp(interface, BLUEZ_OBEX_CLIENT_INTERFACE) == 0) { > + GSList *l; > + > + printf("Bluetooth Obex Client %s removed\n", path); > + > + for (l = players; l; l = l->next) { > + struct player *player = l->data; > + > + player->obex = NULL; > + } > + > + while (obex_sessions) { > + struct obex_session *session = obex_sessions->data; > + > + g_dbus_proxy_unref(session->device); > + g_dbus_proxy_unref(session->obex); > + obex_sessions = g_slist_remove(obex_sessions, session); > + } > + } > +} > + > +static void obex_property_changed(GDBusProxy *proxy, const char *name, > + DBusMessageIter *iter, void *user_data) > +{ > + const char *interface; > + const char *path; > + > + interface = g_dbus_proxy_get_interface(proxy); > + path = g_dbus_proxy_get_path(proxy); > + > + if (strcmp(interface, BLUEZ_OBEX_TRANSFER_INTERFACE) == 0) { > + struct player *player; > + const char *status; > + > + if (strcasecmp(name, "Status") != 0) > + return; > + > + dbus_message_iter_get_basic(iter, &status); > + > + player = find_player_by_obex(path); > + if (player && strcasecmp(status, "complete") == 0) { > + char *filename; > + > + filename = g_strconcat(player->filename, ".tmp", NULL); > + rename(filename, player->filename); > + g_free(player->filename); > + player->filename = NULL; > + > + g_dbus_emit_property_changed(player->conn, > + MPRIS_PLAYER_PATH, > + MPRIS_PLAYER_INTERFACE, > + "Metadata"); > + } > + } > +} > + > int main(int argc, char *argv[]) > { > GOptionContext *context; > @@ -2566,6 +2968,19 @@ int main(int argc, char *argv[]) > g_dbus_client_set_proxy_handlers(client, proxy_added, proxy_removed, > property_changed, NULL); > > + obex_client = g_dbus_client_new(session, BLUEZ_OBEX_BUS_NAME, > + BLUEZ_OBEX_PATH); > + > + g_dbus_client_set_connect_watch(obex_client, obex_connect_handler, > + NULL); > + g_dbus_client_set_disconnect_watch(obex_client, > + obex_disconnect_handler, > + NULL); > + > + g_dbus_client_set_proxy_handlers(obex_client, obex_proxy_added, > + obex_proxy_removed, > + obex_property_changed, NULL); > + > g_main_loop_run(main_loop); > > g_dbus_remove_watch(session, owner_watch); > -- > 2.43.0 > > -- Luiz Augusto von Dentz