diff --git a/app/meson.build b/app/meson.build index fc752e860..e9cc8a9bf 100644 --- a/app/meson.build +++ b/app/meson.build @@ -105,6 +105,11 @@ if usb_support ] endif +mpris_support = get_option('mpris') and host_machine.system() == 'linux' +if mpris_support + src += [ 'src/mpris.c' ] +endif + cc = meson.get_compiler('c') dependencies = [ @@ -123,6 +128,11 @@ if usb_support dependencies += dependency('libusb-1.0') endif +if mpris_support + dependencies += dependency('glib-2.0') + dependencies += dependency('gio-2.0') +endif + if host_machine.system() == 'windows' dependencies += cc.find_library('mingw32') dependencies += cc.find_library('ws2_32') @@ -174,6 +184,9 @@ conf.set('HAVE_V4L2', v4l2_support) # enable HID over AOA support (linux only) conf.set('HAVE_USB', usb_support) +# enable DBus MPRIS support (linux only) +conf.set('HAVE_MPRIS', mpris_support) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/src/events.h b/app/src/events.h index 59c55de42..d1ec9a15b 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -20,6 +20,7 @@ enum { SC_EVENT_TIME_LIMIT_REACHED, SC_EVENT_CONTROLLER_ERROR, SC_EVENT_AOA_OPEN_ERROR, + SC_EVENT_RAISE_WINDOW, }; bool diff --git a/app/src/mpris.c b/app/src/mpris.c new file mode 100644 index 000000000..4a4922d66 --- /dev/null +++ b/app/src/mpris.c @@ -0,0 +1,377 @@ +#include "mpris.h" +#include "events.h" +#include +#include "util/log.h" +#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"; + +static inline void +push_event(uint32_t type, const char *name) { + SDL_Event event; + event.type = type; + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGE("Could not post %s event: %s", name, SDL_GetError()); + // What could we do? + } +} +#define PUSH_EVENT(TYPE) push_event(TYPE, #TYPE) + +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, G_GNUC_UNUSED gpointer user_data) { + if (g_strcmp0(method_name, "Quit") == 0) { + LOGD("mpris: quit"); + g_dbus_method_invocation_return_value(invocation, NULL); + + } else if (g_strcmp0(method_name, "Raise") == 0) { + LOGD("mpris: raise window"); + PUSH_EVENT(SC_EVENT_RAISE_WINDOW); + g_dbus_method_invocation_return_value(invocation, NULL); + } else { + LOGW("mpris: unknown method %s", method_name); + 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, + G_GNUC_UNUSED gpointer user_data) { + GVariant *ret; + + printf("Getting property %s\n", property_name); + if (g_strcmp0(property_name, "CanQuit") == 0) { + ret = g_variant_new_boolean(FALSE); + + } else if (g_strcmp0(property_name, "Fullscreen") == 0) { + int fullscreen = 0; + ret = g_variant_new_boolean(fullscreen); + } else if (g_strcmp0(property_name, "CanSetFullscreen") == 0) { + ret = g_variant_new_boolean(FALSE); + } else if (g_strcmp0(property_name, "CanRaise") == 0) { + ret = g_variant_new_boolean(TRUE); + } 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("Identity"); + } else if (g_strcmp0(property_name, "DesktopEntry") == 0) { + ret = g_variant_new_string("scrcpy"); + } else if (g_strcmp0(property_name, "SupportedUriSchemes") == 0) { + ret = NULL; + } else if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) { + ret = NULL; + } 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, G_GNUC_UNUSED gpointer user_data) { + if (g_strcmp0(property_name, "Fullscreen") == 0) { + int fullscreen; + g_variant_get(value, "b", &fullscreen); + LOGD("mpris: setting fullscreen to %d", 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, G_GNUC_UNUSED gpointer user_data) { + if (g_strcmp0(method_name, "Pause") == 0) { + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "PlayPause") == 0) { + LOGD("mpris: PlayPause"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Play") == 0) { + LOGD("mpris: Play"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Stop") == 0) { + LOGD("mpris: Stop"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Next") == 0) { + LOGD("mpris: Next"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Previous") == 0) { + LOGD("mpris: Previous"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Seek") == 0) { + LOGD("mpris: Seek"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "SetPosition") == 0) { + LOGD("mpris: SetPosition"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "OpenUri") == 0) { + LOGD("mpris: OpenUri"); + g_dbus_method_invocation_return_value(invocation, NULL); + } else { + LOGW("mpris: unknown method %s", method_name); + 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, + G_GNUC_UNUSED gpointer user_data) { + GVariant *ret; + if (g_strcmp0(property_name, "PlaybackStatus") == 0) { + ret = g_variant_new_string("Stopped"); + } else if (g_strcmp0(property_name, "LoopStatus") == 0) { + ret = g_variant_new_string("None"); + } else if (g_strcmp0(property_name, "Rate") == 0) { + ret = g_variant_new_double(1.0); + } else if (g_strcmp0(property_name, "Shuffle") == 0) { + ret = g_variant_new_boolean(FALSE); + } else if (g_strcmp0(property_name, "Metadata") == 0) { + ret = NULL; + } else if (g_strcmp0(property_name, "Volume") == 0) { + ret = g_variant_new_double(100); + } else if (g_strcmp0(property_name, "Position") == 0) { + ret = g_variant_new_int64(0); + } else if (g_strcmp0(property_name, "MinimumRate") == 0) { + ret = g_variant_new_double(100); + } 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(FALSE); + } 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, G_GNUC_UNUSED gpointer user_data) { + if (g_strcmp0(property_name, "LoopStatus") == 0) { + LOGD("mpris: setting loop status"); + } else if (g_strcmp0(property_name, "Rate") == 0) { + double rate = g_variant_get_double(value); + LOGD("mpris: setting rate to %f", rate); + } else if (g_strcmp0(property_name, "Shuffle") == 0) { + int shuffle = g_variant_get_boolean(value); + LOGD("mpris: setting shuffle to %d", shuffle); + } else if (g_strcmp0(property_name, "Volume") == 0) { + double volume = g_variant_get_double(value); + LOGD("mpris: setting volume to %f", 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 void +on_bus_acquired(GDBusConnection *connection, G_GNUC_UNUSED const char *name, + gpointer user_data) { + GError *error = NULL; + struct sc_mpris *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) { + struct sc_mpris *ud = user_data; + pid_t pid = getpid(); + char *name = + g_strdup_printf("org.mpris.MediaPlayer2.scrcpy.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 int +run_mpris(void *data) { + struct sc_mpris *mpris = data; + GMainContext *ctx; + + ctx = g_main_context_new(); + mpris->loop = g_main_loop_new(ctx, FALSE); + + g_main_context_push_thread_default(ctx); + mpris->bus_id = + g_bus_own_name(G_BUS_TYPE_SESSION, "org.mpris.MediaPlayer2.scrcpy", + G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, on_bus_acquired, + NULL, on_name_lost, mpris, NULL); + g_main_context_pop_thread_default(ctx); + + g_main_loop_run(mpris->loop); + + g_dbus_connection_unregister_object(mpris->connection, + mpris->root_interface_id); + g_dbus_connection_unregister_object(mpris->connection, + mpris->player_interface_id); + + g_bus_unown_name(mpris->bus_id); + g_main_loop_unref(mpris->loop); + g_main_context_unref(ctx); + g_dbus_node_info_unref(mpris->introspection_data); + + return 0; +} + +bool +sc_mpris_start(struct sc_mpris *mpris) { + LOGD("mpris: starting glib thread"); + + GError *error = NULL; + + // Load introspection data and split into separate interfaces + mpris->introspection_data = + g_dbus_node_info_new_for_xml(introspection_xml, &error); + if (error != NULL) { + g_printerr("%s", error->message); + } + mpris->root_interface_info = g_dbus_node_info_lookup_interface( + mpris->introspection_data, "org.mpris.MediaPlayer2"); + mpris->player_interface_info = g_dbus_node_info_lookup_interface( + mpris->introspection_data, "org.mpris.MediaPlayer2.Player"); + + mpris->changed_properties = g_hash_table_new(g_str_hash, g_str_equal); + mpris->metadata = NULL; + + bool ok = + sc_thread_create(&mpris->thread, run_mpris, "scrcpy-mpris", mpris); + if (!ok) { + LOGE("mpris: cloud not start mpris thread"); + return false; + } + return true; +} + +void +sc_mpris_stop(struct sc_mpris *mpris) { + LOGD("mpris: stopping glib thread"); + g_main_loop_quit(mpris->loop); + sc_thread_join(&mpris->thread, NULL); +} diff --git a/app/src/mpris.h b/app/src/mpris.h new file mode 100644 index 000000000..4c6027feb --- /dev/null +++ b/app/src/mpris.h @@ -0,0 +1,29 @@ +#ifndef SC_MPRIS_H +#define SC_MPRIS_H + +#include "common.h" +#include "util/thread.h" +#include +#include + +struct sc_mpris { + sc_thread thread; + GMainLoop *loop; + gint bus_id; + GDBusNodeInfo *introspection_data; + 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; +}; + +bool sc_mpris_start(struct sc_mpris *mpris); + +void sc_mpris_stop(struct sc_mpris *mpris); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 854657fbd..b097ea13e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -43,6 +43,9 @@ #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif +#ifdef HAVE_MPRIS +# include "mpris.h" +#endif struct scrcpy { struct sc_server server; @@ -87,6 +90,9 @@ struct scrcpy { struct sc_gamepad_aoa gamepad_aoa; #endif }; +#ifdef HAVE_MPRIS + struct sc_mpris mpris; +#endif struct sc_timeout timeout; }; @@ -398,6 +404,9 @@ scrcpy(struct scrcpy_options *options) { bool keyboard_aoa_initialized = false; bool mouse_aoa_initialized = false; bool gamepad_aoa_initialized = false; +#endif +#ifdef HAVE_MPRIS + bool mpris_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -851,6 +860,13 @@ scrcpy(struct scrcpy_options *options) { } #endif +#ifdef HAVE_MPRIS + if (!sc_mpris_start(&s->mpris)) { + goto end; + } + mpris_initialized = true; +#endif + // Now that the header values have been consumed, the socket(s) will // receive the stream(s). Start the demuxer(s). @@ -983,6 +999,12 @@ scrcpy(struct scrcpy_options *options) { } #endif +#ifdef HAVE_MPRIS + if(mpris_initialized) { + sc_mpris_stop(&s->mpris); + } +#endif + #ifdef HAVE_USB if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); diff --git a/app/src/screen.c b/app/src/screen.c index cb455cb12..b4dc906c5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -864,6 +864,10 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { } return true; } + case SC_EVENT_RAISE_WINDOW: { + SDL_RaiseWindow(screen->window); + return true; + } case SDL_WINDOWEVENT: if (!screen->video && event->window.event == SDL_WINDOWEVENT_EXPOSED) { diff --git a/meson_options.txt b/meson_options.txt index d10306946..2293b9830 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,3 +6,4 @@ option('server_debugger', type: 'boolean', value: false, description: 'Run a ser option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') +option('mpris', type: 'boolean', value: true, description: 'Enable DBus MPRIS when supported')