/* mpris.c * * Copyright 2021 Joshua Strobl * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Huge thanks to the GLib folks for providing an example server test #include #include #include #include "../db/cartographer.h" #include "../playlist/current.h" #include "../playlist/playlist.h" #include "../koto-utils.h" #include "engine.h" #include "mimes.h" #include "mpris.h" extern KotoCartographer *koto_maps; extern KotoCurrentPlaylist *current_playlist; extern GtkApplication *app; extern GtkWindow *main_window; extern KotoPlaybackEngine *playback_engine; extern GList *supported_mimes; GDBusConnection *dbus_conn = NULL; guint mpris_bus_id = 0; GDBusNodeInfo *introspection_data = NULL; static const gchar introspection_xml[] = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; void handle_method_call( GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data ) { (void) connection; (void) sender; (void) object_path; (void) parameters; (void) invocation; (void) user_data; if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2") == 0) { // Root mediaplayer2 interface if (g_strcmp0(method_name, "Raise") == 0) { // Raise the window gtk_window_unminimize(main_window); // Ensure we unminimize the window gtk_window_present(main_window); // Present our main window return; } if (g_strcmp0(method_name, "Quit") == 0) { // Quit the application gtk_application_remove_window(app, main_window); // Remove the window, thereby closing the app return; } } else if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2.Player") == 0) { // Root mediaplayer2 interface if (g_strcmp0(method_name, "Next") == 0) { // Next track koto_playback_engine_forwards(playback_engine); return; } if (g_strcmp0(method_name, "Previous") == 0) { // Previous track koto_playback_engine_backwards(playback_engine); return; } if (g_strcmp0(method_name, "Pause") == 0) { // Explicit pause koto_playback_engine_pause(playback_engine); return; } if (g_strcmp0(method_name, "PlayPause") == 0) { // Explicit playpause koto_playback_engine_toggle(playback_engine); return; } if (g_strcmp0(method_name, "Stop") == 0) { // Explicit stop koto_playback_engine_stop(playback_engine); return; } if (g_strcmp0(method_name, "Play") == 0) { // Explicit play koto_playback_engine_play(playback_engine); return; } } } GVariant* handle_get_property( GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data ) { (void) connection; (void) sender; (void) object_path; (void) interface_name; (void) error; (void) user_data; GVariant *ret; ret = NULL; if (g_strcmp0(property_name, "CanQuit") == 0) { // If property is CanQuit ret = g_variant_new_boolean(TRUE); // Allow quitting. You can escape Hotel California for now. } if (g_strcmp0(property_name, "CanRaise") == 0) { // If property is CanRaise ret = g_variant_new_boolean(TRUE); // Allow raising. } if (g_strcmp0(property_name, "HasTrackList") == 0) { // If property is HasTrackList KotoPlaylist *playlist = koto_current_playlist_get_playlist(current_playlist); if (KOTO_IS_PLAYLIST(playlist)) { ret = g_variant_new_boolean(koto_playlist_get_length(playlist) > 0); } else { // Don't have a playlist ret = g_variant_new_boolean(FALSE); } } if (g_strcmp0(property_name, "Identity") == 0) { // Want identity ret = g_variant_new_string("Koto"); // Not Jason Bourne but close enough } if (g_strcmp0(property_name, "DesktopEntry") == 0) { // Desktop Entry ret = g_variant_new_string("com.github.joshstrobl.koto"); } if (g_strcmp0(property_name, "SupportedUriSchemas") == 0) { // Supported URI Schemas GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE("as")); // Array of strings g_variant_builder_add(builder, "s", "file"); ret = g_variant_new("as", builder); g_variant_builder_unref(builder); // Unref builder since we no longer need it } if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) { // Supported mimetypes GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE("as")); // Array of strings GList *mimes; mimes = NULL; for (mimes = supported_mimes; mimes != NULL; mimes = mimes->next) { // For each mimetype g_variant_builder_add(builder, "s", mimes->data); // Add the mime as a string } ret = g_variant_new("as", builder); g_variant_builder_unref(builder); g_list_free(mimes); } if (g_strcmp0(property_name, "Metadata") == 0) { // Metadata KotoIndexedTrack *current_track = koto_playback_engine_get_current_track(playback_engine); if (KOTO_IS_INDEXED_TRACK(current_track)) { // Currently playing a track ret = koto_indexed_track_get_metadata_vardict(current_track); } else { // No track GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); // Create an empty builder ret = g_variant_builder_end(builder); // return the vardict } } if ( (g_strcmp0(property_name, "CanPlay") == 0) || (g_strcmp0(property_name, "CanPause") == 0) ) { KotoIndexedTrack *current_track = koto_playback_engine_get_current_track(playback_engine); ret = g_variant_new_boolean(KOTO_IS_INDEXED_TRACK(current_track)); } if (g_strcmp0(property_name, "CanSeek") == 0) { // Can control position over mpris ret = g_variant_new_boolean(FALSE); } if (g_strcmp0(property_name, "CanGoNext") == 0) { // Can Go Next // TODO: Add some actual logic here ret = g_variant_new_boolean(TRUE); } if (g_strcmp0(property_name, "CanGoPrevious") == 0) { // Can Go Previous // TODO: Add some actual logic here ret = g_variant_new_boolean(TRUE); } if (g_strcmp0(property_name, "CanControl") == 0){ // Can Control ret = g_variant_new_boolean(TRUE); } if (g_strcmp0(property_name, "PlaybackStatus") == 0) { // Get our playback status GstState current_state = koto_playback_engine_get_state(playback_engine); // Get the current state if (current_state == GST_STATE_PLAYING) { ret = g_variant_new_string("Playing"); } else if (current_state == GST_STATE_PAUSED) { ret = g_variant_new_string("Paused"); } else { ret = g_variant_new_string("Stopped"); } } return ret; } gboolean handle_set_property( GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant *value, GError **error, gpointer user_data ) { (void) connection; (void) sender; (void) interface_name; (void) object_path; (void) error; (void) user_data; if (g_strcmp0(property_name, "LoopStatus") == 0) { // Changing LoopStatus koto_playback_engine_set_track_repeat(playback_engine, g_variant_get_boolean(value)); // Set the loop status state return TRUE; } if (g_strcmp0(property_name, "Shuffle") == 0) { // Changing Shuffle koto_playback_engine_set_track_shuffle(playback_engine, g_variant_get_boolean(value)); // Set the shuffle state return TRUE; } return FALSE; } void koto_update_mpris_playback_state(GstState state) { GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); if (state == GST_STATE_PLAYING) { g_variant_builder_add(builder, "{sv}", "PlaybackStatus", g_variant_new_string("Playing")); } else if (state == GST_STATE_PAUSED) { g_variant_builder_add(builder, "{sv}", "PlaybackStatus", g_variant_new_string("Paused")); } else { g_variant_builder_add(builder, "{sv}", "PlaybackStatus", g_variant_new_string("Stopped")); } g_dbus_connection_emit_signal(dbus_conn, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, NULL), NULL ); } void koto_update_mpris_info_for_track(KotoIndexedTrack *track) { if (!KOTO_IS_INDEXED_TRACK(track)) { return; } GVariant *metadata = koto_indexed_track_get_metadata_vardict(track); // Get the GVariantBuilder variable dict for the metadata koto_update_mpris_info_for_track_with_metadata(track, metadata); } void koto_update_mpris_info_for_track_with_metadata(KotoIndexedTrack *track, GVariant *metadata) { if (!KOTO_IS_INDEXED_TRACK(track)) { return; } GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); g_variant_builder_add(builder, "{sv}", "Metadata", metadata); g_dbus_connection_emit_signal(dbus_conn, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, NULL), NULL ); } static const GDBusInterfaceVTable main_mpris_interface_vtable = { handle_method_call, handle_get_property, handle_set_property, { 0 } }; void on_main_mpris_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { (void) name; (void) user_data; dbus_conn = connection; g_dbus_connection_register_object(dbus_conn, "/org/mpris/MediaPlayer2", introspection_data->interfaces[0], &main_mpris_interface_vtable, NULL, NULL, NULL ); g_dbus_connection_register_object(dbus_conn, "/org/mpris/MediaPlayer2", introspection_data->interfaces[1], &main_mpris_interface_vtable, NULL, NULL, NULL ); } void setup_mpris_interfaces() { introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); g_assert(introspection_data != NULL); mpris_bus_id = g_bus_own_name(G_BUS_TYPE_SESSION, "org.mpris.MediaPlayer2.koto", G_BUS_NAME_OWNER_FLAGS_NONE, on_main_mpris_bus_acquired, NULL, NULL, NULL, NULL ); g_assert(mpris_bus_id > 0); }