From c5866461f97725a679f6ec108fc9845a42b404c2 Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Sun, 1 Sep 2024 19:29:22 +0200 Subject: [PATCH 1/2] initial MPRIS support --- app/meson.build | 13 ++ app/src/events.h | 1 + app/src/mpris.c | 377 ++++++++++++++++++++++++++++++++++++++++++++++ app/src/mpris.h | 29 ++++ app/src/scrcpy.c | 22 +++ app/src/screen.c | 4 + meson_options.txt | 1 + 7 files changed, 447 insertions(+) create mode 100644 app/src/mpris.c create mode 100644 app/src/mpris.h diff --git a/app/meson.build b/app/meson.build index fc752e860c..e9cc8a9bff 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 59c55de42c..d1ec9a15b0 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 0000000000..4a4922d665 --- /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 0000000000..4c6027feb5 --- /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 854657fbdb..b097ea13e4 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 cb455cb12a..b4dc906c5d 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 d103069460..2293b9830a 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') From 38b997005b9579dd16b64f623d0103fc89d11592 Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Fri, 4 Oct 2024 17:42:05 +0200 Subject: [PATCH 2/2] hook up device events --- app/src/control_msg.h | 10 + app/src/device_msg.c | 64 ++++-- app/src/device_msg.h | 10 + app/src/receiver.c | 22 +++ .../java/com/genymobile/scrcpy/Options.java | 5 + .../java/com/genymobile/scrcpy/Server.java | 41 ++++ .../scrcpy/control/ControlMessage.java | 29 +++ .../scrcpy/control/ControlMessageReader.java | 16 ++ .../genymobile/scrcpy/control/Controller.java | 14 ++ .../scrcpy/control/DeviceMessage.java | 27 +++ .../scrcpy/control/DeviceMessageWriter.java | 12 +- .../scrcpy/wrappers/MediaManager.java | 186 ++++++++++++++++++ .../scrcpy/wrappers/ServiceManager.java | 8 + 13 files changed, 427 insertions(+), 17 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1ae8cae43c..da19726040 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,8 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + SC_CONTROL_MSG_TYPE_MEDIA_STATE, + SC_CONTROL_MSG_TYPE_MEDIA_SEEK, }; enum sc_screen_power_mode { @@ -110,6 +112,14 @@ struct sc_control_msg { struct { uint16_t id; } uhid_destroy; + struct { + uint16_t player_id; + uint64_t position; + } media_seek; + struct { + uint16_t player_id; + uint8_t state; + } media_state; }; }; diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 7621c040ec..06b42ddc98 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -7,6 +7,20 @@ #include "util/binary.h" #include "util/log.h" +static int read_message(uint8_t **target, const uint8_t *src, const uint16_t size) { + uint8_t *data = malloc(size + 1); + if (!data) { + LOG_OOM(); + return -1; + } + if (size) { + data[size] = '\0'; + memcpy(data, src, size); + } + *target = data; + return 0; +} + ssize_t sc_device_msg_deserialize(const uint8_t *buf, size_t len, struct sc_device_msg *msg) { @@ -25,17 +39,10 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, if (clipboard_len > len - 5) { return 0; // no complete message } - char *text = malloc(clipboard_len + 1); - if (!text) { - LOG_OOM(); + if (read_message((uint8_t **)&msg->clipboard.text, &buf[5], clipboard_len) == -1) { return -1; } - if (clipboard_len) { - memcpy(text, &buf[5], clipboard_len); - } - text[clipboard_len] = '\0'; - msg->clipboard.text = text; return 5 + clipboard_len; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { @@ -56,21 +63,43 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, if (size < len - 5) { return 0; // not available } - uint8_t *data = malloc(size); - if (!data) { - LOG_OOM(); + + msg->uhid_output.id = id; + msg->uhid_output.size = size; + if (read_message(&msg->uhid_output.data, &buf[5], size) == -1) { return -1; } - if (size) { - memcpy(data, &buf[5], size); + + return 5 + size; + case DEVICE_MSG_TYPE_MEDIA_UPDATE: { + if (len < 5) { + // at least id + size + return 0; // not available + } + uint16_t id = sc_read16be(&buf[1]); + size_t size = sc_read16be(&buf[3]); + if (size < len - 5) { + return 0; // not available } - msg->uhid_output.id = id; - msg->uhid_output.size = size; - msg->uhid_output.data = data; + msg->media_update.id = id; + msg->media_update.size = size; + if (read_message(&msg->media_update.data, &buf[5], size) == -1) { + return -1; + } return 5 + size; } + case DEVICE_MSG_TYPE_MEDIA_REMOVE: { + if (len < 3) { + // at least id + return 0; // not available + } + uint16_t id = sc_read16be(&buf[1]); + msg->media_remove.id = id; + return 3; + } + } default: LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover @@ -86,6 +115,9 @@ sc_device_msg_destroy(struct sc_device_msg *msg) { case DEVICE_MSG_TYPE_UHID_OUTPUT: free(msg->uhid_output.data); break; + case DEVICE_MSG_TYPE_MEDIA_UPDATE: + free(msg->media_update.data); + break; default: // nothing to do break; diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 86b2ccb7cc..e241bcb136 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -15,6 +15,8 @@ enum sc_device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD, DEVICE_MSG_TYPE_UHID_OUTPUT, + DEVICE_MSG_TYPE_MEDIA_UPDATE, + DEVICE_MSG_TYPE_MEDIA_REMOVE, }; struct sc_device_msg { @@ -31,6 +33,14 @@ struct sc_device_msg { uint16_t size; uint8_t *data; // owned, to be freed by free() } uhid_output; + struct { + uint16_t id; + uint16_t size; + uint8_t *data; // owned, to be freed by free() + } media_update; + struct { + uint16_t id; + } media_remove; }; }; diff --git a/app/src/receiver.c b/app/src/receiver.c index b89b0c6e18..e2771e68c8 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -74,6 +74,22 @@ task_uhid_output(void *userdata) { free(data); } +static void +dump_media_update(const struct sc_device_msg* msg) { + uint8_t msg_type = 0; + uint8_t *msg_ptr = NULL; + for (int i = 0; i < msg->media_update.size; i++) { + if (msg_ptr == NULL) { + msg_type = msg->media_update.data[i]; + msg_ptr = &msg->media_update.data[i + 1]; + } else if (msg->media_update.data[i] == 0) { + LOGI("Media update: %i, %s", (int)msg_type, msg_ptr); + msg_ptr = NULL; + msg_type = 0; + } + } +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { @@ -150,6 +166,12 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { return; } + break; + case DEVICE_MSG_TYPE_MEDIA_UPDATE: + dump_media_update(msg); + break; + case DEVICE_MSG_TYPE_MEDIA_REMOVE: + LOGI("Media remove: %i", msg->media_remove.id); break; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 51daeced16..efebd77bed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -21,6 +21,7 @@ public class Options { private int scid = -1; // 31-bit non-negative value, or -1 private boolean video = true; private boolean audio = true; + private boolean mediaControl = true; private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; private AudioCodec audioCodec = AudioCodec.OPUS; @@ -81,6 +82,10 @@ public boolean getAudio() { return audio; } + public boolean getMediaControls() { + return mediaControl; + } + public int getMaxSize() { return maxSize; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 7817fdf5b9..c86ab00156 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -23,7 +23,10 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import com.genymobile.scrcpy.wrappers.MediaManager; +import android.media.MediaMetadata; +import android.media.session.PlaybackState; import android.os.BatteryManager; import android.os.Build; @@ -139,6 +142,7 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc boolean control = options.getControl(); boolean video = options.getVideo(); boolean audio = options.getAudio(); + boolean media = options.getMediaControls(); boolean sendDummyByte = options.getSendDummyByte(); boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; @@ -162,6 +166,40 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc controller.getSender().send(msg); }); asyncProcessors.add(controller); + + if (media) { + MediaManager mediaManager = MediaManager.create(); + + mediaManager.setMediaChangeListener(new MediaManager.MediaChange() { + @Override + public void onMetadataChange(int id, MediaMetadata metadata) { + Ln.i("onMetadataChange " + id); + byte[] data = MediaManager.mediaMetadataSerialize(metadata); + DeviceMessage msg = DeviceMessage.createMediaUpdate(id, data); + controller.getSender().send(msg); + } + + @Override + public void onPlaybackStateChange(int id, PlaybackState playbackState) { + Ln.i("onPlaybackStateChange " + id); + int state = MediaManager.create().playbackStateSerialize(playbackState); + if(state < 0) { + return; + } + DeviceMessage msg = DeviceMessage.createMediaState(id, state); + controller.getSender().send(msg); + } + + @Override + public void onRemove(int id) { + Ln.i("onRemove " + id); + DeviceMessage msg = DeviceMessage.createMediaRemove(id); + controller.getSender().send(msg); + } + }); + + mediaManager.start(); + } } if (audio) { @@ -200,6 +238,9 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc asyncProcessors.add(surfaceEncoder); } + + + Completion completion = new Completion(asyncProcessors.size()); for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.start((fatalError) -> { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index d1406ed0a1..798c17f33a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -23,6 +23,8 @@ public final class ControlMessage { public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; + public static final int TYPE_MEDIA_STATE = 16; + public static final int TYPE_MEDIA_SEEK = 17; public static final long SEQUENCE_INVALID = 0; @@ -48,6 +50,9 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; + private int mediaState; + private long mediaSeek; + private ControlMessage() { } @@ -155,6 +160,22 @@ public static ControlMessage createUhidDestroy(int id) { return msg; } + public static ControlMessage createMediaState(int receiverId, byte state) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_MEDIA_STATE; + msg.id = receiverId; + msg.mediaState = state; + return msg; + } + + public static ControlMessage createMediaSeek(int receiverId, long position) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_MEDIA_STATE; + msg.id = receiverId; + msg.mediaSeek = position; + return msg; + } + public int getType() { return type; } @@ -226,4 +247,12 @@ public int getId() { public byte[] getData() { return data; } + + public long getMediaSeek() { + return mediaSeek; + } + + public int getMediaState() { + return mediaState; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 45116935f7..f8844eacf2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -53,11 +53,27 @@ public ControlMessage read() throws IOException { return parseUhidInput(); case ControlMessage.TYPE_UHID_DESTROY: return parseUhidDestroy(); + case ControlMessage.TYPE_MEDIA_STATE: + return parseMediaPlayStateRequest(); + case ControlMessage.TYPE_MEDIA_SEEK: + return parseMediaSeekRequest(); default: throw new ControlProtocolException("Unknown event type: " + type); } } + private ControlMessage parseMediaSeekRequest() throws IOException { + int receiverId = dis.readUnsignedShort(); + long position = dis.readLong(); + return ControlMessage.createMediaSeek(receiverId, position); + } + + private ControlMessage parseMediaPlayStateRequest() throws IOException { + int receiverId = dis.readUnsignedShort(); + byte state = dis.readByte(); + return ControlMessage.createMediaState(receiverId, state); + } + private ControlMessage parseInjectKeycode() throws IOException { int action = dis.readUnsignedByte(); int keycode = dis.readInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 3825165522..eea55d68f7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -221,6 +221,12 @@ private boolean handleEvent() throws IOException { case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; + case ControlMessage.TYPE_MEDIA_STATE: + mediaUpdateState(msg.getId(), msg.getMediaState()); + break; + case ControlMessage.TYPE_MEDIA_SEEK: + mediaSeek(msg.getId(), msg.getMediaSeek()); + break; default: // do nothing } @@ -228,6 +234,14 @@ private boolean handleEvent() throws IOException { return true; } + private void mediaUpdateState(int id, int mediaState) { + // TODO + } + + private void mediaSeek(int id, long mediaSeek) { + // TODO + } + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { schedulePowerModeOff(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java index 079a7a04fb..8994a788c1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java @@ -5,12 +5,16 @@ public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_ACK_CLIPBOARD = 1; public static final int TYPE_UHID_OUTPUT = 2; + public static final int TYPE_MEDIA_UPDATE = 3; + public static final int TYPE_MEDIA_REMOVE = 4; + public static final int TYPE_MEDIA_STATE = 4; private int type; private String text; private long sequence; private int id; private byte[] data; + private int mediaState; private DeviceMessage() { } @@ -37,6 +41,29 @@ public static DeviceMessage createUhidOutput(int id, byte[] data) { return event; } + public static DeviceMessage createMediaUpdate(int id, byte[] data) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_MEDIA_UPDATE; + event.id = id; + event.data = data; + return event; + } + + public static DeviceMessage createMediaState(int id, int state) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_MEDIA_STATE; + event.id = id; + event.mediaState = state; + return event; + } + + public static DeviceMessage createMediaRemove(int id) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_MEDIA_REMOVE; + event.id = id; + return event; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index a18a2e5dcd..6b9ebe7ed4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -22,6 +22,7 @@ public DeviceMessageWriter(OutputStream rawOutputStream) { public void write(DeviceMessage msg) throws IOException { int type = msg.getType(); dos.writeByte(type); + byte[] data; switch (type) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); @@ -35,10 +36,19 @@ public void write(DeviceMessage msg) throws IOException { break; case DeviceMessage.TYPE_UHID_OUTPUT: dos.writeShort(msg.getId()); - byte[] data = msg.getData(); + data = msg.getData(); dos.writeShort(data.length); dos.write(data); break; + case DeviceMessage.TYPE_MEDIA_UPDATE: + dos.writeShort(msg.getId()); + data = msg.getData(); + dos.writeShort(data.length); + dos.write(data); + break; + case DeviceMessage.TYPE_MEDIA_REMOVE: + dos.writeShort(msg.getId()); + break; default: throw new ControlProtocolException("Unknown event type: " + type); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java new file mode 100644 index 0000000000..2efee1f4be --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java @@ -0,0 +1,186 @@ +package com.genymobile.scrcpy.wrappers; + +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; + +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class MediaManager { + final static byte KEY_DURATION = 0; + final static byte KEY_ALBUM = 1; + final static byte KEY_ARTIST = 2; + final static byte KEY_TITLE = 3; + + final static byte STATE_PLAYING = 0; + final static byte STATE_STOPPED = 1; + final static byte STATE_PAUSED = 2; + + MediaSessionManager sessionManager; + private MediaChange mediaChangeListener; + boolean started = false; + + public interface MediaChange { + void onMetadataChange(int id, MediaMetadata metadata); + void onPlaybackStateChange(int id, PlaybackState playbackState); + void onRemove(int id); + } + public static MediaManager create() { + MediaSessionManager manager = FakeContext.get().getSystemService(MediaSessionManager.class); + return new MediaManager(manager); + } + + int nextId = 0; + HashMap idMap = new HashMap<>(); + List mediaControllers = Collections.emptyList(); + + + private MediaManager(MediaSessionManager sessionManager) { + this.sessionManager = sessionManager; + + } + + public void setMediaChangeListener(MediaChange listener) { + this.mediaChangeListener = listener; + } + + public void start() { + if (started) { + return; + } + + sessionManager.addOnActiveSessionsChangedListener(new MediaSessionManager.OnActiveSessionsChangedListener() { + @Override + public void onActiveSessionsChanged(List controllers) { + Ln.i("MediaManager: Active Sessions changed"); + if (controllers == null) { + controllers = Collections.emptyList(); + } + // add + for(MediaController controller : controllers) { + if (!mediaControllers.contains(controller)) { + addMediaController(controller); + } + } + for(MediaController controller: mediaControllers) { + if (!controllers.contains(controller)) { + removeMediaController(controller); + } + } + + mediaControllers = new ArrayList<>(controllers); + } + }, null); + + mediaControllers = sessionManager.getActiveSessions(null); + for (MediaController controller : mediaControllers) { + addMediaController(controller); + } + + started = true; + } + + public static byte[] mediaMetadataSerialize(MediaMetadata metadata) { + ArrayList payload = new ArrayList(); + for (String key : metadata.keySet()) { + byte field_id; + byte[] field_value; + switch(key) { + case MediaMetadata.METADATA_KEY_DURATION: + field_id = KEY_DURATION; + field_value = (""+metadata.getLong(key)).getBytes(); + break; + case MediaMetadata.METADATA_KEY_ALBUM: + field_id = KEY_ALBUM; + field_value = metadata.getString(key).getBytes(); + break; + case MediaMetadata.METADATA_KEY_ARTIST: + field_id = KEY_ARTIST; + field_value = metadata.getString(key).getBytes(); + break; + case MediaMetadata.METADATA_KEY_TITLE: + field_id = KEY_TITLE; + field_value = metadata.getString(key).getBytes(); + break; + default: + field_id = 0; + field_value = null; + } + + if (field_value != null) { + payload.add(field_id); + for (byte b : field_value) { + payload.add(b); + } + payload.add((byte)0); + } + + + } + byte[] result = new byte[payload.size()]; + for (int i = 0; i < payload.size(); i++) { + result[i] = payload.get(i); + } + return result; + } + + public int playbackStateSerialize(PlaybackState state) { + switch(state.getState()) { + case PlaybackState.STATE_PLAYING: + return STATE_PLAYING; + case PlaybackState.STATE_STOPPED: + return STATE_STOPPED; + case PlaybackState.STATE_PAUSED: + return STATE_PAUSED; + default: + return -1; + } + } + + private void removeMediaController(MediaController controller) { + int controllerId = findId(controller); + Ln.i("Remove MediaController ID:" + controllerId + " pkg:" + controller.getPackageName()); + mediaChangeListener.onRemove(controllerId); + } + + private int findId(MediaController controller) { + String packageName = controller.getPackageName(); + Integer id = this.idMap.get(packageName); + if (id == null) { + id = nextId; + nextId++; + this.idMap.put(packageName, id); + } + + return id; + } + + private void addMediaController(MediaController controller) { + final int controllerId = findId(controller); + Ln.i("New MediaController ID:" + controllerId + " pkg:" + controller.getPackageName()); + controller.registerCallback(new MediaController.Callback() { + @Override + public void onMetadataChanged(MediaMetadata metadata) { + super.onMetadataChanged(metadata); + Ln.i("MediaController metadata change " + controllerId); + mediaChangeListener.onMetadataChange(controllerId, metadata); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + super.onPlaybackStateChanged(state); + Ln.i("MediaController playstate change " + controllerId); + mediaChangeListener.onPlaybackStateChange(controllerId, state); + } + }); + mediaChangeListener.onMetadataChange(controllerId, controller.getMetadata()); + mediaChangeListener.onPlaybackStateChange(controllerId, controller.getPlaybackState()); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index a8a56dabb5..a50eaa148b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -32,6 +32,7 @@ public final class ServiceManager { private static ClipboardManager clipboardManager; private static ActivityManager activityManager; private static CameraManager cameraManager; + private static MediaManager mediaManager; private ServiceManager() { /* not instantiable */ @@ -108,4 +109,11 @@ public static CameraManager getCameraManager() { } return cameraManager; } + + public static MediaManager getMediaManager() { + if (mediaManager == null) { + mediaManager = MediaManager.create(); + } + return mediaManager; + } }