#include #include #include #include #include #include static const char *introspection_xml = "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; typedef struct UserData { mpv_handle *mpv; GMainLoop *loop; gint bus_id; GDBusConnection *connection; GDBusInterfaceInfo *root_interface_info; GDBusInterfaceInfo *player_interface_info; guint root_interface_id; guint player_interface_id; const char *status; const char *loop_status; GHashTable *changed_properties; GVariant *metadata; gboolean seek_expected; gboolean idle; gboolean paused; } UserData; static const char *STATUS_PLAYING = "Playing"; static const char *STATUS_PAUSED = "Paused"; static const char *STATUS_STOPPED = "Stopped"; static const char *LOOP_NONE = "None"; static const char *LOOP_TRACK = "Track"; static const char *LOOP_PLAYLIST = "Playlist"; static gchar *string_to_utf8(gchar *maybe_utf8) { gchar *attempted_validation; attempted_validation = g_utf8_make_valid(maybe_utf8, -1); if (g_utf8_validate(attempted_validation, -1, NULL)) { return attempted_validation; } else { g_free(attempted_validation); return g_strdup(""); } } static void add_metadata_item_string(mpv_handle *mpv, GVariantDict *dict, const char *property, const char *tag) { char *temp = mpv_get_property_string(mpv, property); if (temp) { char *utf8 = string_to_utf8(temp); g_variant_dict_insert(dict, tag, "s", utf8); g_free(utf8); mpv_free(temp); } } static void add_metadata_item_int(mpv_handle *mpv, GVariantDict *dict, const char *property, const char *tag) { int64_t value; int res = mpv_get_property(mpv, property, MPV_FORMAT_INT64, &value); if (res >= 0) { g_variant_dict_insert(dict, tag, "x", value); } } static void add_metadata_item_string_list(mpv_handle *mpv, GVariantDict *dict, const char *property, const char *tag) { char *temp = mpv_get_property_string(mpv, property); if (temp) { GVariantBuilder builder; char **list = g_strsplit(temp, ", ", 0); char **iter = list; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); for (; *iter; iter++) { char *item = *iter; char *utf8 = string_to_utf8(item); g_variant_builder_add(&builder, "s", utf8); g_free(utf8); } g_variant_dict_insert(dict, tag, "as", &builder); g_strfreev(list); mpv_free(temp); } } static gchar *path_to_uri(mpv_handle *mpv, char *path) { #if GLIB_CHECK_VERSION(2, 58, 0) // version which uses g_canonicalize_filename which expands .. and . // and makes the uris neater char* working_dir; gchar* canonical; gchar *uri; working_dir = mpv_get_property_string(mpv, "working-directory"); canonical = g_canonicalize_filename(path, working_dir); uri = g_filename_to_uri(canonical, NULL, NULL); mpv_free(working_dir); g_free(canonical); return uri; #else // for compatibility with older versions of glib gchar *converted; if (g_path_is_absolute(path)) { converted = g_filename_to_uri(path, NULL, NULL); } else { char* working_dir; gchar* absolute; working_dir = mpv_get_property_string(mpv, "working-directory"); absolute = g_build_filename(working_dir, path, NULL); converted = g_filename_to_uri(absolute, NULL, NULL); mpv_free(working_dir); g_free(absolute); } return converted; #endif } static void add_metadata_uri(mpv_handle *mpv, GVariantDict *dict) { char *path; char *uri; path = mpv_get_property_string(mpv, "path"); if (!path) { return; } uri = g_uri_parse_scheme(path); if (uri) { g_variant_dict_insert(dict, "xesam:url", "s", path); g_free(uri); } else { gchar *converted = path_to_uri(mpv, path); g_variant_dict_insert(dict, "xesam:url", "s", converted); g_free(converted); } mpv_free(path); } // Copied from https://github.com/videolan/vlc/blob/master/modules/meta_engine/folder.c static const char art_files[][20] = { "Folder.jpg", /* Windows */ "Folder.png", "AlbumArtSmall.jpg", /* Windows */ "AlbumArt.jpg", /* Windows */ "Album.jpg", ".folder.png", /* KDE? */ "cover.jpg", /* rockbox */ "cover.png", "cover.gif", "front.jpg", "front.png", "front.gif", "front.bmp", "thumb.jpg", }; static const int art_files_count = sizeof(art_files) / sizeof(art_files[0]); static gchar* try_get_local_art(mpv_handle *mpv, char *path) { gchar *dirname = g_path_get_dirname(path), *out = NULL; gboolean found = FALSE; for (int i = 0; i < art_files_count; i++) { gchar *filename = g_build_filename(dirname, art_files[i], NULL); if (g_file_test(filename, G_FILE_TEST_EXISTS)) { out = path_to_uri(mpv, filename); found = TRUE; } g_free(filename); if (found) { break; } } g_free(dirname); return out; } static const char *youtube_url_pattern = "^https?:\\/\\/(?:youtu.be\\/|(?:www\\.)?youtube\\.com\\/watch\\?v=)(?[a-zA-Z0-9_-]*)\\??.*"; static GRegex *youtube_url_regex; static gchar* try_get_youtube_thumbnail(char *path) { gchar *out = NULL; if (!youtube_url_regex) { youtube_url_regex = g_regex_new(youtube_url_pattern, 0, 0, NULL); } GMatchInfo *match_info; gboolean matched = g_regex_match(youtube_url_regex, path, 0, &match_info); if (matched) { gchar *video_id = g_match_info_fetch_named(match_info, "id"); out = g_strconcat("https://i1.ytimg.com/vi/", video_id, "/hqdefault.jpg", NULL); g_free(video_id); } g_match_info_free(match_info); return out; } static gchar* extract_embedded_art(AVFormatContext *context) { AVPacket *packet = NULL; for (unsigned int i = 0; i < context->nb_streams; i++) { if (context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) { packet = &context->streams[i]->attached_pic; } } if (!packet) { return NULL; } gchar *data = g_base64_encode(packet->data, packet->size); gchar *img = g_strconcat("data:image/jpeg;base64,", data, NULL); g_free(data); return img; } static gchar* try_get_embedded_art(char *path) { gchar *out = NULL; AVFormatContext *context = NULL; if (!avformat_open_input(&context, path, NULL, NULL)) { out = extract_embedded_art(context); avformat_close_input(&context); } return out; } // cached last file path, owned by mpv static char *cached_path = NULL; // cached last artwork url, owned by glib static gchar *cached_art_url = NULL; static void add_metadata_art(mpv_handle *mpv, GVariantDict *dict) { char *path = mpv_get_property_string(mpv, "path"); if (!path) { return; } // mpv may call create_metadata multiple times, so cache to save CPU if (!cached_path || strcmp(path, cached_path)) { mpv_free(cached_path); g_free(cached_art_url); cached_path = path; if (g_str_has_prefix(path, "http")) { cached_art_url = try_get_youtube_thumbnail(path); } else { cached_art_url = try_get_embedded_art(path); if (!cached_art_url) { cached_art_url = try_get_local_art(mpv, path); } } } else { mpv_free(path); } if (cached_art_url) { g_variant_dict_insert(dict, "mpris:artUrl", "s", cached_art_url); } } static void add_metadata_content_created(mpv_handle *mpv, GVariantDict *dict) { char *date_str = mpv_get_property_string(mpv, "metadata/by-key/Date"); if (!date_str) { return; } GDate* date = g_date_new(); if (strlen(date_str) == 4) { gint64 year = g_ascii_strtoll(date_str, NULL, 10); if (year != 0) { g_date_set_dmy(date, 1, 1, year); } } else { g_date_set_parse(date, date_str); } if (g_date_valid(date)) { gchar iso8601[20]; g_date_strftime(iso8601, 20, "%Y-%m-%dT00:00:00Z", date); g_variant_dict_insert(dict, "xesam:contentCreated", "s", iso8601); } g_date_free(date); mpv_free(date_str); } static GVariant *create_metadata(UserData *ud) { GVariantDict dict; int64_t track; double duration; char *temp_str; int res; g_variant_dict_init(&dict, NULL); // mpris:trackid mpv_get_property(ud->mpv, "playlist-pos", MPV_FORMAT_INT64, &track); // playlist-pos < 0 if there is no playlist or current track if (track < 0) { temp_str = g_strdup("/noplaylist"); } else { temp_str = g_strdup_printf("/%" PRId64, track); } g_variant_dict_insert(&dict, "mpris:trackid", "o", temp_str); g_free(temp_str); // mpris:length res = mpv_get_property(ud->mpv, "duration", MPV_FORMAT_DOUBLE, &duration); if (res == MPV_ERROR_SUCCESS) { g_variant_dict_insert(&dict, "mpris:length", "x", (int64_t)(duration * 1000000.0)); } // initial value. Replaced with metadata value if available add_metadata_item_string(ud->mpv, &dict, "media-title", "xesam:title"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Title", "xesam:title"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Album", "xesam:album"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Genre", "xesam:genre"); /* Musicbrainz metadata mappings (https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html) */ // IDv3 metadata format add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Artist Id", "mb:artistId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Track Id", "mb:trackId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Album Artist Id", "mb:albumArtistId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Album Id", "mb:albumId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Release Track Id", "mb:releaseTrackId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Work Id", "mb:workId"); // Vorbis & APEv2 metadata format add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ARTISTID", "mb:artistId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_TRACKID", "mb:trackId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ALBUMARTISTID", "mb:albumArtistId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ALBUMID", "mb:albumId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_RELEASETRACKID", "mb:releaseTrackId"); add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_WORKID", "mb:workId"); add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/uploader", "xesam:artist"); add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Artist", "xesam:artist"); add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Album_Artist", "xesam:albumArtist"); add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Composer", "xesam:composer"); add_metadata_item_int(ud->mpv, &dict, "metadata/by-key/Track", "xesam:trackNumber"); add_metadata_item_int(ud->mpv, &dict, "metadata/by-key/Disc", "xesam:discNumber"); add_metadata_uri(ud->mpv, &dict); add_metadata_art(ud->mpv, &dict); add_metadata_content_created(ud->mpv, &dict); return g_variant_dict_end(&dict); } static void method_call_root(G_GNUC_UNUSED GDBusConnection *connection, G_GNUC_UNUSED const char *sender, G_GNUC_UNUSED const char *object_path, G_GNUC_UNUSED const char *interface_name, const char *method_name, G_GNUC_UNUSED GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { UserData *ud = (UserData*)user_data; if (g_strcmp0(method_name, "Quit") == 0) { const char *cmd[] = {"quit", NULL}; mpv_command_async(ud->mpv, 0, cmd); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Raise") == 0) { // Can't raise g_dbus_method_invocation_return_value(invocation, NULL); } else { g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Unknown method"); } } static GVariant *get_property_root(G_GNUC_UNUSED GDBusConnection *connection, G_GNUC_UNUSED const char *sender, G_GNUC_UNUSED const char *object_path, G_GNUC_UNUSED const char *interface_name, const char *property_name, G_GNUC_UNUSED GError **error, gpointer user_data) { UserData *ud = (UserData*)user_data; GVariant *ret; if (g_strcmp0(property_name, "CanQuit") == 0) { ret = g_variant_new_boolean(TRUE); } else if (g_strcmp0(property_name, "Fullscreen") == 0) { int fullscreen; mpv_get_property(ud->mpv, "fullscreen", MPV_FORMAT_FLAG, &fullscreen); ret = g_variant_new_boolean(fullscreen); } else if (g_strcmp0(property_name, "CanSetFullscreen") == 0) { int can_fullscreen; mpv_get_property(ud->mpv, "vo-configured", MPV_FORMAT_FLAG, &can_fullscreen); ret = g_variant_new_boolean(can_fullscreen); } else if (g_strcmp0(property_name, "CanRaise") == 0) { ret = g_variant_new_boolean(FALSE); } else if (g_strcmp0(property_name, "HasTrackList") == 0) { ret = g_variant_new_boolean(FALSE); } else if (g_strcmp0(property_name, "Identity") == 0) { ret = g_variant_new_string("mpv"); } else if (g_strcmp0(property_name, "DesktopEntry") == 0) { ret = g_variant_new_string("mpv"); } else if (g_strcmp0(property_name, "SupportedUriSchemes") == 0) { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); g_variant_builder_add(&builder, "s", "ftp"); g_variant_builder_add(&builder, "s", "http"); g_variant_builder_add(&builder, "s", "https"); g_variant_builder_add(&builder, "s", "mms"); g_variant_builder_add(&builder, "s", "rtmp"); g_variant_builder_add(&builder, "s", "rtsp"); g_variant_builder_add(&builder, "s", "sftp"); g_variant_builder_add(&builder, "s", "smb"); ret = g_variant_builder_end(&builder); } else if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); g_variant_builder_add(&builder, "s", "application/ogg"); g_variant_builder_add(&builder, "s", "audio/mpeg"); // TODO add the rest ret = g_variant_builder_end(&builder); } else { ret = NULL; g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Unknown property %s", property_name); } return ret; } static gboolean set_property_root(G_GNUC_UNUSED GDBusConnection *connection, G_GNUC_UNUSED const char *sender, G_GNUC_UNUSED const char *object_path, G_GNUC_UNUSED const char *interface_name, const char *property_name, GVariant *value, G_GNUC_UNUSED GError **error, gpointer user_data) { UserData *ud = (UserData*)user_data; if (g_strcmp0(property_name, "Fullscreen") == 0) { int fullscreen; g_variant_get(value, "b", &fullscreen); mpv_set_property(ud->mpv, "fullscreen", MPV_FORMAT_FLAG, &fullscreen); } else { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Cannot set property %s", property_name); return FALSE; } return TRUE; } static GDBusInterfaceVTable vtable_root = { method_call_root, get_property_root, set_property_root, {0} }; static void method_call_player(G_GNUC_UNUSED GDBusConnection *connection, G_GNUC_UNUSED const char *sender, G_GNUC_UNUSED const char *_object_path, G_GNUC_UNUSED const char *interface_name, const char *method_name, G_GNUC_UNUSED GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { UserData *ud = (UserData*)user_data; if (g_strcmp0(method_name, "Pause") == 0) { int paused = TRUE; mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "PlayPause") == 0) { int paused; if (ud->status == STATUS_PAUSED) { paused = FALSE; } else { paused = TRUE; } mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Play") == 0) { int paused = FALSE; mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Stop") == 0) { const char *cmd[] = {"stop", NULL}; mpv_command_async(ud->mpv, 0, cmd); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Next") == 0) { const char *cmd[] = {"playlist_next", NULL}; mpv_command_async(ud->mpv, 0, cmd); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Previous") == 0) { const char *cmd[] = {"playlist_prev", NULL}; mpv_command_async(ud->mpv, 0, cmd); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Seek") == 0) { int64_t offset_us; // in microseconds char *offset_str; g_variant_get(parameters, "(x)", &offset_us); double offset_s = offset_us / 1000000.0; offset_str = g_strdup_printf("%f", offset_s); const char *cmd[] = {"seek", offset_str, NULL}; mpv_command_async(ud->mpv, 0, cmd); g_dbus_method_invocation_return_value(invocation, NULL); g_free(offset_str); } else if (g_strcmp0(method_name, "SetPosition") == 0) { int64_t current_id; char *object_path; double new_position_s; int64_t new_position_us; mpv_get_property(ud->mpv, "playlist-pos", MPV_FORMAT_INT64, ¤t_id); g_variant_get(parameters, "(&ox)", &object_path, &new_position_us); new_position_s = ((float)new_position_us) / 1000000.0; // us -> s if (current_id == g_ascii_strtoll(object_path + 1, NULL, 10)) { mpv_set_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &new_position_s); } g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "OpenUri") == 0) { char *uri; g_variant_get(parameters, "(&s)", &uri); const char *cmd[] = {"loadfile", uri, NULL}; mpv_command_async(ud->mpv, 0, cmd); g_dbus_method_invocation_return_value(invocation, NULL); } else { g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Unknown method"); } } static GVariant *get_property_player(G_GNUC_UNUSED GDBusConnection *connection, G_GNUC_UNUSED const char *sender, G_GNUC_UNUSED const char *object_path, G_GNUC_UNUSED const char *interface_name, const char *property_name, GError **error, gpointer user_data) { UserData *ud = (UserData*)user_data; GVariant *ret; if (g_strcmp0(property_name, "PlaybackStatus") == 0) { ret = g_variant_new_string(ud->status); } else if (g_strcmp0(property_name, "LoopStatus") == 0) { ret = g_variant_new_string(ud->loop_status); } else if (g_strcmp0(property_name, "Rate") == 0) { double rate; mpv_get_property(ud->mpv, "speed", MPV_FORMAT_DOUBLE, &rate); ret = g_variant_new_double(rate); } else if (g_strcmp0(property_name, "Shuffle") == 0) { int shuffle; mpv_get_property(ud->mpv, "playlist-shuffle", MPV_FORMAT_FLAG, &shuffle); ret = g_variant_new_boolean(shuffle); } else if (g_strcmp0(property_name, "Metadata") == 0) { if (!ud->metadata) { ud->metadata = create_metadata(ud); } // Increase reference count to prevent it from being freed after returning g_variant_ref(ud->metadata); ret = ud->metadata; } else if (g_strcmp0(property_name, "Volume") == 0) { double volume; mpv_get_property(ud->mpv, "volume", MPV_FORMAT_DOUBLE, &volume); volume /= 100; ret = g_variant_new_double(volume); } else if (g_strcmp0(property_name, "Position") == 0) { double position_s; int64_t position_us; mpv_get_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_s); position_us = position_s * 1000000.0; // s -> us ret = g_variant_new_int64(position_us); } else if (g_strcmp0(property_name, "MinimumRate") == 0) { ret = g_variant_new_double(0.01); } else if (g_strcmp0(property_name, "MaximumRate") == 0) { ret = g_variant_new_double(100); } else if (g_strcmp0(property_name, "CanGoNext") == 0) { ret = g_variant_new_boolean(TRUE); } else if (g_strcmp0(property_name, "CanGoPrevious") == 0) { ret = g_variant_new_boolean(TRUE); } else if (g_strcmp0(property_name, "CanPlay") == 0) { ret = g_variant_new_boolean(TRUE); } else if (g_strcmp0(property_name, "CanPause") == 0) { ret = g_variant_new_boolean(TRUE); } else if (g_strcmp0(property_name, "CanSeek") == 0) { ret = g_variant_new_boolean(TRUE); } else if (g_strcmp0(property_name, "CanControl") == 0) { ret = g_variant_new_boolean(TRUE); } else { ret = NULL; g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Unknown property %s", property_name); } return ret; } static gboolean set_property_player(G_GNUC_UNUSED GDBusConnection *connection, G_GNUC_UNUSED const char *sender, G_GNUC_UNUSED const char *object_path, G_GNUC_UNUSED const char *interface_name, const char *property_name, GVariant *value, G_GNUC_UNUSED GError **error, gpointer user_data) { UserData *ud = (UserData*)user_data; if (g_strcmp0(property_name, "LoopStatus") == 0) { const char *status; int t = TRUE; int f = FALSE; status = g_variant_get_string(value, NULL); if (g_strcmp0(status, "Track") == 0) { mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &t); mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &f); } else if (g_strcmp0(status, "Playlist") == 0) { mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &f); mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &t); } else { mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &f); mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &f); } } else if (g_strcmp0(property_name, "Rate") == 0) { double rate = g_variant_get_double(value); mpv_set_property(ud->mpv, "speed", MPV_FORMAT_DOUBLE, &rate); } else if (g_strcmp0(property_name, "Shuffle") == 0) { int shuffle = g_variant_get_boolean(value); mpv_set_property(ud->mpv, "playlist-shuffle", MPV_FORMAT_FLAG, &shuffle); } else if (g_strcmp0(property_name, "Volume") == 0) { double volume = g_variant_get_double(value); volume *= 100; mpv_set_property(ud->mpv, "volume", MPV_FORMAT_DOUBLE, &volume); } else { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Cannot set property %s", property_name); return FALSE; } return TRUE; } static GDBusInterfaceVTable vtable_player = { method_call_player, get_property_player, set_property_player, {0} }; static gboolean emit_property_changes(gpointer data) { UserData *ud = (UserData*)data; GError *error = NULL; gpointer prop_name, prop_value; GHashTableIter iter; if (g_hash_table_size(ud->changed_properties) > 0) { GVariant *params; GVariantBuilder *properties = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); GVariantBuilder *invalidated = g_variant_builder_new(G_VARIANT_TYPE("as")); g_hash_table_iter_init(&iter, ud->changed_properties); while (g_hash_table_iter_next(&iter, &prop_name, &prop_value)) { if (prop_value) { g_variant_builder_add(properties, "{sv}", prop_name, prop_value); } else { g_variant_builder_add(invalidated, "s", prop_name); } } params = g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", properties, invalidated); g_variant_builder_unref(properties); g_variant_builder_unref(invalidated); g_dbus_connection_emit_signal(ud->connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", params, &error); if (error != NULL) { g_printerr("%s", error->message); } g_hash_table_remove_all(ud->changed_properties); } return TRUE; } static void emit_seeked_signal(UserData *ud) { GVariant *params; double position_s; int64_t position_us; GError *error = NULL; mpv_get_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_s); position_us = position_s * 1000000.0; // s -> us params = g_variant_new("(x)", position_us); g_dbus_connection_emit_signal(ud->connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "Seeked", params, &error); if (error != NULL) { g_printerr("%s", error->message); } } static GVariant * set_playback_status(UserData *ud) { if (ud->idle) { ud->status = STATUS_STOPPED; } else if (ud->paused) { ud->status = STATUS_PAUSED; } else { ud->status = STATUS_PLAYING; } return g_variant_new_string(ud->status); } static void set_stopped_status(UserData *ud) { const char *prop_name = "PlaybackStatus"; GVariant *prop_value = g_variant_new_string(STATUS_STOPPED); ud->status = STATUS_STOPPED; g_hash_table_insert(ud->changed_properties, (gpointer)prop_name, prop_value); emit_property_changes(ud); } // Register D-Bus object and interfaces static void on_bus_acquired(GDBusConnection *connection, G_GNUC_UNUSED const char *name, gpointer user_data) { GError *error = NULL; UserData *ud = user_data; ud->connection = connection; ud->root_interface_id = g_dbus_connection_register_object(connection, "/org/mpris/MediaPlayer2", ud->root_interface_info, &vtable_root, user_data, NULL, &error); if (error != NULL) { g_printerr("%s", error->message); } ud->player_interface_id = g_dbus_connection_register_object(connection, "/org/mpris/MediaPlayer2", ud->player_interface_info, &vtable_player, user_data, NULL, &error); if (error != NULL) { g_printerr("%s", error->message); } } static void on_name_lost(GDBusConnection *connection, G_GNUC_UNUSED const char *_name, gpointer user_data) { if (connection) { UserData *ud = user_data; pid_t pid = getpid(); char *name = g_strdup_printf("org.mpris.MediaPlayer2.mpv.instance%d", pid); ud->bus_id = g_bus_own_name(G_BUS_TYPE_SESSION, name, G_BUS_NAME_OWNER_FLAGS_NONE, NULL, NULL, NULL, &ud, NULL); g_free(name); } } static void handle_property_change(const char *name, void *data, UserData *ud) { const char *prop_name = NULL; GVariant *prop_value = NULL; if (g_strcmp0(name, "pause") == 0) { ud->paused = *(int*)data; prop_name = "PlaybackStatus"; prop_value = set_playback_status(ud); } else if (g_strcmp0(name, "idle-active") == 0) { ud->idle = *(int*)data; prop_name = "PlaybackStatus"; prop_value = set_playback_status(ud); } else if (g_strcmp0(name, "media-title") == 0 || g_strcmp0(name, "duration") == 0) { // Free existing metadata object if (ud->metadata) { g_variant_unref(ud->metadata); } ud->metadata = create_metadata(ud); prop_name = "Metadata"; prop_value = ud->metadata; } else if (g_strcmp0(name, "speed") == 0) { double *rate = data; prop_name = "Rate"; prop_value = g_variant_new_double(*rate); } else if (g_strcmp0(name, "volume") == 0) { double *volume = data; *volume /= 100; prop_name = "Volume"; prop_value = g_variant_new_double(*volume); } else if (g_strcmp0(name, "loop-file") == 0) { char *status = *(char **)data; if (g_strcmp0(status, "no") != 0) { ud->loop_status = LOOP_TRACK; } else { char *playlist_status; mpv_get_property(ud->mpv, "loop-playlist", MPV_FORMAT_STRING, &playlist_status); if (g_strcmp0(playlist_status, "no") != 0) { ud->loop_status = LOOP_PLAYLIST; } else { ud->loop_status = LOOP_NONE; } mpv_free(playlist_status); } prop_name = "LoopStatus"; prop_value = g_variant_new_string(ud->loop_status); } else if (g_strcmp0(name, "loop-playlist") == 0) { char *status = *(char **)data; if (g_strcmp0(status, "no") != 0) { ud->loop_status = LOOP_PLAYLIST; } else { char *file_status; mpv_get_property(ud->mpv, "loop-file", MPV_FORMAT_STRING, &file_status); if (g_strcmp0(file_status, "no") != 0) { ud->loop_status = LOOP_TRACK; } else { ud->loop_status = LOOP_NONE; } mpv_free(file_status); } prop_name = "LoopStatus"; prop_value = g_variant_new_string(ud->loop_status); } else if (g_strcmp0(name, "fullscreen") == 0) { gboolean *status = data; prop_name = "Fullscreen"; prop_value = g_variant_new_boolean(*status); } if (prop_name) { if (prop_value) { g_variant_ref(prop_value); } g_hash_table_insert(ud->changed_properties, (gpointer)prop_name, prop_value); } } static gboolean event_handler(int fd, G_GNUC_UNUSED GIOCondition condition, gpointer data) { UserData *ud = data; gboolean has_event = TRUE; // Discard data in pipe char unused[16]; while (read(fd, unused, sizeof(unused)) > 0); while (has_event) { mpv_event *event = mpv_wait_event(ud->mpv, 0); switch (event->event_id) { case MPV_EVENT_NONE: has_event = FALSE; break; case MPV_EVENT_SHUTDOWN: set_stopped_status(ud); g_main_loop_quit(ud->loop); break; case MPV_EVENT_PROPERTY_CHANGE: { mpv_event_property *prop_event = (mpv_event_property*)event->data; handle_property_change(prop_event->name, prop_event->data, ud); } break; case MPV_EVENT_SEEK: ud->seek_expected = TRUE; break; case MPV_EVENT_PLAYBACK_RESTART: { if (ud->seek_expected) { emit_seeked_signal(ud); ud->seek_expected = FALSE; } } break; default: break; } } return TRUE; } static void wakeup_handler(void *fd) { (void)!write(*((int*)fd), "0", 1); } // Plugin entry point int mpv_open_cplugin(mpv_handle *mpv) { GMainContext *ctx; GMainLoop *loop; UserData ud = {0}; GError *error = NULL; GDBusNodeInfo *introspection_data = NULL; int pipe[2]; GSource *mpv_pipe_source; GSource *timeout_source; ctx = g_main_context_new(); loop = g_main_loop_new(ctx, FALSE); // Load introspection data and split into separate interfaces introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error); if (error != NULL) { g_printerr("%s", error->message); } ud.root_interface_info = g_dbus_node_info_lookup_interface(introspection_data, "org.mpris.MediaPlayer2"); ud.player_interface_info = g_dbus_node_info_lookup_interface(introspection_data, "org.mpris.MediaPlayer2.Player"); ud.mpv = mpv; ud.loop = loop; ud.status = STATUS_STOPPED; ud.loop_status = LOOP_NONE; ud.changed_properties = g_hash_table_new(g_str_hash, g_str_equal); ud.seek_expected = FALSE; ud.idle = FALSE; ud.paused = FALSE; g_main_context_push_thread_default(ctx); ud.bus_id = g_bus_own_name(G_BUS_TYPE_SESSION, "org.mpris.MediaPlayer2.mpv", G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, on_bus_acquired, NULL, on_name_lost, &ud, NULL); g_main_context_pop_thread_default(ctx); // Receive event for property changes mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG); mpv_observe_property(mpv, 0, "idle-active", MPV_FORMAT_FLAG); mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING); mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE); mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE); mpv_observe_property(mpv, 0, "loop-file", MPV_FORMAT_STRING); mpv_observe_property(mpv, 0, "loop-playlist", MPV_FORMAT_STRING); mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_INT64); mpv_observe_property(mpv, 0, "fullscreen", MPV_FORMAT_FLAG); // Run callback whenever there are events g_unix_open_pipe(pipe, 0, &error); if (error != NULL) { g_printerr("%s", error->message); } fcntl(pipe[0], F_SETFL, O_NONBLOCK); mpv_set_wakeup_callback(mpv, wakeup_handler, &pipe[1]); mpv_pipe_source = g_unix_fd_source_new(pipe[0], G_IO_IN); g_source_set_callback(mpv_pipe_source, G_SOURCE_FUNC(event_handler), &ud, NULL); g_source_attach(mpv_pipe_source, ctx); // Emit any new property changes every 100ms timeout_source = g_timeout_source_new(100); g_source_set_callback(timeout_source, G_SOURCE_FUNC(emit_property_changes), &ud, NULL); g_source_attach(timeout_source, ctx); g_main_loop_run(loop); g_source_unref(mpv_pipe_source); g_source_unref(timeout_source); g_dbus_connection_unregister_object(ud.connection, ud.root_interface_id); g_dbus_connection_unregister_object(ud.connection, ud.player_interface_id); g_bus_unown_name(ud.bus_id); g_main_loop_unref(loop); g_main_context_unref(ctx); g_dbus_node_info_unref(introspection_data); return 0; }